BlockedNumberProvider.java revision 35676f9095853d4a5ac15875a9ddb1aea6b0b809
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.annotation.NonNull; 19import android.annotation.Nullable; 20import android.app.AppOpsManager; 21import android.content.*; 22import android.content.pm.ApplicationInfo; 23import android.content.pm.PackageManager; 24import android.database.Cursor; 25import android.database.sqlite.SQLiteDatabase; 26import android.database.sqlite.SQLiteQueryBuilder; 27import android.net.Uri; 28import android.os.*; 29import android.os.Process; 30import android.provider.BlockedNumberContract; 31import android.telecom.TelecomManager; 32import android.text.TextUtils; 33import android.util.Log; 34import com.android.common.content.ProjectionMap; 35import com.android.internal.annotations.VisibleForTesting; 36import com.android.providers.blockednumber.BlockedNumberDatabaseHelper.Tables; 37 38/** 39 * Blocked phone number provider. 40 * 41 * <p>Note the provider allows emergency numbers. The caller (telecom) should never call it with 42 * emergency numbers. 43 */ 44public class BlockedNumberProvider extends ContentProvider { 45 static final String TAG = "BlockedNumbers"; 46 47 private static final boolean DEBUG = true; // DO NOT SUBMIT WITH TRUE. 48 49 private static final int BLOCKED_LIST = 1000; 50 private static final int BLOCKED_ID = 1001; 51 52 private static final UriMatcher sUriMatcher; 53 54 static { 55 sUriMatcher = new UriMatcher(0); 56 sUriMatcher.addURI(BlockedNumberContract.AUTHORITY, "blocked", BLOCKED_LIST); 57 sUriMatcher.addURI(BlockedNumberContract.AUTHORITY, "blocked/#", BLOCKED_ID); 58 } 59 60 private static final ProjectionMap sBlockedNumberColumns = ProjectionMap.builder() 61 .add(BlockedNumberContract.BlockedNumbers.COLUMN_ID) 62 .add(BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER) 63 .add(BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER) 64 .build(); 65 66 private static final String ID_SELECTION = 67 BlockedNumberContract.BlockedNumbers.COLUMN_ID + "=?"; 68 69 @VisibleForTesting 70 protected BlockedNumberDatabaseHelper mDbHelper; 71 72 @Override 73 public boolean onCreate() { 74 mDbHelper = BlockedNumberDatabaseHelper.getInstance(getContext()); 75 return true; 76 } 77 78 /** 79 * TODO CTS: 80 * - BLOCKED_LIST 81 * - BLOCKED_ID 82 * - Other random URLs should fail 83 */ 84 @Override 85 public String getType(@NonNull Uri uri) { 86 final int match = sUriMatcher.match(uri); 87 switch (match) { 88 case BLOCKED_LIST: 89 return BlockedNumberContract.BlockedNumbers.CONTENT_TYPE; 90 case BLOCKED_ID: 91 return BlockedNumberContract.BlockedNumbers.CONTENT_ITEM_TYPE; 92 default: 93 throw new IllegalArgumentException("Unsupported URI: " + uri); 94 } 95 } 96 97 /** 98 * TODO CTS: 99 * - BLOCKED_LIST 100 * With no columns should fail 101 * With COLUMN_INDEX_ORIGINAL only 102 * With COLUMN_INDEX_E164 only should fail 103 * With COLUMN_INDEX_ORIGINAL + COLUMN_INDEX_E164 104 * With with throwIfSpecified columns, should fail. 105 * 106 * - BLOCKED_ID should fail 107 * - Other random URLs should fail 108 */ 109 @Override 110 public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { 111 enforceWritePermission(); 112 113 final int match = sUriMatcher.match(uri); 114 switch (match) { 115 case BLOCKED_LIST: 116 Uri blockedUri = insertBlockedNumber(values); 117 getContext().getContentResolver().notifyChange(blockedUri, null); 118 return blockedUri; 119 default: 120 throw new IllegalArgumentException("Unsupported URI: " + uri); 121 } 122 } 123 124 /** 125 * Implements the "blocked/" insert. 126 */ 127 private Uri insertBlockedNumber(ContentValues cv) { 128 throwIfSpecified(cv, BlockedNumberContract.BlockedNumbers.COLUMN_ID); 129 130 final String phoneNumber = cv.getAsString( 131 BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER); 132 133 if (TextUtils.isEmpty(phoneNumber)) { 134 throw new IllegalArgumentException("Missing a required column " + 135 BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER); 136 } 137 138 // Fill in with autogenerated columns. 139 final String e164Number = Utils.getE164Number(getContext(), phoneNumber, 140 cv.getAsString(BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER)); 141 cv.put(BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER, e164Number); 142 143 if (DEBUG) { 144 Log.d(TAG, String.format("inserted blocked number: %s", cv)); 145 } 146 147 // Then insert. 148 final long id = mDbHelper.getWritableDatabase().insertOrThrow( 149 BlockedNumberDatabaseHelper.Tables.BLOCKED_NUMBERS, null, cv); 150 151 return ContentUris.withAppendedId(BlockedNumberContract.BlockedNumbers.CONTENT_URI, id); 152 } 153 154 private static void throwIfSpecified(ContentValues cv, String column) { 155 if (cv.containsKey(column)) { 156 throw new IllegalArgumentException("Column " + column + " must not be specified"); 157 } 158 } 159 160 /** 161 * TODO CTS: 162 * - Any call should fail 163 */ 164 @Override 165 public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, 166 @Nullable String[] selectionArgs) { 167 throw new UnsupportedOperationException( 168 "Update is not supported. Use delete + insert instead"); 169 } 170 171 /** 172 * TODO CTS: 173 * - BLOCKED_LIST, with selection and without. 174 * - BLOCKED_ID , with selection and without. With should fail. 175 */ 176 @Override 177 public int delete(@NonNull Uri uri, @Nullable String selection, 178 @Nullable String[] selectionArgs) { 179 enforceWritePermission(); 180 181 final int match = sUriMatcher.match(uri); 182 int numRows; 183 switch (match) { 184 case BLOCKED_LIST: 185 numRows = deleteBlockedNumber(selection, selectionArgs); 186 break; 187 case BLOCKED_ID: 188 numRows = deleteBlockedNumberWithId(ContentUris.parseId(uri), selection); 189 break; 190 default: 191 throw new IllegalArgumentException("Unsupported URI: " + uri); 192 } 193 getContext().getContentResolver().notifyChange(uri, null); 194 return numRows; 195 } 196 197 /** 198 * Implements the "blocked/#" delete. 199 */ 200 private int deleteBlockedNumberWithId(long id, String selection) { 201 throwForNonEmptySelection(selection); 202 203 return deleteBlockedNumber(ID_SELECTION, new String[]{Long.toString(id)}); 204 } 205 206 /** 207 * Implements the "blocked/" delete. 208 */ 209 private int deleteBlockedNumber(String selection, String[] selectionArgs) { 210 final SQLiteDatabase db = mDbHelper.getWritableDatabase(); 211 212 // When selection is specified, compile it within (...) to detect SQL injection. 213 if (!TextUtils.isEmpty(selection)) { 214 db.validateSql("select 1 FROM " + Tables.BLOCKED_NUMBERS + " WHERE " + 215 Utils.wrapSelectionWithParens(selection), 216 /* cancellationSignal =*/ null); 217 } 218 219 return db.delete( 220 BlockedNumberDatabaseHelper.Tables.BLOCKED_NUMBERS, 221 selection, selectionArgs); 222 } 223 224 @Override 225 public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, 226 @Nullable String[] selectionArgs, @Nullable String sortOrder) { 227 enforceReadPermission(); 228 229 return query(uri, projection, selection, selectionArgs, sortOrder, null); 230 } 231 232 @Override 233 public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, 234 @Nullable String[] selectionArgs, @Nullable String sortOrder, 235 @Nullable CancellationSignal cancellationSignal) { 236 enforceReadPermission(); 237 238 final int match = sUriMatcher.match(uri); 239 Cursor cursor; 240 switch (match) { 241 case BLOCKED_LIST: 242 cursor = queryBlockedList(projection, selection, selectionArgs, sortOrder, 243 cancellationSignal); 244 break; 245 case BLOCKED_ID: 246 cursor = queryBlockedListWithId(ContentUris.parseId(uri), projection, selection, 247 cancellationSignal); 248 break; 249 default: 250 throw new IllegalArgumentException("Unsupported URI: " + uri); 251 } 252 // Tell the cursor what uri to watch, so it knows when its source data changes 253 cursor.setNotificationUri(getContext().getContentResolver(), uri); 254 return cursor; 255 } 256 257 /** 258 * Implements the "blocked/#" query. 259 */ 260 private Cursor queryBlockedListWithId(long id, String[] projection, String selection, 261 CancellationSignal cancellationSignal) { 262 throwForNonEmptySelection(selection); 263 264 return queryBlockedList(projection, ID_SELECTION, new String[]{Long.toString(id)}, 265 null, cancellationSignal); 266 } 267 268 /** 269 * Implements the "blocked/" query. 270 */ 271 private Cursor queryBlockedList(String[] projection, String selection, String[] selectionArgs, 272 String sortOrder, CancellationSignal cancellationSignal) { 273 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 274 qb.setStrict(true); 275 qb.setTables(BlockedNumberDatabaseHelper.Tables.BLOCKED_NUMBERS); 276 qb.setProjectionMap(sBlockedNumberColumns); 277 278 return qb.query(mDbHelper.getReadableDatabase(), projection, selection, selectionArgs, 279 /* groupBy =*/ null, /* having =*/null, sortOrder, 280 /* limit =*/ null, cancellationSignal); 281 } 282 283 private void throwForNonEmptySelection(String selection) { 284 if (!TextUtils.isEmpty(selection)) { 285 throw new IllegalArgumentException( 286 "When ID is specified in URI, selection must be null"); 287 } 288 } 289 290 /** 291 * TODO CTS: 292 * - METHOD_IS_BLOCKED with various matching / non-matching arguments. 293 * 294 * - other random methods should fail 295 */ 296 @Override 297 public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) { 298 enforceReadPermission(); 299 300 final Bundle res = new Bundle(); 301 switch (method) { 302 case BlockedNumberContract.METHOD_IS_BLOCKED: 303 res.putBoolean(BlockedNumberContract.RES_NUMBER_IS_BLOCKED, isBlocked(arg)); 304 break; 305 default: 306 throw new IllegalArgumentException("Unsupported method " + method); 307 } 308 return res; 309 } 310 311 private boolean isBlocked(String phoneNumber) { 312 if (TextUtils.isEmpty(phoneNumber)) { 313 return false; 314 } 315 316 final String inE164 = Utils.getE164Number(getContext(), phoneNumber, null); // may be empty. 317 318 if (DEBUG) { 319 Log.d(TAG, String.format("isBlocked: in=%s, e164=%s", phoneNumber, inE164)); 320 } 321 322 final Cursor c = mDbHelper.getReadableDatabase().rawQuery( 323 "SELECT " + 324 BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER + "," + 325 BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER + 326 " FROM " + BlockedNumberDatabaseHelper.Tables.BLOCKED_NUMBERS + 327 " WHERE " + BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER + "=?1" + 328 " OR (?2 != '' AND " + 329 BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER + "=?2)", 330 new String[] {phoneNumber, inE164} 331 ); 332 try { 333 while (c.moveToNext()) { 334 if (DEBUG) { 335 final String original = c.getString(0); 336 final String e164 = c.getString(1); 337 338 Log.d(TAG, String.format("match found: original=%s, e164=%s", original, e164)); 339 } 340 return true; 341 } 342 } finally { 343 c.close(); 344 } 345 // No match found. 346 return false; 347 } 348 349 /** 350 * Throws {@link SecurityException} when the caller is not root, system, the system dialer, 351 * the user selected dialer, or the default SMS app. 352 * 353 * NOT TESTED YET 354 * 355 * TODO CTS: 356 * - Call should fail for random 3p apps. 357 * 358 * TODO Add a permission to allow the contacts app to access? 359 * TODO Add a permission to allow carrier apps? 360 */ 361 public void enforceReadPermission() { 362 final int callingUid = Binder.getCallingUid(); 363 364 // System and root can always call it. (and myself) 365 if (UserHandle.isSameApp(callingUid, android.os.Process.SYSTEM_UID) 366 || (callingUid == Process.ROOT_UID) 367 || (callingUid == Process.myUid())) { 368 return; 369 } 370 371 final String callingPackage = getCallingPackage(); 372 if (TextUtils.isEmpty(callingPackage)) { 373 Log.w(TAG, "callingPackage not accessible"); 374 } else { 375 376 final TelecomManager telecom = getContext().getSystemService(TelecomManager.class); 377 378 if (callingPackage.equals(telecom.getDefaultDialerPackage()) 379 || callingPackage.equals(telecom.getSystemDialerPackage())) { 380 return; 381 } 382 383 // Allow the default SMS app and the dialer app to access it. 384 final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class); 385 386 if (appOps.noteOp(AppOpsManager.OP_WRITE_SMS, 387 Binder.getCallingUid(), callingPackage) == AppOpsManager.MODE_ALLOWED) { 388 return; 389 } 390 391 // TODO: Add an explicit permission instead. 392 try { 393 ApplicationInfo applicationInfo = getContext(). 394 getPackageManager().getPackageInfo(callingPackage, 0).applicationInfo; 395 if (applicationInfo.isPrivilegedApp() || applicationInfo.isSystemApp()) { 396 return; 397 } 398 } catch (PackageManager.NameNotFoundException e) { 399 Log.w(TAG, "package not found: " + e); 400 } 401 } 402 throw new SecurityException("Caller must be system, default dialer or default SMS app"); 403 } 404 405 /** 406 * TODO CTS: 407 * - Call should fail for random 3p apps. 408 */ 409 public void enforceWritePermission() { 410 // Same check as read. 411 enforceReadPermission(); 412 } 413} 414