1 /* 2 * Copyright (C) 2009 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.providers.contacts; 18 19 import static com.android.providers.contacts.util.DbQueryUtils.checkForSupportedColumns; 20 import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause; 21 import static com.android.providers.contacts.util.DbQueryUtils.getInequalityClause; 22 23 import android.app.AppOpsManager; 24 import android.content.ContentProvider; 25 import android.content.ContentProviderOperation; 26 import android.content.ContentProviderResult; 27 import android.content.ContentResolver; 28 import android.content.ContentUris; 29 import android.content.ContentValues; 30 import android.content.Context; 31 import android.content.OperationApplicationException; 32 import android.content.UriMatcher; 33 import android.database.Cursor; 34 import android.database.DatabaseUtils; 35 import android.database.sqlite.SQLiteDatabase; 36 import android.database.sqlite.SQLiteQueryBuilder; 37 import android.net.Uri; 38 import android.os.Binder; 39 import android.os.UserHandle; 40 import android.os.UserManager; 41 import android.provider.CallLog; 42 import android.provider.CallLog.Calls; 43 import android.telecom.PhoneAccount; 44 import android.telecom.PhoneAccountHandle; 45 import android.telecom.TelecomManager; 46 import android.text.TextUtils; 47 import android.util.ArrayMap; 48 import android.util.Log; 49 50 import com.android.internal.annotations.VisibleForTesting; 51 import com.android.internal.util.ProviderAccessStats; 52 import com.android.providers.contacts.CallLogDatabaseHelper.DbProperties; 53 import com.android.providers.contacts.CallLogDatabaseHelper.Tables; 54 import com.android.providers.contacts.util.SelectionBuilder; 55 import com.android.providers.contacts.util.UserUtils; 56 57 import java.io.FileDescriptor; 58 import java.io.PrintWriter; 59 import java.util.ArrayList; 60 import java.util.Arrays; 61 import java.util.List; 62 import java.util.concurrent.CountDownLatch; 63 64 /** 65 * Call log content provider. 66 */ 67 public class CallLogProvider extends ContentProvider { 68 private static final String TAG = "CallLogProvider"; 69 70 public static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE); 71 72 private static final int BACKGROUND_TASK_INITIALIZE = 0; 73 private static final int BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT = 1; 74 75 /** Selection clause for selecting all calls that were made after a certain time */ 76 private static final String MORE_RECENT_THAN_SELECTION = Calls.DATE + "> ?"; 77 /** Selection clause to use to exclude voicemail records. */ 78 private static final String EXCLUDE_VOICEMAIL_SELECTION = getInequalityClause( 79 Calls.TYPE, Calls.VOICEMAIL_TYPE); 80 /** Selection clause to exclude hidden records. */ 81 private static final String EXCLUDE_HIDDEN_SELECTION = getEqualityClause( 82 Calls.PHONE_ACCOUNT_HIDDEN, 0); 83 84 @VisibleForTesting 85 static final String[] CALL_LOG_SYNC_PROJECTION = new String[] { 86 Calls.NUMBER, 87 Calls.NUMBER_PRESENTATION, 88 Calls.TYPE, 89 Calls.FEATURES, 90 Calls.DATE, 91 Calls.DURATION, 92 Calls.DATA_USAGE, 93 Calls.PHONE_ACCOUNT_COMPONENT_NAME, 94 Calls.PHONE_ACCOUNT_ID, 95 Calls.ADD_FOR_ALL_USERS 96 }; 97 98 static final String[] MINIMAL_PROJECTION = new String[] { Calls._ID }; 99 100 private static final int CALLS = 1; 101 102 private static final int CALLS_ID = 2; 103 104 private static final int CALLS_FILTER = 3; 105 106 private static final String UNHIDE_BY_PHONE_ACCOUNT_QUERY = 107 "UPDATE " + Tables.CALLS + " SET " + Calls.PHONE_ACCOUNT_HIDDEN + "=0 WHERE " + 108 Calls.PHONE_ACCOUNT_COMPONENT_NAME + "=? AND " + Calls.PHONE_ACCOUNT_ID + "=?;"; 109 110 private static final String UNHIDE_BY_ADDRESS_QUERY = 111 "UPDATE " + Tables.CALLS + " SET " + Calls.PHONE_ACCOUNT_HIDDEN + "=0 WHERE " + 112 Calls.PHONE_ACCOUNT_ADDRESS + "=?;"; 113 114 private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); 115 static { sURIMatcher.addURI(CallLog.AUTHORITY, "calls", CALLS)116 sURIMatcher.addURI(CallLog.AUTHORITY, "calls", CALLS); sURIMatcher.addURI(CallLog.AUTHORITY, "calls/#", CALLS_ID)117 sURIMatcher.addURI(CallLog.AUTHORITY, "calls/#", CALLS_ID); sURIMatcher.addURI(CallLog.AUTHORITY, "calls/filter/*", CALLS_FILTER)118 sURIMatcher.addURI(CallLog.AUTHORITY, "calls/filter/*", CALLS_FILTER); 119 120 // Shadow provider only supports "/calls". sURIMatcher.addURI(CallLog.SHADOW_AUTHORITY, "calls", CALLS)121 sURIMatcher.addURI(CallLog.SHADOW_AUTHORITY, "calls", CALLS); 122 } 123 124 private static final ArrayMap<String, String> sCallsProjectionMap; 125 static { 126 127 // Calls projection map 128 sCallsProjectionMap = new ArrayMap<>(); sCallsProjectionMap.put(Calls._ID, Calls._ID)129 sCallsProjectionMap.put(Calls._ID, Calls._ID); sCallsProjectionMap.put(Calls.NUMBER, Calls.NUMBER)130 sCallsProjectionMap.put(Calls.NUMBER, Calls.NUMBER); sCallsProjectionMap.put(Calls.POST_DIAL_DIGITS, Calls.POST_DIAL_DIGITS)131 sCallsProjectionMap.put(Calls.POST_DIAL_DIGITS, Calls.POST_DIAL_DIGITS); sCallsProjectionMap.put(Calls.VIA_NUMBER, Calls.VIA_NUMBER)132 sCallsProjectionMap.put(Calls.VIA_NUMBER, Calls.VIA_NUMBER); sCallsProjectionMap.put(Calls.NUMBER_PRESENTATION, Calls.NUMBER_PRESENTATION)133 sCallsProjectionMap.put(Calls.NUMBER_PRESENTATION, Calls.NUMBER_PRESENTATION); sCallsProjectionMap.put(Calls.DATE, Calls.DATE)134 sCallsProjectionMap.put(Calls.DATE, Calls.DATE); sCallsProjectionMap.put(Calls.DURATION, Calls.DURATION)135 sCallsProjectionMap.put(Calls.DURATION, Calls.DURATION); sCallsProjectionMap.put(Calls.DATA_USAGE, Calls.DATA_USAGE)136 sCallsProjectionMap.put(Calls.DATA_USAGE, Calls.DATA_USAGE); sCallsProjectionMap.put(Calls.TYPE, Calls.TYPE)137 sCallsProjectionMap.put(Calls.TYPE, Calls.TYPE); sCallsProjectionMap.put(Calls.FEATURES, Calls.FEATURES)138 sCallsProjectionMap.put(Calls.FEATURES, Calls.FEATURES); sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, Calls.PHONE_ACCOUNT_COMPONENT_NAME)139 sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, Calls.PHONE_ACCOUNT_COMPONENT_NAME); sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_ID, Calls.PHONE_ACCOUNT_ID)140 sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_ID, Calls.PHONE_ACCOUNT_ID); sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_ADDRESS, Calls.PHONE_ACCOUNT_ADDRESS)141 sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_ADDRESS, Calls.PHONE_ACCOUNT_ADDRESS); sCallsProjectionMap.put(Calls.NEW, Calls.NEW)142 sCallsProjectionMap.put(Calls.NEW, Calls.NEW); sCallsProjectionMap.put(Calls.VOICEMAIL_URI, Calls.VOICEMAIL_URI)143 sCallsProjectionMap.put(Calls.VOICEMAIL_URI, Calls.VOICEMAIL_URI); sCallsProjectionMap.put(Calls.TRANSCRIPTION, Calls.TRANSCRIPTION)144 sCallsProjectionMap.put(Calls.TRANSCRIPTION, Calls.TRANSCRIPTION); sCallsProjectionMap.put(Calls.TRANSCRIPTION_STATE, Calls.TRANSCRIPTION_STATE)145 sCallsProjectionMap.put(Calls.TRANSCRIPTION_STATE, Calls.TRANSCRIPTION_STATE); sCallsProjectionMap.put(Calls.IS_READ, Calls.IS_READ)146 sCallsProjectionMap.put(Calls.IS_READ, Calls.IS_READ); sCallsProjectionMap.put(Calls.CACHED_NAME, Calls.CACHED_NAME)147 sCallsProjectionMap.put(Calls.CACHED_NAME, Calls.CACHED_NAME); sCallsProjectionMap.put(Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_TYPE)148 sCallsProjectionMap.put(Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_TYPE); sCallsProjectionMap.put(Calls.CACHED_NUMBER_LABEL, Calls.CACHED_NUMBER_LABEL)149 sCallsProjectionMap.put(Calls.CACHED_NUMBER_LABEL, Calls.CACHED_NUMBER_LABEL); sCallsProjectionMap.put(Calls.COUNTRY_ISO, Calls.COUNTRY_ISO)150 sCallsProjectionMap.put(Calls.COUNTRY_ISO, Calls.COUNTRY_ISO); sCallsProjectionMap.put(Calls.GEOCODED_LOCATION, Calls.GEOCODED_LOCATION)151 sCallsProjectionMap.put(Calls.GEOCODED_LOCATION, Calls.GEOCODED_LOCATION); sCallsProjectionMap.put(Calls.CACHED_LOOKUP_URI, Calls.CACHED_LOOKUP_URI)152 sCallsProjectionMap.put(Calls.CACHED_LOOKUP_URI, Calls.CACHED_LOOKUP_URI); sCallsProjectionMap.put(Calls.CACHED_MATCHED_NUMBER, Calls.CACHED_MATCHED_NUMBER)153 sCallsProjectionMap.put(Calls.CACHED_MATCHED_NUMBER, Calls.CACHED_MATCHED_NUMBER); sCallsProjectionMap.put(Calls.CACHED_NORMALIZED_NUMBER, Calls.CACHED_NORMALIZED_NUMBER)154 sCallsProjectionMap.put(Calls.CACHED_NORMALIZED_NUMBER, Calls.CACHED_NORMALIZED_NUMBER); sCallsProjectionMap.put(Calls.CACHED_PHOTO_ID, Calls.CACHED_PHOTO_ID)155 sCallsProjectionMap.put(Calls.CACHED_PHOTO_ID, Calls.CACHED_PHOTO_ID); sCallsProjectionMap.put(Calls.CACHED_PHOTO_URI, Calls.CACHED_PHOTO_URI)156 sCallsProjectionMap.put(Calls.CACHED_PHOTO_URI, Calls.CACHED_PHOTO_URI); sCallsProjectionMap.put(Calls.CACHED_FORMATTED_NUMBER, Calls.CACHED_FORMATTED_NUMBER)157 sCallsProjectionMap.put(Calls.CACHED_FORMATTED_NUMBER, Calls.CACHED_FORMATTED_NUMBER); sCallsProjectionMap.put(Calls.ADD_FOR_ALL_USERS, Calls.ADD_FOR_ALL_USERS)158 sCallsProjectionMap.put(Calls.ADD_FOR_ALL_USERS, Calls.ADD_FOR_ALL_USERS); sCallsProjectionMap.put(Calls.LAST_MODIFIED, Calls.LAST_MODIFIED)159 sCallsProjectionMap.put(Calls.LAST_MODIFIED, Calls.LAST_MODIFIED); 160 sCallsProjectionMap put(Calls.CALL_SCREENING_COMPONENT_NAME, Calls.CALL_SCREENING_COMPONENT_NAME)161 .put(Calls.CALL_SCREENING_COMPONENT_NAME, Calls.CALL_SCREENING_COMPONENT_NAME); sCallsProjectionMap.put(Calls.CALL_SCREENING_APP_NAME, Calls.CALL_SCREENING_APP_NAME)162 sCallsProjectionMap.put(Calls.CALL_SCREENING_APP_NAME, Calls.CALL_SCREENING_APP_NAME); sCallsProjectionMap.put(Calls.BLOCK_REASON, Calls.BLOCK_REASON)163 sCallsProjectionMap.put(Calls.BLOCK_REASON, Calls.BLOCK_REASON); 164 } 165 166 private static final String ALLOWED_PACKAGE_FOR_TESTING = "com.android.providers.contacts"; 167 168 @VisibleForTesting 169 static final String PARAM_KEY_QUERY_FOR_TESTING = "query_for_testing"; 170 171 /** 172 * A long to override the clock used for timestamps, or "null" to reset to the system clock. 173 */ 174 @VisibleForTesting 175 static final String PARAM_KEY_SET_TIME_FOR_TESTING = "set_time_for_testing"; 176 177 private static Long sTimeForTestMillis; 178 179 private ContactsTaskScheduler mTaskScheduler; 180 181 private volatile CountDownLatch mReadAccessLatch; 182 183 private CallLogDatabaseHelper mDbHelper; 184 private DatabaseUtils.InsertHelper mCallsInserter; 185 private boolean mUseStrictPhoneNumberComparation; 186 private int mMinMatch; 187 private VoicemailPermissions mVoicemailPermissions; 188 private CallLogInsertionHelper mCallLogInsertionHelper; 189 190 private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<>(); 191 private final ThreadLocal<Integer> mCallingUid = new ThreadLocal<>(); 192 private final ProviderAccessStats mStats = new ProviderAccessStats(); 193 isShadow()194 protected boolean isShadow() { 195 return false; 196 } 197 getProviderName()198 protected final String getProviderName() { 199 return this.getClass().getSimpleName(); 200 } 201 202 @Override onCreate()203 public boolean onCreate() { 204 if (VERBOSE_LOGGING) { 205 Log.v(TAG, "onCreate: " + this.getClass().getSimpleName() 206 + " user=" + android.os.Process.myUserHandle().getIdentifier()); 207 } 208 209 setAppOps(AppOpsManager.OP_READ_CALL_LOG, AppOpsManager.OP_WRITE_CALL_LOG); 210 if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) { 211 Log.d(Constants.PERFORMANCE_TAG, getProviderName() + ".onCreate start"); 212 } 213 final Context context = getContext(); 214 mDbHelper = getDatabaseHelper(context); 215 mUseStrictPhoneNumberComparation = 216 context.getResources().getBoolean( 217 com.android.internal.R.bool.config_use_strict_phone_number_comparation); 218 mMinMatch = 219 context.getResources().getInteger( 220 com.android.internal.R.integer.config_phonenumber_compare_min_match); 221 mVoicemailPermissions = new VoicemailPermissions(context); 222 mCallLogInsertionHelper = createCallLogInsertionHelper(context); 223 224 mReadAccessLatch = new CountDownLatch(1); 225 226 mTaskScheduler = new ContactsTaskScheduler(getClass().getSimpleName()) { 227 @Override 228 public void onPerformTask(int taskId, Object arg) { 229 performBackgroundTask(taskId, arg); 230 } 231 }; 232 233 mTaskScheduler.scheduleTask(BACKGROUND_TASK_INITIALIZE, null); 234 235 if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) { 236 Log.d(Constants.PERFORMANCE_TAG, getProviderName() + ".onCreate finish"); 237 } 238 return true; 239 } 240 241 @VisibleForTesting createCallLogInsertionHelper(final Context context)242 protected CallLogInsertionHelper createCallLogInsertionHelper(final Context context) { 243 return DefaultCallLogInsertionHelper.getInstance(context); 244 } 245 246 @VisibleForTesting setMinMatchForTest(int minMatch)247 public void setMinMatchForTest(int minMatch) { 248 mMinMatch = minMatch; 249 } 250 251 @VisibleForTesting getMinMatchForTest()252 public int getMinMatchForTest() { 253 return mMinMatch; 254 } 255 getDatabaseHelper(final Context context)256 protected CallLogDatabaseHelper getDatabaseHelper(final Context context) { 257 return CallLogDatabaseHelper.getInstance(context); 258 } 259 applyingBatch()260 protected boolean applyingBatch() { 261 final Boolean applying = mApplyingBatch.get(); 262 return applying != null && applying; 263 } 264 265 @Override applyBatch(ArrayList<ContentProviderOperation> operations)266 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 267 throws OperationApplicationException { 268 final int callingUid = Binder.getCallingUid(); 269 mCallingUid.set(callingUid); 270 271 mStats.incrementBatchStats(callingUid); 272 mApplyingBatch.set(true); 273 try { 274 return super.applyBatch(operations); 275 } finally { 276 mApplyingBatch.set(false); 277 mStats.finishOperation(callingUid); 278 } 279 } 280 281 @Override bulkInsert(Uri uri, ContentValues[] values)282 public int bulkInsert(Uri uri, ContentValues[] values) { 283 final int callingUid = Binder.getCallingUid(); 284 mCallingUid.set(callingUid); 285 286 mStats.incrementBatchStats(callingUid); 287 mApplyingBatch.set(true); 288 try { 289 return super.bulkInsert(uri, values); 290 } finally { 291 mApplyingBatch.set(false); 292 mStats.finishOperation(callingUid); 293 } 294 } 295 296 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)297 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 298 String sortOrder) { 299 // Note don't use mCallingUid here. That's only used by mutation functions. 300 final int callingUid = Binder.getCallingUid(); 301 302 mStats.incrementQueryStats(callingUid); 303 try { 304 return queryInternal(uri, projection, selection, selectionArgs, sortOrder); 305 } finally { 306 mStats.finishOperation(callingUid); 307 } 308 } 309 queryInternal(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)310 private Cursor queryInternal(Uri uri, String[] projection, String selection, 311 String[] selectionArgs, String sortOrder) { 312 if (VERBOSE_LOGGING) { 313 Log.v(TAG, "query: uri=" + uri + " projection=" + Arrays.toString(projection) + 314 " selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) + 315 " order=[" + sortOrder + "] CPID=" + Binder.getCallingPid() + 316 " CUID=" + Binder.getCallingUid() + 317 " User=" + UserUtils.getCurrentUserHandle(getContext())); 318 } 319 320 queryForTesting(uri); 321 322 waitForAccess(mReadAccessLatch); 323 final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 324 qb.setTables(Tables.CALLS); 325 qb.setProjectionMap(sCallsProjectionMap); 326 qb.setStrict(true); 327 328 final SelectionBuilder selectionBuilder = new SelectionBuilder(selection); 329 checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, true /*isQuery*/); 330 selectionBuilder.addClause(EXCLUDE_HIDDEN_SELECTION); 331 332 final int match = sURIMatcher.match(uri); 333 switch (match) { 334 case CALLS: 335 break; 336 337 case CALLS_ID: { 338 selectionBuilder.addClause(getEqualityClause(Calls._ID, 339 parseCallIdFromUri(uri))); 340 break; 341 } 342 343 case CALLS_FILTER: { 344 List<String> pathSegments = uri.getPathSegments(); 345 String phoneNumber = pathSegments.size() >= 2 ? pathSegments.get(2) : null; 346 if (!TextUtils.isEmpty(phoneNumber)) { 347 qb.appendWhere("PHONE_NUMBERS_EQUAL(number, "); 348 qb.appendWhereEscapeString(phoneNumber); 349 qb.appendWhere(mUseStrictPhoneNumberComparation ? ", 1)" 350 : ", 0, " + mMinMatch + ")"); 351 } else { 352 qb.appendWhere(Calls.NUMBER_PRESENTATION + "!=" 353 + Calls.PRESENTATION_ALLOWED); 354 } 355 break; 356 } 357 358 default: 359 throw new IllegalArgumentException("Unknown URL " + uri); 360 } 361 362 final int limit = getIntParam(uri, Calls.LIMIT_PARAM_KEY, 0); 363 final int offset = getIntParam(uri, Calls.OFFSET_PARAM_KEY, 0); 364 String limitClause = null; 365 if (limit > 0) { 366 limitClause = offset + "," + limit; 367 } 368 369 final SQLiteDatabase db = mDbHelper.getReadableDatabase(); 370 final Cursor c = qb.query(db, projection, selectionBuilder.build(), selectionArgs, null, 371 null, sortOrder, limitClause); 372 if (c != null) { 373 c.setNotificationUri(getContext().getContentResolver(), CallLog.CONTENT_URI); 374 } 375 return c; 376 } 377 queryForTesting(Uri uri)378 private void queryForTesting(Uri uri) { 379 if (!uri.getBooleanQueryParameter(PARAM_KEY_QUERY_FOR_TESTING, false)) { 380 return; 381 } 382 if (!getCallingPackage().equals(ALLOWED_PACKAGE_FOR_TESTING)) { 383 throw new IllegalArgumentException("query_for_testing set from foreign package " 384 + getCallingPackage()); 385 } 386 387 String timeString = uri.getQueryParameter(PARAM_KEY_SET_TIME_FOR_TESTING); 388 if (timeString != null) { 389 if (timeString.equals("null")) { 390 sTimeForTestMillis = null; 391 } else { 392 sTimeForTestMillis = Long.parseLong(timeString); 393 } 394 } 395 } 396 397 @VisibleForTesting getTimeForTestMillis()398 static Long getTimeForTestMillis() { 399 return sTimeForTestMillis; 400 } 401 402 /** 403 * Gets an integer query parameter from a given uri. 404 * 405 * @param uri The uri to extract the query parameter from. 406 * @param key The query parameter key. 407 * @param defaultValue A default value to return if the query parameter does not exist. 408 * @return The value from the query parameter in the Uri. Or the default value if the parameter 409 * does not exist in the uri. 410 * @throws IllegalArgumentException when the value in the query parameter is not an integer. 411 */ getIntParam(Uri uri, String key, int defaultValue)412 private int getIntParam(Uri uri, String key, int defaultValue) { 413 String valueString = uri.getQueryParameter(key); 414 if (valueString == null) { 415 return defaultValue; 416 } 417 418 try { 419 return Integer.parseInt(valueString); 420 } catch (NumberFormatException e) { 421 String msg = "Integer required for " + key + " parameter but value '" + valueString + 422 "' was found instead."; 423 throw new IllegalArgumentException(msg, e); 424 } 425 } 426 427 @Override getType(Uri uri)428 public String getType(Uri uri) { 429 int match = sURIMatcher.match(uri); 430 switch (match) { 431 case CALLS: 432 return Calls.CONTENT_TYPE; 433 case CALLS_ID: 434 return Calls.CONTENT_ITEM_TYPE; 435 case CALLS_FILTER: 436 return Calls.CONTENT_TYPE; 437 default: 438 throw new IllegalArgumentException("Unknown URI: " + uri); 439 } 440 } 441 442 @Override insert(Uri uri, ContentValues values)443 public Uri insert(Uri uri, ContentValues values) { 444 final int callingUid = 445 applyingBatch() ? mCallingUid.get() : Binder.getCallingUid(); 446 447 mStats.incrementInsertStats(callingUid, applyingBatch()); 448 try { 449 return insertInternal(uri, values); 450 } finally { 451 mStats.finishOperation(callingUid); 452 } 453 } 454 455 @Override update(Uri uri, ContentValues values, String selection, String[] selectionArgs)456 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 457 final int callingUid = 458 applyingBatch() ? mCallingUid.get() : Binder.getCallingUid(); 459 460 mStats.incrementInsertStats(callingUid, applyingBatch()); 461 try { 462 return updateInternal(uri, values, selection, selectionArgs); 463 } finally { 464 mStats.finishOperation(callingUid); 465 } 466 } 467 468 @Override delete(Uri uri, String selection, String[] selectionArgs)469 public int delete(Uri uri, String selection, String[] selectionArgs) { 470 final int callingUid = 471 applyingBatch() ? mCallingUid.get() : Binder.getCallingUid(); 472 473 mStats.incrementInsertStats(callingUid, applyingBatch()); 474 try { 475 return deleteInternal(uri, selection, selectionArgs); 476 } finally { 477 mStats.finishOperation(callingUid); 478 } 479 } 480 insertInternal(Uri uri, ContentValues values)481 private Uri insertInternal(Uri uri, ContentValues values) { 482 if (VERBOSE_LOGGING) { 483 Log.v(TAG, "insert: uri=" + uri + " values=[" + values + "]" + 484 " CPID=" + Binder.getCallingPid() + 485 " CUID=" + Binder.getCallingUid()); 486 } 487 waitForAccess(mReadAccessLatch); 488 checkForSupportedColumns(sCallsProjectionMap, values); 489 // Inserting a voicemail record through call_log requires the voicemail 490 // permission and also requires the additional voicemail param set. 491 if (hasVoicemailValue(values)) { 492 checkIsAllowVoicemailRequest(uri); 493 mVoicemailPermissions.checkCallerHasWriteAccess(getCallingPackage()); 494 } 495 if (mCallsInserter == null) { 496 SQLiteDatabase db = mDbHelper.getWritableDatabase(); 497 mCallsInserter = new DatabaseUtils.InsertHelper(db, Tables.CALLS); 498 } 499 500 ContentValues copiedValues = new ContentValues(values); 501 502 // Add the computed fields to the copied values. 503 mCallLogInsertionHelper.addComputedValues(copiedValues); 504 505 long rowId = createDatabaseModifier(mCallsInserter).insert(copiedValues); 506 if (rowId > 0) { 507 return ContentUris.withAppendedId(uri, rowId); 508 } 509 return null; 510 } 511 updateInternal(Uri uri, ContentValues values, String selection, String[] selectionArgs)512 private int updateInternal(Uri uri, ContentValues values, 513 String selection, String[] selectionArgs) { 514 if (VERBOSE_LOGGING) { 515 Log.v(TAG, "update: uri=" + uri + 516 " selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) + 517 " values=[" + values + "] CPID=" + Binder.getCallingPid() + 518 " CUID=" + Binder.getCallingUid() + 519 " User=" + UserUtils.getCurrentUserHandle(getContext())); 520 } 521 waitForAccess(mReadAccessLatch); 522 checkForSupportedColumns(sCallsProjectionMap, values); 523 // Request that involves changing record type to voicemail requires the 524 // voicemail param set in the uri. 525 if (hasVoicemailValue(values)) { 526 checkIsAllowVoicemailRequest(uri); 527 } 528 529 SelectionBuilder selectionBuilder = new SelectionBuilder(selection); 530 checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, false /*isQuery*/); 531 532 final SQLiteDatabase db = mDbHelper.getWritableDatabase(); 533 final int matchedUriId = sURIMatcher.match(uri); 534 switch (matchedUriId) { 535 case CALLS: 536 break; 537 538 case CALLS_ID: 539 selectionBuilder.addClause(getEqualityClause(Calls._ID, parseCallIdFromUri(uri))); 540 break; 541 542 default: 543 throw new UnsupportedOperationException("Cannot update URL: " + uri); 544 } 545 546 return createDatabaseModifier(db).update(uri, Tables.CALLS, values, selectionBuilder.build(), 547 selectionArgs); 548 } 549 deleteInternal(Uri uri, String selection, String[] selectionArgs)550 private int deleteInternal(Uri uri, String selection, String[] selectionArgs) { 551 if (VERBOSE_LOGGING) { 552 Log.v(TAG, "delete: uri=" + uri + 553 " selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) + 554 " CPID=" + Binder.getCallingPid() + 555 " CUID=" + Binder.getCallingUid() + 556 " User=" + UserUtils.getCurrentUserHandle(getContext())); 557 } 558 waitForAccess(mReadAccessLatch); 559 SelectionBuilder selectionBuilder = new SelectionBuilder(selection); 560 checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, false /*isQuery*/); 561 562 final SQLiteDatabase db = mDbHelper.getWritableDatabase(); 563 final int matchedUriId = sURIMatcher.match(uri); 564 switch (matchedUriId) { 565 case CALLS: 566 // TODO: Special case - We may want to forward the delete request on user 0 to the 567 // shadow provider too. 568 return createDatabaseModifier(db).delete(Tables.CALLS, 569 selectionBuilder.build(), selectionArgs); 570 default: 571 throw new UnsupportedOperationException("Cannot delete that URL: " + uri); 572 } 573 } 574 adjustForNewPhoneAccount(PhoneAccountHandle handle)575 void adjustForNewPhoneAccount(PhoneAccountHandle handle) { 576 mTaskScheduler.scheduleTask(BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT, handle); 577 } 578 579 /** 580 * Returns a {@link DatabaseModifier} that takes care of sending necessary notifications 581 * after the operation is performed. 582 */ createDatabaseModifier(SQLiteDatabase db)583 private DatabaseModifier createDatabaseModifier(SQLiteDatabase db) { 584 return new DbModifierWithNotification(Tables.CALLS, db, getContext()); 585 } 586 587 /** 588 * Same as {@link #createDatabaseModifier(SQLiteDatabase)} but used for insert helper operations 589 * only. 590 */ createDatabaseModifier(DatabaseUtils.InsertHelper insertHelper)591 private DatabaseModifier createDatabaseModifier(DatabaseUtils.InsertHelper insertHelper) { 592 return new DbModifierWithNotification(Tables.CALLS, insertHelper, getContext()); 593 } 594 595 private static final Integer VOICEMAIL_TYPE = new Integer(Calls.VOICEMAIL_TYPE); hasVoicemailValue(ContentValues values)596 private boolean hasVoicemailValue(ContentValues values) { 597 return VOICEMAIL_TYPE.equals(values.getAsInteger(Calls.TYPE)); 598 } 599 600 /** 601 * Checks if the supplied uri requests to include voicemails and take appropriate 602 * action. 603 * <p> If voicemail is requested, then check for voicemail permissions. Otherwise 604 * modify the selection to restrict to non-voicemail entries only. 605 */ checkVoicemailPermissionAndAddRestriction(Uri uri, SelectionBuilder selectionBuilder, boolean isQuery)606 private void checkVoicemailPermissionAndAddRestriction(Uri uri, 607 SelectionBuilder selectionBuilder, boolean isQuery) { 608 if (isAllowVoicemailRequest(uri)) { 609 if (isQuery) { 610 mVoicemailPermissions.checkCallerHasReadAccess(getCallingPackage()); 611 } else { 612 mVoicemailPermissions.checkCallerHasWriteAccess(getCallingPackage()); 613 } 614 } else { 615 selectionBuilder.addClause(EXCLUDE_VOICEMAIL_SELECTION); 616 } 617 } 618 619 /** 620 * Determines if the supplied uri has the request to allow voicemails to be 621 * included. 622 */ isAllowVoicemailRequest(Uri uri)623 private boolean isAllowVoicemailRequest(Uri uri) { 624 return uri.getBooleanQueryParameter(Calls.ALLOW_VOICEMAILS_PARAM_KEY, false); 625 } 626 627 /** 628 * Checks to ensure that the given uri has allow_voicemail set. Used by 629 * insert and update operations to check that ContentValues with voicemail 630 * call type must use the voicemail uri. 631 * @throws IllegalArgumentException if allow_voicemail is not set. 632 */ checkIsAllowVoicemailRequest(Uri uri)633 private void checkIsAllowVoicemailRequest(Uri uri) { 634 if (!isAllowVoicemailRequest(uri)) { 635 throw new IllegalArgumentException( 636 String.format("Uri %s cannot be used for voicemail record." + 637 " Please set '%s=true' in the uri.", uri, 638 Calls.ALLOW_VOICEMAILS_PARAM_KEY)); 639 } 640 } 641 642 /** 643 * Parses the call Id from the given uri, assuming that this is a uri that 644 * matches CALLS_ID. For other uri types the behaviour is undefined. 645 * @throws IllegalArgumentException if the id included in the Uri is not a valid long value. 646 */ parseCallIdFromUri(Uri uri)647 private long parseCallIdFromUri(Uri uri) { 648 try { 649 return Long.parseLong(uri.getPathSegments().get(1)); 650 } catch (NumberFormatException e) { 651 throw new IllegalArgumentException("Invalid call id in uri: " + uri, e); 652 } 653 } 654 655 /** 656 * Sync all calllog entries that were inserted 657 */ syncEntries()658 private void syncEntries() { 659 if (isShadow()) { 660 return; // It's the shadow provider itself. No copying. 661 } 662 663 final UserManager userManager = UserUtils.getUserManager(getContext()); 664 665 // TODO: http://b/24944959 666 if (!Calls.shouldHaveSharedCallLogEntries(getContext(), userManager, 667 userManager.getUserHandle())) { 668 return; 669 } 670 671 final int myUserId = userManager.getUserHandle(); 672 673 // See the comment in Calls.addCall() for the logic. 674 675 if (userManager.isSystemUser()) { 676 // If it's the system user, just copy from shadow. 677 syncEntriesFrom(UserHandle.USER_SYSTEM, /* sourceIsShadow = */ true, 678 /* forAllUsersOnly =*/ false); 679 } else { 680 // Otherwise, copy from system's real provider, as well as self's shadow. 681 syncEntriesFrom(UserHandle.USER_SYSTEM, /* sourceIsShadow = */ false, 682 /* forAllUsersOnly =*/ true); 683 syncEntriesFrom(myUserId, /* sourceIsShadow = */ true, 684 /* forAllUsersOnly =*/ false); 685 } 686 } 687 syncEntriesFrom(int sourceUserId, boolean sourceIsShadow, boolean forAllUsersOnly)688 private void syncEntriesFrom(int sourceUserId, boolean sourceIsShadow, 689 boolean forAllUsersOnly) { 690 691 final Uri sourceUri = sourceIsShadow ? Calls.SHADOW_CONTENT_URI : Calls.CONTENT_URI; 692 693 final long lastSyncTime = getLastSyncTime(sourceIsShadow); 694 695 final Uri uri = ContentProvider.maybeAddUserId(sourceUri, sourceUserId); 696 final long newestTimeStamp; 697 final ContentResolver cr = getContext().getContentResolver(); 698 699 final StringBuilder selection = new StringBuilder(); 700 701 selection.append( 702 "(" + EXCLUDE_VOICEMAIL_SELECTION + ") AND (" + MORE_RECENT_THAN_SELECTION + ")"); 703 704 if (forAllUsersOnly) { 705 selection.append(" AND (" + Calls.ADD_FOR_ALL_USERS + "=1)"); 706 } 707 708 final Cursor cursor = cr.query( 709 uri, 710 CALL_LOG_SYNC_PROJECTION, 711 selection.toString(), 712 new String[] {String.valueOf(lastSyncTime)}, 713 Calls.DATE + " ASC"); 714 if (cursor == null) { 715 return; 716 } 717 try { 718 newestTimeStamp = copyEntriesFromCursor(cursor, lastSyncTime, sourceIsShadow); 719 } finally { 720 cursor.close(); 721 } 722 if (sourceIsShadow) { 723 // delete all entries in shadow. 724 cr.delete(uri, Calls.DATE + "<= ?", new String[] {String.valueOf(newestTimeStamp)}); 725 } 726 } 727 728 /** 729 * Un-hides any hidden call log entries that are associated with the specified handle. 730 * 731 * @param handle The handle to the newly registered {@link android.telecom.PhoneAccount}. 732 */ adjustForNewPhoneAccountInternal(PhoneAccountHandle handle)733 private void adjustForNewPhoneAccountInternal(PhoneAccountHandle handle) { 734 String[] handleArgs = 735 new String[] { handle.getComponentName().flattenToString(), handle.getId() }; 736 737 // Check to see if any entries exist for this handle. If so (not empty), run the un-hiding 738 // update. If not, then try to identify the call from the phone number. 739 Cursor cursor = query(Calls.CONTENT_URI, MINIMAL_PROJECTION, 740 Calls.PHONE_ACCOUNT_COMPONENT_NAME + " =? AND " + Calls.PHONE_ACCOUNT_ID + " =?", 741 handleArgs, null); 742 743 if (cursor != null) { 744 try { 745 if (cursor.getCount() >= 1) { 746 // run un-hiding process based on phone account 747 mDbHelper.getWritableDatabase().execSQL( 748 UNHIDE_BY_PHONE_ACCOUNT_QUERY, handleArgs); 749 } else { 750 TelecomManager tm = getContext().getSystemService(TelecomManager.class); 751 if (tm != null) { 752 PhoneAccount account = tm.getPhoneAccount(handle); 753 if (account != null && account.getAddress() != null) { 754 // We did not find any items for the specific phone account, so run the 755 // query based on the phone number instead. 756 mDbHelper.getWritableDatabase().execSQL(UNHIDE_BY_ADDRESS_QUERY, 757 new String[] { account.getAddress().toString() }); 758 } 759 760 } 761 } 762 } finally { 763 cursor.close(); 764 } 765 } 766 767 } 768 769 /** 770 * @param cursor to copy call log entries from 771 */ 772 @VisibleForTesting copyEntriesFromCursor(Cursor cursor, long lastSyncTime, boolean forShadow)773 long copyEntriesFromCursor(Cursor cursor, long lastSyncTime, boolean forShadow) { 774 long latestTimestamp = 0; 775 final ContentValues values = new ContentValues(); 776 final SQLiteDatabase db = mDbHelper.getWritableDatabase(); 777 db.beginTransaction(); 778 try { 779 final String[] args = new String[2]; 780 cursor.moveToPosition(-1); 781 while (cursor.moveToNext()) { 782 values.clear(); 783 DatabaseUtils.cursorRowToContentValues(cursor, values); 784 785 final String startTime = values.getAsString(Calls.DATE); 786 final String number = values.getAsString(Calls.NUMBER); 787 788 if (startTime == null || number == null) { 789 continue; 790 } 791 792 if (cursor.isLast()) { 793 try { 794 latestTimestamp = Long.valueOf(startTime); 795 } catch (NumberFormatException e) { 796 Log.e(TAG, "Call log entry does not contain valid start time: " 797 + startTime); 798 } 799 } 800 801 // Avoid duplicating an already existing entry (which is uniquely identified by 802 // the number, and the start time) 803 args[0] = startTime; 804 args[1] = number; 805 if (DatabaseUtils.queryNumEntries(db, Tables.CALLS, 806 Calls.DATE + " = ? AND " + Calls.NUMBER + " = ?", args) > 0) { 807 continue; 808 } 809 810 db.insert(Tables.CALLS, null, values); 811 } 812 813 if (latestTimestamp > lastSyncTime) { 814 setLastTimeSynced(latestTimestamp, forShadow); 815 } 816 817 db.setTransactionSuccessful(); 818 } finally { 819 db.endTransaction(); 820 } 821 return latestTimestamp; 822 } 823 getLastSyncTimePropertyName(boolean forShadow)824 private static String getLastSyncTimePropertyName(boolean forShadow) { 825 return forShadow 826 ? DbProperties.CALL_LOG_LAST_SYNCED_FOR_SHADOW 827 : DbProperties.CALL_LOG_LAST_SYNCED; 828 } 829 830 @VisibleForTesting getLastSyncTime(boolean forShadow)831 long getLastSyncTime(boolean forShadow) { 832 try { 833 return Long.valueOf(mDbHelper.getProperty(getLastSyncTimePropertyName(forShadow), "0")); 834 } catch (NumberFormatException e) { 835 return 0; 836 } 837 } 838 setLastTimeSynced(long time, boolean forShadow)839 private void setLastTimeSynced(long time, boolean forShadow) { 840 mDbHelper.setProperty(getLastSyncTimePropertyName(forShadow), String.valueOf(time)); 841 } 842 waitForAccess(CountDownLatch latch)843 private static void waitForAccess(CountDownLatch latch) { 844 if (latch == null) { 845 return; 846 } 847 848 while (true) { 849 try { 850 latch.await(); 851 return; 852 } catch (InterruptedException e) { 853 Thread.currentThread().interrupt(); 854 } 855 } 856 } 857 performBackgroundTask(int task, Object arg)858 private void performBackgroundTask(int task, Object arg) { 859 if (task == BACKGROUND_TASK_INITIALIZE) { 860 try { 861 syncEntries(); 862 } finally { 863 mReadAccessLatch.countDown(); 864 mReadAccessLatch = null; 865 } 866 } else if (task == BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT) { 867 adjustForNewPhoneAccountInternal((PhoneAccountHandle) arg); 868 } 869 } 870 871 @Override shutdown()872 public void shutdown() { 873 mTaskScheduler.shutdownForTest(); 874 } 875 876 @Override dump(FileDescriptor fd, PrintWriter writer, String[] args)877 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 878 mStats.dump(writer, " "); 879 } 880 } 881