1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.dialer.blocking; 18 19 import android.annotation.TargetApi; 20 import android.content.AsyncQueryHandler; 21 import android.content.ContentValues; 22 import android.content.Context; 23 import android.database.Cursor; 24 import android.database.DatabaseUtils; 25 import android.database.sqlite.SQLiteDatabaseCorruptException; 26 import android.net.Uri; 27 import android.os.Build.VERSION_CODES; 28 import android.support.annotation.Nullable; 29 import android.support.annotation.VisibleForTesting; 30 import android.support.v4.os.UserManagerCompat; 31 import android.telephony.PhoneNumberUtils; 32 import android.text.TextUtils; 33 import com.android.dialer.common.Assert; 34 import com.android.dialer.common.LogUtil; 35 import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns; 36 import com.android.dialer.database.FilteredNumberContract.FilteredNumberTypes; 37 import java.util.Map; 38 import java.util.concurrent.ConcurrentHashMap; 39 40 public class FilteredNumberAsyncQueryHandler extends AsyncQueryHandler { 41 42 public static final int INVALID_ID = -1; 43 // Id used to replace null for blocked id since ConcurrentHashMap doesn't allow null key/value. 44 @VisibleForTesting static final int BLOCKED_NUMBER_CACHE_NULL_ID = -1; 45 46 @VisibleForTesting 47 static final Map<String, Integer> blockedNumberCache = new ConcurrentHashMap<>(); 48 49 private static final int NO_TOKEN = 0; 50 private final Context context; 51 FilteredNumberAsyncQueryHandler(Context context)52 public FilteredNumberAsyncQueryHandler(Context context) { 53 super(context.getContentResolver()); 54 this.context = context; 55 } 56 57 @Override onQueryComplete(int token, Object cookie, Cursor cursor)58 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 59 try { 60 if (cookie != null) { 61 ((Listener) cookie).onQueryComplete(token, cookie, cursor); 62 } 63 } finally { 64 if (cursor != null) { 65 cursor.close(); 66 } 67 } 68 } 69 70 @Override onInsertComplete(int token, Object cookie, Uri uri)71 protected void onInsertComplete(int token, Object cookie, Uri uri) { 72 if (cookie != null) { 73 ((Listener) cookie).onInsertComplete(token, cookie, uri); 74 } 75 } 76 77 @Override onUpdateComplete(int token, Object cookie, int result)78 protected void onUpdateComplete(int token, Object cookie, int result) { 79 if (cookie != null) { 80 ((Listener) cookie).onUpdateComplete(token, cookie, result); 81 } 82 } 83 84 @Override onDeleteComplete(int token, Object cookie, int result)85 protected void onDeleteComplete(int token, Object cookie, int result) { 86 if (cookie != null) { 87 ((Listener) cookie).onDeleteComplete(token, cookie, result); 88 } 89 } 90 hasBlockedNumbers(final OnHasBlockedNumbersListener listener)91 void hasBlockedNumbers(final OnHasBlockedNumbersListener listener) { 92 if (!FilteredNumberCompat.canAttemptBlockOperations(context)) { 93 listener.onHasBlockedNumbers(false); 94 return; 95 } 96 startQuery( 97 NO_TOKEN, 98 new Listener() { 99 @Override 100 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 101 listener.onHasBlockedNumbers(cursor != null && cursor.getCount() > 0); 102 } 103 }, 104 FilteredNumberCompat.getContentUri(context, null), 105 new String[] {FilteredNumberCompat.getIdColumnName(context)}, 106 FilteredNumberCompat.useNewFiltering(context) 107 ? null 108 : FilteredNumberColumns.TYPE + "=" + FilteredNumberTypes.BLOCKED_NUMBER, 109 null, 110 null); 111 } 112 113 /** 114 * Checks if the given number is blocked, calling the given {@link OnCheckBlockedListener} with 115 * the id for the blocked number, {@link #INVALID_ID}, or {@code null} based on the result of the 116 * check. 117 */ isBlockedNumber( final OnCheckBlockedListener listener, @Nullable final String number, String countryIso)118 public void isBlockedNumber( 119 final OnCheckBlockedListener listener, @Nullable final String number, String countryIso) { 120 if (number == null) { 121 listener.onCheckComplete(INVALID_ID); 122 return; 123 } 124 if (!FilteredNumberCompat.canAttemptBlockOperations(context)) { 125 listener.onCheckComplete(null); 126 return; 127 } 128 Integer cachedId = blockedNumberCache.get(number); 129 if (cachedId != null) { 130 if (listener == null) { 131 return; 132 } 133 if (cachedId == BLOCKED_NUMBER_CACHE_NULL_ID) { 134 cachedId = null; 135 } 136 listener.onCheckComplete(cachedId); 137 return; 138 } 139 140 if (!UserManagerCompat.isUserUnlocked(context)) { 141 LogUtil.i( 142 "FilteredNumberAsyncQueryHandler.isBlockedNumber", 143 "Device locked in FBE mode, cannot access blocked number database"); 144 listener.onCheckComplete(INVALID_ID); 145 return; 146 } 147 148 String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso); 149 String formattedNumber = FilteredNumbersUtil.getBlockableNumber(context, e164Number, number); 150 if (TextUtils.isEmpty(formattedNumber)) { 151 listener.onCheckComplete(INVALID_ID); 152 blockedNumberCache.put(number, INVALID_ID); 153 return; 154 } 155 156 startQuery( 157 NO_TOKEN, 158 new Listener() { 159 @Override 160 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 161 /* 162 * In the frameworking blocking, numbers can be blocked in both e164 format 163 * and not, resulting in multiple rows being returned for this query. For 164 * example, both '16502530000' and '6502530000' can exist at the same time 165 * and will be returned by this query. 166 */ 167 if (cursor == null || cursor.getCount() == 0) { 168 blockedNumberCache.put(number, BLOCKED_NUMBER_CACHE_NULL_ID); 169 listener.onCheckComplete(null); 170 return; 171 } 172 cursor.moveToFirst(); 173 // New filtering doesn't have a concept of type 174 if (!FilteredNumberCompat.useNewFiltering(context) 175 && cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns.TYPE)) 176 != FilteredNumberTypes.BLOCKED_NUMBER) { 177 blockedNumberCache.put(number, BLOCKED_NUMBER_CACHE_NULL_ID); 178 listener.onCheckComplete(null); 179 return; 180 } 181 Integer blockedId = cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns._ID)); 182 blockedNumberCache.put(number, blockedId); 183 listener.onCheckComplete(blockedId); 184 } 185 }, 186 FilteredNumberCompat.getContentUri(context, null), 187 FilteredNumberCompat.filter( 188 new String[] { 189 FilteredNumberCompat.getIdColumnName(context), 190 FilteredNumberCompat.getTypeColumnName(context) 191 }), 192 getIsBlockedNumberSelection(e164Number != null) + " = ?", 193 new String[] {formattedNumber}, 194 null); 195 } 196 197 /** 198 * Synchronously check if this number has been blocked. 199 * 200 * @return blocked id. 201 */ 202 @TargetApi(VERSION_CODES.M) 203 @Nullable getBlockedIdSynchronous(@ullable String number, String countryIso)204 public Integer getBlockedIdSynchronous(@Nullable String number, String countryIso) { 205 Assert.isWorkerThread(); 206 if (number == null) { 207 return null; 208 } 209 if (!FilteredNumberCompat.canAttemptBlockOperations(context)) { 210 return null; 211 } 212 Integer cachedId = blockedNumberCache.get(number); 213 if (cachedId != null) { 214 if (cachedId == BLOCKED_NUMBER_CACHE_NULL_ID) { 215 cachedId = null; 216 } 217 return cachedId; 218 } 219 220 String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso); 221 String formattedNumber = FilteredNumbersUtil.getBlockableNumber(context, e164Number, number); 222 if (TextUtils.isEmpty(formattedNumber)) { 223 return null; 224 } 225 226 try (Cursor cursor = 227 context 228 .getContentResolver() 229 .query( 230 FilteredNumberCompat.getContentUri(context, null), 231 FilteredNumberCompat.filter( 232 new String[] { 233 FilteredNumberCompat.getIdColumnName(context), 234 FilteredNumberCompat.getTypeColumnName(context) 235 }), 236 getIsBlockedNumberSelection(e164Number != null) + " = ?", 237 new String[] {formattedNumber}, 238 null)) { 239 /* 240 * In the frameworking blocking, numbers can be blocked in both e164 format 241 * and not, resulting in multiple rows being returned for this query. For 242 * example, both '16502530000' and '6502530000' can exist at the same time 243 * and will be returned by this query. 244 */ 245 if (cursor == null || cursor.getCount() == 0) { 246 blockedNumberCache.put(number, BLOCKED_NUMBER_CACHE_NULL_ID); 247 return null; 248 } 249 cursor.moveToFirst(); 250 int blockedId = cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns._ID)); 251 blockedNumberCache.put(number, blockedId); 252 return blockedId; 253 } catch (SecurityException e) { 254 LogUtil.e("FilteredNumberAsyncQueryHandler.getBlockedIdSynchronous", null, e); 255 return null; 256 } 257 } 258 259 @VisibleForTesting clearCache()260 public void clearCache() { 261 blockedNumberCache.clear(); 262 } 263 264 /* 265 * TODO: b/27779827, non-e164 numbers can be blocked in the new form of blocking. As a 266 * temporary workaround, determine which column of the database to query based on whether the 267 * number is e164 or not. 268 */ getIsBlockedNumberSelection(boolean isE164Number)269 private String getIsBlockedNumberSelection(boolean isE164Number) { 270 if (FilteredNumberCompat.useNewFiltering(context) && !isE164Number) { 271 return FilteredNumberCompat.getOriginalNumberColumnName(context); 272 } 273 return FilteredNumberCompat.getE164NumberColumnName(context); 274 } 275 blockNumber( final OnBlockNumberListener listener, String number, @Nullable String countryIso)276 public void blockNumber( 277 final OnBlockNumberListener listener, String number, @Nullable String countryIso) { 278 blockNumber(listener, null, number, countryIso); 279 } 280 281 /** Add a number manually blocked by the user. */ blockNumber( final OnBlockNumberListener listener, @Nullable String normalizedNumber, String number, @Nullable String countryIso)282 public void blockNumber( 283 final OnBlockNumberListener listener, 284 @Nullable String normalizedNumber, 285 String number, 286 @Nullable String countryIso) { 287 blockNumber( 288 listener, 289 FilteredNumberCompat.newBlockNumberContentValues( 290 context, number, normalizedNumber, countryIso)); 291 } 292 293 /** 294 * Block a number with specified ContentValues. Can be manually added or a restored row from 295 * performing the 'undo' action after unblocking. 296 */ blockNumber(final OnBlockNumberListener listener, ContentValues values)297 public void blockNumber(final OnBlockNumberListener listener, ContentValues values) { 298 blockedNumberCache.clear(); 299 if (!FilteredNumberCompat.canAttemptBlockOperations(context)) { 300 listener.onBlockComplete(null); 301 return; 302 } 303 startInsert( 304 NO_TOKEN, 305 new Listener() { 306 @Override 307 public void onInsertComplete(int token, Object cookie, Uri uri) { 308 if (listener != null) { 309 listener.onBlockComplete(uri); 310 } 311 } 312 }, 313 FilteredNumberCompat.getContentUri(context, null), 314 values); 315 } 316 317 /** 318 * Unblocks the number with the given id. 319 * 320 * @param listener (optional) The {@link OnUnblockNumberListener} called after the number is 321 * unblocked. 322 * @param id The id of the number to unblock. 323 */ unblock(@ullable final OnUnblockNumberListener listener, Integer id)324 public void unblock(@Nullable final OnUnblockNumberListener listener, Integer id) { 325 if (id == null) { 326 throw new IllegalArgumentException("Null id passed into unblock"); 327 } 328 unblock(listener, FilteredNumberCompat.getContentUri(context, id)); 329 } 330 331 /** 332 * Removes row from database. 333 * 334 * @param listener (optional) The {@link OnUnblockNumberListener} called after the number is 335 * unblocked. 336 * @param uri The uri of row to remove, from {@link FilteredNumberAsyncQueryHandler#blockNumber}. 337 */ unblock(@ullable final OnUnblockNumberListener listener, final Uri uri)338 public void unblock(@Nullable final OnUnblockNumberListener listener, final Uri uri) { 339 blockedNumberCache.clear(); 340 if (!FilteredNumberCompat.canAttemptBlockOperations(context)) { 341 if (listener != null) { 342 listener.onUnblockComplete(0, null); 343 } 344 return; 345 } 346 startQuery( 347 NO_TOKEN, 348 new Listener() { 349 @Override 350 public void onQueryComplete(int token, Object cookie, Cursor cursor) { 351 int rowsReturned = cursor == null ? 0 : cursor.getCount(); 352 if (rowsReturned != 1) { 353 throw new SQLiteDatabaseCorruptException( 354 "Returned " + rowsReturned + " rows for uri " + uri + "where 1 expected."); 355 } 356 cursor.moveToFirst(); 357 final ContentValues values = new ContentValues(); 358 DatabaseUtils.cursorRowToContentValues(cursor, values); 359 values.remove(FilteredNumberCompat.getIdColumnName(context)); 360 361 startDelete( 362 NO_TOKEN, 363 new Listener() { 364 @Override 365 public void onDeleteComplete(int token, Object cookie, int result) { 366 if (listener != null) { 367 listener.onUnblockComplete(result, values); 368 } 369 } 370 }, 371 uri, 372 null, 373 null); 374 } 375 }, 376 uri, 377 null, 378 null, 379 null, 380 null); 381 } 382 383 public interface OnCheckBlockedListener { 384 385 /** 386 * Invoked after querying if a number is blocked. 387 * 388 * @param id The ID of the row if blocked, null otherwise. 389 */ onCheckComplete(Integer id)390 void onCheckComplete(Integer id); 391 } 392 393 public interface OnBlockNumberListener { 394 395 /** 396 * Invoked after inserting a blocked number. 397 * 398 * @param uri The uri of the newly created row. 399 */ onBlockComplete(Uri uri)400 void onBlockComplete(Uri uri); 401 } 402 403 public interface OnUnblockNumberListener { 404 405 /** 406 * Invoked after removing a blocked number 407 * 408 * @param rows The number of rows affected (expected value 1). 409 * @param values The deleted data (used for restoration). 410 */ onUnblockComplete(int rows, ContentValues values)411 void onUnblockComplete(int rows, ContentValues values); 412 } 413 414 interface OnHasBlockedNumbersListener { 415 416 /** 417 * @param hasBlockedNumbers {@code true} if any blocked numbers are stored. {@code false} 418 * otherwise. 419 */ onHasBlockedNumbers(boolean hasBlockedNumbers)420 void onHasBlockedNumbers(boolean hasBlockedNumbers); 421 } 422 423 /** Methods for FilteredNumberAsyncQueryHandler result returns. */ 424 private abstract static class Listener { 425 onQueryComplete(int token, Object cookie, Cursor cursor)426 protected void onQueryComplete(int token, Object cookie, Cursor cursor) {} 427 onInsertComplete(int token, Object cookie, Uri uri)428 protected void onInsertComplete(int token, Object cookie, Uri uri) {} 429 onUpdateComplete(int token, Object cookie, int result)430 protected void onUpdateComplete(int token, Object cookie, int result) {} 431 onDeleteComplete(int token, Object cookie, int result)432 protected void onDeleteComplete(int token, Object cookie, int result) {} 433 } 434 } 435