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