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