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