summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java')
-rw-r--r--java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java428
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) {}
+ }
+}