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