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