BlockedNumberProvider.java revision a42ead888255f167b2d5dc405974bddd12d7695b
1/* 2 * Copyright (C) 2016 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 */ 16package com.android.providers.blockednumber; 17 18import android.Manifest; 19import android.annotation.NonNull; 20import android.annotation.Nullable; 21import android.app.AppOpsManager; 22import android.content.ContentProvider; 23import android.content.ContentUris; 24import android.content.ContentValues; 25import android.content.Context; 26import android.content.Intent; 27import android.content.SharedPreferences; 28import android.content.UriMatcher; 29import android.content.pm.PackageManager; 30import android.database.Cursor; 31import android.database.sqlite.SQLiteDatabase; 32import android.database.sqlite.SQLiteQueryBuilder; 33import android.net.Uri; 34import android.os.Binder; 35import android.os.Bundle; 36import android.os.CancellationSignal; 37import android.os.Process; 38import android.os.UserManager; 39import android.provider.BlockedNumberContract; 40import android.provider.BlockedNumberContract.SystemContract; 41import android.telecom.TelecomManager; 42import android.telephony.CarrierConfigManager; 43import android.telephony.PhoneNumberUtils; 44import android.telephony.TelephonyManager; 45import android.text.TextUtils; 46import android.util.Log; 47 48import com.android.common.content.ProjectionMap; 49import com.android.internal.annotations.VisibleForTesting; 50import com.android.providers.blockednumber.BlockedNumberDatabaseHelper.Tables; 51 52/** 53 * Blocked phone number provider. 54 * 55 * <p>Note the provider allows emergency numbers. The caller (telecom) should never call it with 56 * emergency numbers. 57 */ 58public class BlockedNumberProvider extends ContentProvider { 59 static final String TAG = "BlockedNumbers"; 60 61 private static final boolean DEBUG = true; // DO NOT SUBMIT WITH TRUE. 62 63 private static final int BLOCKED_LIST = 1000; 64 private static final int BLOCKED_ID = 1001; 65 66 private static final UriMatcher sUriMatcher; 67 68 private static final String PREF_FILE = "block_number_provider_prefs"; 69 private static final String BLOCK_SUPPRESSION_EXPIRY_TIME_PREF = 70 "block_suppression_expiry_time_pref"; 71 private static final int MAX_BLOCKING_DISABLED_DURATION_SECONDS = 7 * 24 * 3600; // 1 week 72 73 static { 74 sUriMatcher = new UriMatcher(0); 75 sUriMatcher.addURI(BlockedNumberContract.AUTHORITY, "blocked", BLOCKED_LIST); 76 sUriMatcher.addURI(BlockedNumberContract.AUTHORITY, "blocked/#", BLOCKED_ID); 77 } 78 79 private static final ProjectionMap sBlockedNumberColumns = ProjectionMap.builder() 80 .add(BlockedNumberContract.BlockedNumbers.COLUMN_ID) 81 .add(BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER) 82 .add(BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER) 83 .build(); 84 85 private static final String ID_SELECTION = 86 BlockedNumberContract.BlockedNumbers.COLUMN_ID + "=?"; 87 88 @VisibleForTesting 89 protected BlockedNumberDatabaseHelper mDbHelper; 90 91 @Override 92 public boolean onCreate() { 93 mDbHelper = BlockedNumberDatabaseHelper.getInstance(getContext()); 94 return true; 95 } 96 97 @Override 98 public String getType(@NonNull Uri uri) { 99 final int match = sUriMatcher.match(uri); 100 switch (match) { 101 case BLOCKED_LIST: 102 return BlockedNumberContract.BlockedNumbers.CONTENT_TYPE; 103 case BLOCKED_ID: 104 return BlockedNumberContract.BlockedNumbers.CONTENT_ITEM_TYPE; 105 default: 106 throw new IllegalArgumentException("Unsupported URI: " + uri); 107 } 108 } 109 110 @Override 111 public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { 112 enforceWritePermissionAndPrimaryUser(); 113 114 final int match = sUriMatcher.match(uri); 115 switch (match) { 116 case BLOCKED_LIST: 117 Uri blockedUri = insertBlockedNumber(values); 118 getContext().getContentResolver().notifyChange(blockedUri, null); 119 return blockedUri; 120 default: 121 throw new IllegalArgumentException("Unsupported URI: " + uri); 122 } 123 } 124 125 /** 126 * Implements the "blocked/" insert. 127 */ 128 private Uri insertBlockedNumber(ContentValues cv) { 129 throwIfSpecified(cv, BlockedNumberContract.BlockedNumbers.COLUMN_ID); 130 131 final String phoneNumber = cv.getAsString( 132 BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER); 133 134 if (TextUtils.isEmpty(phoneNumber)) { 135 throw new IllegalArgumentException("Missing a required column " + 136 BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER); 137 } 138 139 // Fill in with autogenerated columns. 140 final String e164Number = Utils.getE164Number(getContext(), phoneNumber, 141 cv.getAsString(BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER)); 142 cv.put(BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER, e164Number); 143 144 if (DEBUG) { 145 Log.d(TAG, String.format("inserted blocked number: %s", cv)); 146 } 147 148 // Then insert. 149 final long id = mDbHelper.getWritableDatabase().insertOrThrow( 150 BlockedNumberDatabaseHelper.Tables.BLOCKED_NUMBERS, null, cv); 151 152 return ContentUris.withAppendedId(BlockedNumberContract.BlockedNumbers.CONTENT_URI, id); 153 } 154 155 private static void throwIfSpecified(ContentValues cv, String column) { 156 if (cv.containsKey(column)) { 157 throw new IllegalArgumentException("Column " + column + " must not be specified"); 158 } 159 } 160 161 @Override 162 public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, 163 @Nullable String[] selectionArgs) { 164 enforceWritePermissionAndPrimaryUser(); 165 166 throw new UnsupportedOperationException( 167 "Update is not supported. Use delete + insert instead"); 168 } 169 170 @Override 171 public int delete(@NonNull Uri uri, @Nullable String selection, 172 @Nullable String[] selectionArgs) { 173 enforceWritePermissionAndPrimaryUser(); 174 175 final int match = sUriMatcher.match(uri); 176 int numRows; 177 switch (match) { 178 case BLOCKED_LIST: 179 numRows = deleteBlockedNumber(selection, selectionArgs); 180 break; 181 case BLOCKED_ID: 182 numRows = deleteBlockedNumberWithId(ContentUris.parseId(uri), selection); 183 break; 184 default: 185 throw new IllegalArgumentException("Unsupported URI: " + uri); 186 } 187 getContext().getContentResolver().notifyChange(uri, null); 188 return numRows; 189 } 190 191 /** 192 * Implements the "blocked/#" delete. 193 */ 194 private int deleteBlockedNumberWithId(long id, String selection) { 195 throwForNonEmptySelection(selection); 196 197 return deleteBlockedNumber(ID_SELECTION, new String[]{Long.toString(id)}); 198 } 199 200 /** 201 * Implements the "blocked/" delete. 202 */ 203 private int deleteBlockedNumber(String selection, String[] selectionArgs) { 204 final SQLiteDatabase db = mDbHelper.getWritableDatabase(); 205 206 // When selection is specified, compile it within (...) to detect SQL injection. 207 if (!TextUtils.isEmpty(selection)) { 208 db.validateSql("select 1 FROM " + Tables.BLOCKED_NUMBERS + " WHERE " + 209 Utils.wrapSelectionWithParens(selection), 210 /* cancellationSignal =*/ null); 211 } 212 213 return db.delete( 214 BlockedNumberDatabaseHelper.Tables.BLOCKED_NUMBERS, 215 selection, selectionArgs); 216 } 217 218 @Override 219 public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, 220 @Nullable String[] selectionArgs, @Nullable String sortOrder) { 221 enforceReadPermissionAndPrimaryUser(); 222 223 return query(uri, projection, selection, selectionArgs, sortOrder, null); 224 } 225 226 @Override 227 public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, 228 @Nullable String[] selectionArgs, @Nullable String sortOrder, 229 @Nullable CancellationSignal cancellationSignal) { 230 enforceReadPermissionAndPrimaryUser(); 231 232 final int match = sUriMatcher.match(uri); 233 Cursor cursor; 234 switch (match) { 235 case BLOCKED_LIST: 236 cursor = queryBlockedList(projection, selection, selectionArgs, sortOrder, 237 cancellationSignal); 238 break; 239 case BLOCKED_ID: 240 cursor = queryBlockedListWithId(ContentUris.parseId(uri), projection, selection, 241 cancellationSignal); 242 break; 243 default: 244 throw new IllegalArgumentException("Unsupported URI: " + uri); 245 } 246 // Tell the cursor what uri to watch, so it knows when its source data changes 247 cursor.setNotificationUri(getContext().getContentResolver(), uri); 248 return cursor; 249 } 250 251 /** 252 * Implements the "blocked/#" query. 253 */ 254 private Cursor queryBlockedListWithId(long id, String[] projection, String selection, 255 CancellationSignal cancellationSignal) { 256 throwForNonEmptySelection(selection); 257 258 return queryBlockedList(projection, ID_SELECTION, new String[]{Long.toString(id)}, 259 null, cancellationSignal); 260 } 261 262 /** 263 * Implements the "blocked/" query. 264 */ 265 private Cursor queryBlockedList(String[] projection, String selection, String[] selectionArgs, 266 String sortOrder, CancellationSignal cancellationSignal) { 267 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 268 qb.setStrict(true); 269 qb.setTables(BlockedNumberDatabaseHelper.Tables.BLOCKED_NUMBERS); 270 qb.setProjectionMap(sBlockedNumberColumns); 271 272 return qb.query(mDbHelper.getReadableDatabase(), projection, selection, selectionArgs, 273 /* groupBy =*/ null, /* having =*/null, sortOrder, 274 /* limit =*/ null, cancellationSignal); 275 } 276 277 private void throwForNonEmptySelection(String selection) { 278 if (!TextUtils.isEmpty(selection)) { 279 throw new IllegalArgumentException( 280 "When ID is specified in URI, selection must be null"); 281 } 282 } 283 284 @Override 285 public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) { 286 final Bundle res = new Bundle(); 287 switch (method) { 288 case BlockedNumberContract.METHOD_IS_BLOCKED: 289 enforceReadPermissionAndPrimaryUser(); 290 291 res.putBoolean(BlockedNumberContract.RES_NUMBER_IS_BLOCKED, isBlocked(arg)); 292 break; 293 case BlockedNumberContract.METHOD_CAN_CURRENT_USER_BLOCK_NUMBERS: 294 enforceReadPermission(); 295 296 res.putBoolean( 297 BlockedNumberContract.RES_CAN_BLOCK_NUMBERS, canCurrentUserBlockUsers()); 298 break; 299 case SystemContract.METHOD_NOTIFY_EMERGENCY_CONTACT: 300 enforceSystemWritePermissionAndPrimaryUser(); 301 302 notifyEmergencyContact(); 303 break; 304 case SystemContract.METHOD_END_BLOCK_SUPPRESSION: 305 enforceSystemWritePermissionAndPrimaryUser(); 306 307 endBlockSuppression(); 308 break; 309 case SystemContract.METHOD_GET_BLOCK_SUPPRESSION_STATUS: 310 enforceSystemReadPermissionAndPrimaryUser(); 311 312 SystemContract.BlockSuppressionStatus status = getBlockSuppressionStatus(); 313 res.putBoolean(SystemContract.RES_IS_BLOCKING_SUPPRESSED, status.isSuppressed); 314 res.putLong(SystemContract.RES_BLOCKING_SUPPRESSED_UNTIL_TIMESTAMP, 315 status.untilTimestampMillis); 316 break; 317 case SystemContract.METHOD_SHOULD_SYSTEM_BLOCK_NUMBER: 318 enforceSystemReadPermissionAndPrimaryUser(); 319 res.putBoolean( 320 BlockedNumberContract.RES_NUMBER_IS_BLOCKED, shouldSystemBlockNumber(arg)); 321 break; 322 default: 323 enforceReadPermissionAndPrimaryUser(); 324 325 throw new IllegalArgumentException("Unsupported method " + method); 326 } 327 return res; 328 } 329 330 private boolean isEmergencyNumber(String phoneNumber) { 331 if (TextUtils.isEmpty(phoneNumber)) { 332 return false; 333 } 334 335 final String e164Number = Utils.getE164Number(getContext(), phoneNumber, null); 336 return PhoneNumberUtils.isEmergencyNumber(phoneNumber) 337 || PhoneNumberUtils.isEmergencyNumber(e164Number); 338 } 339 340 private boolean isBlocked(String phoneNumber) { 341 if (TextUtils.isEmpty(phoneNumber)) { 342 return false; 343 } 344 345 final String inE164 = Utils.getE164Number(getContext(), phoneNumber, null); // may be empty. 346 347 if (DEBUG) { 348 Log.d(TAG, String.format("isBlocked: in=%s, e164=%s", phoneNumber, inE164)); 349 } 350 351 final Cursor c = mDbHelper.getReadableDatabase().rawQuery( 352 "SELECT " + 353 BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER + "," + 354 BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER + 355 " FROM " + BlockedNumberDatabaseHelper.Tables.BLOCKED_NUMBERS + 356 " WHERE " + BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER + "=?1" + 357 " OR (?2 != '' AND " + 358 BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER + "=?2)", 359 new String[] {phoneNumber, inE164} 360 ); 361 try { 362 while (c.moveToNext()) { 363 if (DEBUG) { 364 final String original = c.getString(0); 365 final String e164 = c.getString(1); 366 367 Log.d(TAG, String.format("match found: original=%s, e164=%s", original, e164)); 368 } 369 return true; 370 } 371 } finally { 372 c.close(); 373 } 374 // No match found. 375 return false; 376 } 377 378 private boolean canCurrentUserBlockUsers() { 379 UserManager userManager = getContext().getSystemService(UserManager.class); 380 return userManager.isPrimaryUser(); 381 } 382 383 private void notifyEmergencyContact() { 384 writeBlockSuppressionExpiryTimePref(System.currentTimeMillis() + 385 getBlockSuppressSecondsFromCarrierConfig() * 1000); 386 notifyBlockSuppressionStateChange(); 387 } 388 389 private void endBlockSuppression() { 390 // Nothing to do if blocks are not being suppressed. 391 if (getBlockSuppressionStatus().isSuppressed) { 392 writeBlockSuppressionExpiryTimePref(0); 393 notifyBlockSuppressionStateChange(); 394 } 395 } 396 397 private SystemContract.BlockSuppressionStatus getBlockSuppressionStatus() { 398 SharedPreferences pref = getContext().getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE); 399 long blockSuppressionExpiryTimeMillis = pref.getLong(BLOCK_SUPPRESSION_EXPIRY_TIME_PREF, 0); 400 return new SystemContract.BlockSuppressionStatus(System.currentTimeMillis() < 401 blockSuppressionExpiryTimeMillis, blockSuppressionExpiryTimeMillis); 402 } 403 404 private boolean shouldSystemBlockNumber(String phoneNumber) { 405 if (getBlockSuppressionStatus().isSuppressed) { 406 return false; 407 } 408 if (isEmergencyNumber(phoneNumber)) { 409 return false; 410 } 411 return isBlocked(phoneNumber); 412 } 413 414 private void writeBlockSuppressionExpiryTimePref(long expiryTimeMillis) { 415 SharedPreferences pref = getContext().getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE); 416 SharedPreferences.Editor editor = pref.edit(); 417 editor.putLong(BLOCK_SUPPRESSION_EXPIRY_TIME_PREF, expiryTimeMillis); 418 editor.apply(); 419 } 420 421 private long getBlockSuppressSecondsFromCarrierConfig() { 422 CarrierConfigManager carrierConfigManager = 423 getContext().getSystemService(CarrierConfigManager.class); 424 int carrierConfigValue = carrierConfigManager.getConfig().getInt 425 (CarrierConfigManager.KEY_DURATION_BLOCKING_DISABLED_AFTER_EMERGENCY_INT); 426 boolean isValidValue = carrierConfigValue >=0 && carrierConfigValue <= 427 MAX_BLOCKING_DISABLED_DURATION_SECONDS; 428 return isValidValue ? carrierConfigValue : CarrierConfigManager.getDefaultConfig().getInt( 429 CarrierConfigManager.KEY_DURATION_BLOCKING_DISABLED_AFTER_EMERGENCY_INT); 430 } 431 432 /** 433 * Returns {@code false} when the caller is not root, the user selected dialer, the 434 * default SMS app or a carrier app. 435 */ 436 private boolean checkForPrivilegedApplications() { 437 if (Binder.getCallingUid() == Process.ROOT_UID) { 438 return true; 439 } 440 441 final String callingPackage = getCallingPackage(); 442 if (TextUtils.isEmpty(callingPackage)) { 443 Log.w(TAG, "callingPackage not accessible"); 444 } else { 445 final TelecomManager telecom = getContext().getSystemService(TelecomManager.class); 446 447 if (callingPackage.equals(telecom.getDefaultDialerPackage()) 448 || callingPackage.equals(telecom.getSystemDialerPackage())) { 449 return true; 450 } 451 final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class); 452 if (appOps.noteOp(AppOpsManager.OP_WRITE_SMS, 453 Binder.getCallingUid(), callingPackage) == AppOpsManager.MODE_ALLOWED) { 454 return true; 455 } 456 457 final TelephonyManager telephonyManager = 458 getContext().getSystemService(TelephonyManager.class); 459 return telephonyManager.checkCarrierPrivilegesForPackage(callingPackage) == 460 TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS; 461 } 462 return false; 463 } 464 465 private void notifyBlockSuppressionStateChange() { 466 Intent intent = new Intent(SystemContract.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED); 467 getContext().sendBroadcast(intent, Manifest.permission.READ_BLOCKED_NUMBERS); 468 } 469 470 private void enforceReadPermission() { 471 checkForPermission(android.Manifest.permission.READ_BLOCKED_NUMBERS); 472 } 473 474 private void enforceReadPermissionAndPrimaryUser() { 475 checkForPermissionAndPrimaryUser(android.Manifest.permission.READ_BLOCKED_NUMBERS); 476 } 477 478 private void enforceWritePermissionAndPrimaryUser() { 479 checkForPermissionAndPrimaryUser(android.Manifest.permission.WRITE_BLOCKED_NUMBERS); 480 } 481 482 private void checkForPermissionAndPrimaryUser(String permission) { 483 checkForPermission(permission); 484 if (!canCurrentUserBlockUsers()) { 485 throw new UnsupportedOperationException(); 486 } 487 } 488 489 private void checkForPermission(String permission) { 490 boolean permitted = passesSystemPermissionCheck(permission) 491 || checkForPrivilegedApplications(); 492 if (!permitted) { 493 throwSecurityException(); 494 } 495 } 496 497 private void enforceSystemReadPermissionAndPrimaryUser() { 498 enforceSystemPermissionAndUser(android.Manifest.permission.READ_BLOCKED_NUMBERS); 499 } 500 501 private void enforceSystemWritePermissionAndPrimaryUser() { 502 enforceSystemPermissionAndUser(android.Manifest.permission.WRITE_BLOCKED_NUMBERS); 503 } 504 505 private void enforceSystemPermissionAndUser(String permission) { 506 if (!canCurrentUserBlockUsers()) { 507 throw new UnsupportedOperationException(); 508 } 509 510 if (!passesSystemPermissionCheck(permission)) { 511 throwSecurityException(); 512 } 513 } 514 515 private boolean passesSystemPermissionCheck(String permission) { 516 return getContext().checkCallingPermission(permission) 517 == PackageManager.PERMISSION_GRANTED; 518 } 519 520 private void throwSecurityException() { 521 throw new SecurityException("Caller must be system, default dialer or default SMS app"); 522 } 523} 524