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.content.AsyncQueryHandler; 20 import android.content.ContentValues; 21 import android.content.Context; 22 import android.database.Cursor; 23 import android.database.DatabaseUtils; 24 import android.database.sqlite.SQLiteDatabaseCorruptException; 25 import android.net.Uri; 26 import android.support.annotation.Nullable; 27 import android.support.annotation.VisibleForTesting; 28 import android.support.v4.os.UserManagerCompat; 29 import android.telephony.PhoneNumberUtils; 30 import android.text.TextUtils; 31 import com.android.dialer.common.Assert; 32 import com.android.dialer.common.LogUtil; 33 import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns; 34 import com.android.dialer.database.FilteredNumberContract.FilteredNumberTypes; 35 import java.util.Map; 36 import java.util.concurrent.ConcurrentHashMap; 37 38 /** TODO(calderwoodra): documentation */ 39 @Deprecated 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 @Nullable getBlockedIdSynchronous(@ullable String number, String countryIso)203 public Integer getBlockedIdSynchronous(@Nullable String number, String countryIso) { 204 Assert.isWorkerThread(); 205 if (number == null) { 206 return null; 207 } 208 if (!FilteredNumberCompat.canAttemptBlockOperations(context)) { 209 return null; 210 } 211 Integer cachedId = blockedNumberCache.get(number); 212 if (cachedId != null) { 213 if (cachedId == BLOCKED_NUMBER_CACHE_NULL_ID) { 214 cachedId = null; 215 } 216 return cachedId; 217 } 218 219 String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso); 220 String formattedNumber = FilteredNumbersUtil.getBlockableNumber(context, e164Number, number); 221 if (TextUtils.isEmpty(formattedNumber)) { 222 return null; 223 } 224 225 try (Cursor cursor = 226 context 227 .getContentResolver() 228 .query( 229 FilteredNumberCompat.getContentUri(context, null), 230 FilteredNumberCompat.filter( 231 new String[] { 232 FilteredNumberCompat.getIdColumnName(context), 233 FilteredNumberCompat.getTypeColumnName(context) 234 }), 235 getIsBlockedNumberSelection(e164Number != null) + " = ?", 236 new String[] {formattedNumber}, 237 null)) { 238 /* 239 * In the frameworking blocking, numbers can be blocked in both e164 format 240 * and not, resulting in multiple rows being returned for this query. For 241 * example, both '16502530000' and '6502530000' can exist at the same time 242 * and will be returned by this query. 243 */ 244 if (cursor == null || cursor.getCount() == 0) { 245 blockedNumberCache.put(number, BLOCKED_NUMBER_CACHE_NULL_ID); 246 return null; 247 } 248 cursor.moveToFirst(); 249 int blockedId = cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns._ID)); 250 blockedNumberCache.put(number, blockedId); 251 return blockedId; 252 } catch (SecurityException e) { 253 LogUtil.e("FilteredNumberAsyncQueryHandler.getBlockedIdSynchronous", null, e); 254 return null; 255 } 256 } 257 258 @VisibleForTesting clearCache()259 public void clearCache() { 260 blockedNumberCache.clear(); 261 } 262 263 /* 264 * TODO(maxwelb): a bug, non-e164 numbers can be blocked in the new form of blocking. As a 265 * temporary workaround, determine which column of the database to query based on whether the 266 * number is e164 or not. 267 */ getIsBlockedNumberSelection(boolean isE164Number)268 private String getIsBlockedNumberSelection(boolean isE164Number) { 269 if (FilteredNumberCompat.useNewFiltering(context) && !isE164Number) { 270 return FilteredNumberCompat.getOriginalNumberColumnName(context); 271 } 272 return FilteredNumberCompat.getE164NumberColumnName(context); 273 } 274 blockNumber( final OnBlockNumberListener listener, String number, @Nullable String countryIso)275 public void blockNumber( 276 final OnBlockNumberListener listener, String number, @Nullable String countryIso) { 277 blockNumber(listener, null, number, countryIso); 278 } 279 280 /** Add a number manually blocked by the user. */ blockNumber( final OnBlockNumberListener listener, @Nullable String normalizedNumber, String number, @Nullable String countryIso)281 public void blockNumber( 282 final OnBlockNumberListener listener, 283 @Nullable String normalizedNumber, 284 String number, 285 @Nullable String countryIso) { 286 blockNumber( 287 listener, 288 FilteredNumberCompat.newBlockNumberContentValues( 289 context, number, normalizedNumber, countryIso)); 290 } 291 292 /** 293 * Block a number with specified ContentValues. Can be manually added or a restored row from 294 * performing the 'undo' action after unblocking. 295 */ blockNumber(final OnBlockNumberListener listener, ContentValues values)296 public void blockNumber(final OnBlockNumberListener listener, ContentValues values) { 297 blockedNumberCache.clear(); 298 if (!FilteredNumberCompat.canAttemptBlockOperations(context)) { 299 if (listener != null) { 300 listener.onBlockComplete(null); 301 } 302 return; 303 } 304 startInsert( 305 NO_TOKEN, 306 new Listener() { 307 @Override 308 public void onInsertComplete(int token, Object cookie, Uri uri) { 309 if (listener != null) { 310 listener.onBlockComplete(uri); 311 } 312 } 313 }, 314 FilteredNumberCompat.getContentUri(context, null), 315 values); 316 } 317 318 /** 319 * Unblocks the number with the given id. 320 * 321 * @param listener (optional) The {@link OnUnblockNumberListener} called after the number is 322 * unblocked. 323 * @param id The id of the number to unblock. 324 */ unblock(@ullable final OnUnblockNumberListener listener, Integer id)325 public void unblock(@Nullable final OnUnblockNumberListener listener, Integer id) { 326 if (id == null) { 327 throw new IllegalArgumentException("Null id passed into unblock"); 328 } 329 unblock(listener, FilteredNumberCompat.getContentUri(context, id)); 330 } 331 332 /** 333 * Removes row from database. 334 * 335 * @param listener (optional) The {@link OnUnblockNumberListener} called after the number is 336 * unblocked. 337 * @param uri The uri of row to remove, from {@link FilteredNumberAsyncQueryHandler#blockNumber}. 338 */ unblock(@ullable final OnUnblockNumberListener listener, final Uri uri)339 public void unblock(@Nullable final OnUnblockNumberListener listener, final Uri uri) { 340 blockedNumberCache.clear(); 341 if (!FilteredNumberCompat.canAttemptBlockOperations(context)) { 342 if (listener != null) { 343 listener.onUnblockComplete(0, null); 344 } 345 return; 346 } 347 startQuery( 348 NO_TOKEN, 349 new Listener() { 350 @Override 351 public void onQueryComplete(int token, Object cookie, Cursor cursor) { 352 int rowsReturned = cursor == null ? 0 : cursor.getCount(); 353 if (rowsReturned != 1) { 354 throw new SQLiteDatabaseCorruptException( 355 "Returned " + rowsReturned + " rows for uri " + uri + "where 1 expected."); 356 } 357 cursor.moveToFirst(); 358 final ContentValues values = new ContentValues(); 359 DatabaseUtils.cursorRowToContentValues(cursor, values); 360 values.remove(FilteredNumberCompat.getIdColumnName(context)); 361 362 startDelete( 363 NO_TOKEN, 364 new Listener() { 365 @Override 366 public void onDeleteComplete(int token, Object cookie, int result) { 367 if (listener != null) { 368 listener.onUnblockComplete(result, values); 369 } 370 } 371 }, 372 uri, 373 null, 374 null); 375 } 376 }, 377 uri, 378 null, 379 null, 380 null, 381 null); 382 } 383 384 /** TODO(calderwoodra): documentation */ 385 public interface OnCheckBlockedListener { 386 387 /** 388 * Invoked after querying if a number is blocked. 389 * 390 * @param id The ID of the row if blocked, null otherwise. 391 */ onCheckComplete(Integer id)392 void onCheckComplete(Integer id); 393 } 394 395 /** TODO(calderwoodra): documentation */ 396 public interface OnBlockNumberListener { 397 398 /** 399 * Invoked after inserting a blocked number. 400 * 401 * @param uri The uri of the newly created row. 402 */ onBlockComplete(Uri uri)403 void onBlockComplete(Uri uri); 404 } 405 406 /** TODO(calderwoodra): documentation */ 407 public interface OnUnblockNumberListener { 408 409 /** 410 * Invoked after removing a blocked number 411 * 412 * @param rows The number of rows affected (expected value 1). 413 * @param values The deleted data (used for restoration). 414 */ onUnblockComplete(int rows, ContentValues values)415 void onUnblockComplete(int rows, ContentValues values); 416 } 417 418 /** TODO(calderwoodra): documentation */ 419 interface OnHasBlockedNumbersListener { 420 421 /** 422 * @param hasBlockedNumbers {@code true} if any blocked numbers are stored. {@code false} 423 * otherwise. 424 */ onHasBlockedNumbers(boolean hasBlockedNumbers)425 void onHasBlockedNumbers(boolean hasBlockedNumbers); 426 } 427 428 /** Methods for FilteredNumberAsyncQueryHandler result returns. */ 429 private abstract static class Listener { 430 onQueryComplete(int token, Object cookie, Cursor cursor)431 protected void onQueryComplete(int token, Object cookie, Cursor cursor) {} 432 onInsertComplete(int token, Object cookie, Uri uri)433 protected void onInsertComplete(int token, Object cookie, Uri uri) {} 434 onUpdateComplete(int token, Object cookie, int result)435 protected void onUpdateComplete(int token, Object cookie, int result) {} 436 onDeleteComplete(int token, Object cookie, int result)437 protected void onDeleteComplete(int token, Object cookie, int result) {} 438 } 439 } 440