diff options
Diffstat (limited to 'java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java')
-rw-r--r-- | java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java | 428 |
1 files changed, 428 insertions, 0 deletions
diff --git a/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java b/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java new file mode 100644 index 000000000..852e7a0ed --- /dev/null +++ b/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java @@ -0,0 +1,428 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.blocking; + +import android.annotation.TargetApi; +import android.content.AsyncQueryHandler; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteDatabaseCorruptException; +import android.net.Uri; +import android.os.Build.VERSION_CODES; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.v4.os.UserManagerCompat; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns; +import com.android.dialer.database.FilteredNumberContract.FilteredNumberTypes; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class FilteredNumberAsyncQueryHandler extends AsyncQueryHandler { + + public static final int INVALID_ID = -1; + // Id used to replace null for blocked id since ConcurrentHashMap doesn't allow null key/value. + @VisibleForTesting static final int BLOCKED_NUMBER_CACHE_NULL_ID = -1; + + @VisibleForTesting + static final Map<String, Integer> blockedNumberCache = new ConcurrentHashMap<>(); + + private static final int NO_TOKEN = 0; + private final Context context; + + public FilteredNumberAsyncQueryHandler(Context context) { + super(context.getContentResolver()); + this.context = context; + } + + @Override + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + if (cookie != null) { + ((Listener) cookie).onQueryComplete(token, cookie, cursor); + } + } + + @Override + protected void onInsertComplete(int token, Object cookie, Uri uri) { + if (cookie != null) { + ((Listener) cookie).onInsertComplete(token, cookie, uri); + } + } + + @Override + protected void onUpdateComplete(int token, Object cookie, int result) { + if (cookie != null) { + ((Listener) cookie).onUpdateComplete(token, cookie, result); + } + } + + @Override + protected void onDeleteComplete(int token, Object cookie, int result) { + if (cookie != null) { + ((Listener) cookie).onDeleteComplete(token, cookie, result); + } + } + + public void hasBlockedNumbers(final OnHasBlockedNumbersListener listener) { + if (!FilteredNumberCompat.canAttemptBlockOperations(context)) { + listener.onHasBlockedNumbers(false); + return; + } + startQuery( + NO_TOKEN, + new Listener() { + @Override + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + listener.onHasBlockedNumbers(cursor != null && cursor.getCount() > 0); + } + }, + FilteredNumberCompat.getContentUri(context, null), + new String[] {FilteredNumberCompat.getIdColumnName(context)}, + FilteredNumberCompat.useNewFiltering(context) + ? null + : FilteredNumberColumns.TYPE + "=" + FilteredNumberTypes.BLOCKED_NUMBER, + null, + null); + } + + /** + * Checks if the given number is blocked, calling the given {@link OnCheckBlockedListener} with + * the id for the blocked number, {@link #INVALID_ID}, or {@code null} based on the result of the + * check. + */ + public void isBlockedNumber( + final OnCheckBlockedListener listener, @Nullable final String number, String countryIso) { + if (number == null) { + listener.onCheckComplete(INVALID_ID); + return; + } + if (!FilteredNumberCompat.canAttemptBlockOperations(context)) { + listener.onCheckComplete(null); + return; + } + Integer cachedId = blockedNumberCache.get(number); + if (cachedId != null) { + if (listener == null) { + return; + } + if (cachedId == BLOCKED_NUMBER_CACHE_NULL_ID) { + cachedId = null; + } + listener.onCheckComplete(cachedId); + return; + } + + String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso); + String formattedNumber = FilteredNumbersUtil.getBlockableNumber(context, e164Number, number); + if (TextUtils.isEmpty(formattedNumber)) { + listener.onCheckComplete(INVALID_ID); + blockedNumberCache.put(number, INVALID_ID); + return; + } + + if (!UserManagerCompat.isUserUnlocked(context)) { + LogUtil.i( + "FilteredNumberAsyncQueryHandler.isBlockedNumber", + "Device locked in FBE mode, cannot access blocked number database"); + listener.onCheckComplete(INVALID_ID); + return; + } + + startQuery( + NO_TOKEN, + new Listener() { + @Override + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + /* + * In the frameworking blocking, numbers can be blocked in both e164 format + * and not, resulting in multiple rows being returned for this query. For + * example, both '16502530000' and '6502530000' can exist at the same time + * and will be returned by this query. + */ + if (cursor == null || cursor.getCount() == 0) { + blockedNumberCache.put(number, BLOCKED_NUMBER_CACHE_NULL_ID); + listener.onCheckComplete(null); + return; + } + cursor.moveToFirst(); + // New filtering doesn't have a concept of type + if (!FilteredNumberCompat.useNewFiltering(context) + && cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns.TYPE)) + != FilteredNumberTypes.BLOCKED_NUMBER) { + blockedNumberCache.put(number, BLOCKED_NUMBER_CACHE_NULL_ID); + listener.onCheckComplete(null); + return; + } + Integer blockedId = cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns._ID)); + blockedNumberCache.put(number, blockedId); + listener.onCheckComplete(blockedId); + } + }, + FilteredNumberCompat.getContentUri(context, null), + FilteredNumberCompat.filter( + new String[] { + FilteredNumberCompat.getIdColumnName(context), + FilteredNumberCompat.getTypeColumnName(context) + }), + getIsBlockedNumberSelection(e164Number != null) + " = ?", + new String[] {formattedNumber}, + null); + } + + /** + * Synchronously check if this number has been blocked. + * + * @return blocked id. + */ + @TargetApi(VERSION_CODES.M) + @Nullable + public Integer getBlockedIdSynchronousForCalllogOnly(@Nullable String number, String countryIso) { + Assert.isWorkerThread(); + if (number == null) { + return null; + } + if (!FilteredNumberCompat.canAttemptBlockOperations(context)) { + return null; + } + Integer cachedId = blockedNumberCache.get(number); + if (cachedId != null) { + if (cachedId == BLOCKED_NUMBER_CACHE_NULL_ID) { + cachedId = null; + } + return cachedId; + } + + String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso); + String formattedNumber = FilteredNumbersUtil.getBlockableNumber(context, e164Number, number); + if (TextUtils.isEmpty(formattedNumber)) { + return null; + } + + try (Cursor cursor = + context + .getContentResolver() + .query( + FilteredNumberCompat.getContentUri(context, null), + FilteredNumberCompat.filter( + new String[] { + FilteredNumberCompat.getIdColumnName(context), + FilteredNumberCompat.getTypeColumnName(context) + }), + getIsBlockedNumberSelection(e164Number != null) + " = ?", + new String[] {formattedNumber}, + null)) { + /* + * In the frameworking blocking, numbers can be blocked in both e164 format + * and not, resulting in multiple rows being returned for this query. For + * example, both '16502530000' and '6502530000' can exist at the same time + * and will be returned by this query. + */ + if (cursor == null || cursor.getCount() == 0) { + blockedNumberCache.put(number, BLOCKED_NUMBER_CACHE_NULL_ID); + return null; + } + cursor.moveToFirst(); + int blockedId = cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns._ID)); + blockedNumberCache.put(number, blockedId); + return blockedId; + } catch (SecurityException e) { + LogUtil.e("FilteredNumberAsyncQueryHandler.getBlockedIdSynchronousForCalllogOnly", null, e); + return null; + } + } + + @VisibleForTesting + public void clearCache() { + blockedNumberCache.clear(); + } + + /* + * TODO: b/27779827, non-e164 numbers can be blocked in the new form of blocking. As a + * temporary workaround, determine which column of the database to query based on whether the + * number is e164 or not. + */ + private String getIsBlockedNumberSelection(boolean isE164Number) { + if (FilteredNumberCompat.useNewFiltering(context) && !isE164Number) { + return FilteredNumberCompat.getOriginalNumberColumnName(context); + } + return FilteredNumberCompat.getE164NumberColumnName(context); + } + + public void blockNumber( + final OnBlockNumberListener listener, String number, @Nullable String countryIso) { + blockNumber(listener, null, number, countryIso); + } + + /** Add a number manually blocked by the user. */ + public void blockNumber( + final OnBlockNumberListener listener, + @Nullable String normalizedNumber, + String number, + @Nullable String countryIso) { + blockNumber( + listener, + FilteredNumberCompat.newBlockNumberContentValues( + context, number, normalizedNumber, countryIso)); + } + + /** + * Block a number with specified ContentValues. Can be manually added or a restored row from + * performing the 'undo' action after unblocking. + */ + public void blockNumber(final OnBlockNumberListener listener, ContentValues values) { + blockedNumberCache.clear(); + if (!FilteredNumberCompat.canAttemptBlockOperations(context)) { + listener.onBlockComplete(null); + return; + } + startInsert( + NO_TOKEN, + new Listener() { + @Override + public void onInsertComplete(int token, Object cookie, Uri uri) { + if (listener != null) { + listener.onBlockComplete(uri); + } + } + }, + FilteredNumberCompat.getContentUri(context, null), + values); + } + + /** + * Unblocks the number with the given id. + * + * @param listener (optional) The {@link OnUnblockNumberListener} called after the number is + * unblocked. + * @param id The id of the number to unblock. + */ + public void unblock(@Nullable final OnUnblockNumberListener listener, Integer id) { + if (id == null) { + throw new IllegalArgumentException("Null id passed into unblock"); + } + unblock(listener, FilteredNumberCompat.getContentUri(context, id)); + } + + /** + * Removes row from database. + * + * @param listener (optional) The {@link OnUnblockNumberListener} called after the number is + * unblocked. + * @param uri The uri of row to remove, from {@link FilteredNumberAsyncQueryHandler#blockNumber}. + */ + public void unblock(@Nullable final OnUnblockNumberListener listener, final Uri uri) { + blockedNumberCache.clear(); + if (!FilteredNumberCompat.canAttemptBlockOperations(context)) { + if (listener != null) { + listener.onUnblockComplete(0, null); + } + return; + } + startQuery( + NO_TOKEN, + new Listener() { + @Override + public void onQueryComplete(int token, Object cookie, Cursor cursor) { + int rowsReturned = cursor == null ? 0 : cursor.getCount(); + if (rowsReturned != 1) { + throw new SQLiteDatabaseCorruptException( + "Returned " + rowsReturned + " rows for uri " + uri + "where 1 expected."); + } + cursor.moveToFirst(); + final ContentValues values = new ContentValues(); + DatabaseUtils.cursorRowToContentValues(cursor, values); + values.remove(FilteredNumberCompat.getIdColumnName(context)); + + startDelete( + NO_TOKEN, + new Listener() { + @Override + public void onDeleteComplete(int token, Object cookie, int result) { + if (listener != null) { + listener.onUnblockComplete(result, values); + } + } + }, + uri, + null, + null); + } + }, + uri, + null, + null, + null, + null); + } + + public interface OnCheckBlockedListener { + + /** + * Invoked after querying if a number is blocked. + * + * @param id The ID of the row if blocked, null otherwise. + */ + void onCheckComplete(Integer id); + } + + public interface OnBlockNumberListener { + + /** + * Invoked after inserting a blocked number. + * + * @param uri The uri of the newly created row. + */ + void onBlockComplete(Uri uri); + } + + public interface OnUnblockNumberListener { + + /** + * Invoked after removing a blocked number + * + * @param rows The number of rows affected (expected value 1). + * @param values The deleted data (used for restoration). + */ + void onUnblockComplete(int rows, ContentValues values); + } + + public interface OnHasBlockedNumbersListener { + + /** + * @param hasBlockedNumbers {@code true} if any blocked numbers are stored. {@code false} + * otherwise. + */ + void onHasBlockedNumbers(boolean hasBlockedNumbers); + } + + /** Methods for FilteredNumberAsyncQueryHandler result returns. */ + private abstract static class Listener { + + protected void onQueryComplete(int token, Object cookie, Cursor cursor) {} + + protected void onInsertComplete(int token, Object cookie, Uri uri) {} + + protected void onUpdateComplete(int token, Object cookie, int result) {} + + protected void onDeleteComplete(int token, Object cookie, int result) {} + } +} |