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.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.app.AppOpsManager; 26 import android.content.ContentProvider; 27 import android.content.ContentProviderOperation; 28 import android.content.ContentProviderResult; 29 import android.content.ContentResolver; 30 import android.content.ContentUris; 31 import android.content.ContentValues; 32 import android.content.Context; 33 import android.content.OperationApplicationException; 34 import android.content.UriMatcher; 35 import android.database.Cursor; 36 import android.database.DatabaseUtils; 37 import android.database.sqlite.SQLiteDatabase; 38 import android.database.sqlite.SQLiteQueryBuilder; 39 import android.net.Uri; 40 import android.os.Binder; 41 import android.os.Bundle; 42 import android.os.ParcelFileDescriptor; 43 import android.os.ParcelableException; 44 import android.os.StatFs; 45 import android.os.UserHandle; 46 import android.os.UserManager; 47 import android.provider.CallLog; 48 import android.provider.CallLog.Calls; 49 import android.telecom.PhoneAccount; 50 import android.telecom.PhoneAccountHandle; 51 import android.telecom.TelecomManager; 52 import android.telephony.TelephonyManager; 53 import android.text.TextUtils; 54 import android.util.ArrayMap; 55 import android.util.Log; 56 57 import com.android.internal.annotations.VisibleForTesting; 58 import com.android.internal.util.ProviderAccessStats; 59 import com.android.providers.contacts.CallLogDatabaseHelper.DbProperties; 60 import com.android.providers.contacts.CallLogDatabaseHelper.Tables; 61 import com.android.providers.contacts.util.SelectionBuilder; 62 import com.android.providers.contacts.util.UserUtils; 63 64 import java.io.FileDescriptor; 65 import java.io.FileInputStream; 66 import java.io.FileNotFoundException; 67 import java.io.IOException; 68 import java.io.OutputStream; 69 import java.io.PrintWriter; 70 import java.io.StringWriter; 71 import java.nio.file.DirectoryStream; 72 import java.nio.file.Files; 73 import java.nio.file.NoSuchFileException; 74 import java.nio.file.OpenOption; 75 import java.nio.file.Path; 76 import java.nio.file.StandardOpenOption; 77 import java.nio.file.attribute.FileTime; 78 import java.util.ArrayList; 79 import java.util.Arrays; 80 import java.util.List; 81 import java.util.UUID; 82 import java.util.concurrent.CountDownLatch; 83 import java.util.stream.Collectors; 84 85 /** 86 * Call log content provider. 87 */ 88 public class CallLogProvider extends ContentProvider { 89 private static final String TAG = "CallLogProvider"; 90 91 public static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE); 92 93 private static final int BACKGROUND_TASK_INITIALIZE = 0; 94 private static final int BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT = 1; 95 96 /** Selection clause for selecting all calls that were made after a certain time */ 97 private static final String MORE_RECENT_THAN_SELECTION = Calls.DATE + "> ?"; 98 /** Selection clause to use to exclude voicemail records. */ 99 private static final String EXCLUDE_VOICEMAIL_SELECTION = getInequalityClause( 100 Calls.TYPE, Calls.VOICEMAIL_TYPE); 101 /** Selection clause to exclude hidden records. */ 102 private static final String EXCLUDE_HIDDEN_SELECTION = getEqualityClause( 103 Calls.PHONE_ACCOUNT_HIDDEN, 0); 104 105 private static final String CALL_COMPOSER_PICTURE_DIRECTORY_NAME = "call_composer_pics"; 106 private static final String CALL_COMPOSER_ALL_USERS_DIRECTORY_NAME = "all_users"; 107 108 // Constants to be used with ContentProvider#call in order to sync call composer pics between 109 // users. Defined here because they're for internal use only. 110 /** 111 * Method name used to get a list of {@link Uri}s for call composer pictures inserted for all 112 * users after a certain date 113 */ 114 private static final String GET_CALL_COMPOSER_IMAGE_URIS = 115 "com.android.providers.contacts.GET_CALL_COMPOSER_IMAGE_URIS"; 116 117 /** 118 * Long-valued extra containing the date to filter by expressed as milliseconds after the epoch. 119 */ 120 private static final String EXTRA_SINCE_DATE = 121 "com.android.providers.contacts.extras.SINCE_DATE"; 122 123 /** 124 * Boolean-valued extra indicating whether to read from the shadow portion of the calllog 125 * (i.e. device-encrypted storage rather than credential-encrypted) 126 */ 127 private static final String EXTRA_IS_SHADOW = 128 "com.android.providers.contacts.extras.IS_SHADOW"; 129 130 /** 131 * Boolean-valued extra indicating whether to return Uris only for those images that are 132 * supposed to be inserted for all users. 133 */ 134 private static final String EXTRA_ALL_USERS_ONLY = 135 "com.android.providers.contacts.extras.ALL_USERS_ONLY"; 136 137 private static final String EXTRA_RESULT_URIS = 138 "com.android.provider.contacts.extras.EXTRA_RESULT_URIS"; 139 140 @VisibleForTesting 141 static final String[] CALL_LOG_SYNC_PROJECTION = new String[] { 142 Calls.NUMBER, 143 Calls.NUMBER_PRESENTATION, 144 Calls.TYPE, 145 Calls.FEATURES, 146 Calls.DATE, 147 Calls.DURATION, 148 Calls.DATA_USAGE, 149 Calls.PHONE_ACCOUNT_COMPONENT_NAME, 150 Calls.PHONE_ACCOUNT_ID, 151 Calls.PRIORITY, 152 Calls.SUBJECT, 153 Calls.COMPOSER_PHOTO_URI, 154 // Location is deliberately omitted 155 Calls.ADD_FOR_ALL_USERS 156 }; 157 158 static final String[] MINIMAL_PROJECTION = new String[] { Calls._ID }; 159 160 private static final int CALLS = 1; 161 162 private static final int CALLS_ID = 2; 163 164 private static final int CALLS_FILTER = 3; 165 166 private static final int CALL_COMPOSER_NEW_PICTURE = 4; 167 168 private static final int CALL_COMPOSER_PICTURE = 5; 169 170 private static final String UNHIDE_BY_PHONE_ACCOUNT_QUERY = 171 "UPDATE " + Tables.CALLS + " SET " + Calls.PHONE_ACCOUNT_HIDDEN + "=0 WHERE " + 172 Calls.PHONE_ACCOUNT_COMPONENT_NAME + "=? AND " + Calls.PHONE_ACCOUNT_ID + "=?;"; 173 174 private static final String UNHIDE_BY_ADDRESS_QUERY = 175 "UPDATE " + Tables.CALLS + " SET " + Calls.PHONE_ACCOUNT_HIDDEN + "=0 WHERE " + 176 Calls.PHONE_ACCOUNT_ADDRESS + "=?;"; 177 178 private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); 179 static { sURIMatcher.addURI(CallLog.AUTHORITY, R, CALLS)180 sURIMatcher.addURI(CallLog.AUTHORITY, "calls", CALLS); sURIMatcher.addURI(CallLog.AUTHORITY, R, CALLS_ID)181 sURIMatcher.addURI(CallLog.AUTHORITY, "calls/#", CALLS_ID); sURIMatcher.addURI(CallLog.AUTHORITY, R, CALLS_FILTER)182 sURIMatcher.addURI(CallLog.AUTHORITY, "calls/filter/*", CALLS_FILTER); sURIMatcher.addURI(CallLog.AUTHORITY, CallLog.CALL_COMPOSER_SEGMENT, CALL_COMPOSER_NEW_PICTURE)183 sURIMatcher.addURI(CallLog.AUTHORITY, CallLog.CALL_COMPOSER_SEGMENT, 184 CALL_COMPOSER_NEW_PICTURE); sURIMatcher.addURI(CallLog.AUTHORITY, CallLog.CALL_COMPOSER_SEGMENT + R, CALL_COMPOSER_PICTURE)185 sURIMatcher.addURI(CallLog.AUTHORITY, CallLog.CALL_COMPOSER_SEGMENT + "/*", 186 CALL_COMPOSER_PICTURE); 187 188 // Shadow provider only supports "/calls" and "/call_composer". sURIMatcher.addURI(CallLog.SHADOW_AUTHORITY, R, CALLS)189 sURIMatcher.addURI(CallLog.SHADOW_AUTHORITY, "calls", CALLS); sURIMatcher.addURI(CallLog.SHADOW_AUTHORITY, CallLog.CALL_COMPOSER_SEGMENT, CALL_COMPOSER_NEW_PICTURE)190 sURIMatcher.addURI(CallLog.SHADOW_AUTHORITY, CallLog.CALL_COMPOSER_SEGMENT, 191 CALL_COMPOSER_NEW_PICTURE); sURIMatcher.addURI(CallLog.SHADOW_AUTHORITY, CallLog.CALL_COMPOSER_SEGMENT + R, CALL_COMPOSER_PICTURE)192 sURIMatcher.addURI(CallLog.SHADOW_AUTHORITY, CallLog.CALL_COMPOSER_SEGMENT + "/*", 193 CALL_COMPOSER_PICTURE); 194 } 195 196 public static final ArrayMap<String, String> sCallsProjectionMap; 197 static { 198 199 // Calls projection map 200 sCallsProjectionMap = new ArrayMap<>(); sCallsProjectionMap.put(Calls._ID, Calls._ID)201 sCallsProjectionMap.put(Calls._ID, Calls._ID); sCallsProjectionMap.put(Calls.NUMBER, Calls.NUMBER)202 sCallsProjectionMap.put(Calls.NUMBER, Calls.NUMBER); sCallsProjectionMap.put(Calls.POST_DIAL_DIGITS, Calls.POST_DIAL_DIGITS)203 sCallsProjectionMap.put(Calls.POST_DIAL_DIGITS, Calls.POST_DIAL_DIGITS); sCallsProjectionMap.put(Calls.VIA_NUMBER, Calls.VIA_NUMBER)204 sCallsProjectionMap.put(Calls.VIA_NUMBER, Calls.VIA_NUMBER); sCallsProjectionMap.put(Calls.NUMBER_PRESENTATION, Calls.NUMBER_PRESENTATION)205 sCallsProjectionMap.put(Calls.NUMBER_PRESENTATION, Calls.NUMBER_PRESENTATION); sCallsProjectionMap.put(Calls.DATE, Calls.DATE)206 sCallsProjectionMap.put(Calls.DATE, Calls.DATE); sCallsProjectionMap.put(Calls.DURATION, Calls.DURATION)207 sCallsProjectionMap.put(Calls.DURATION, Calls.DURATION); sCallsProjectionMap.put(Calls.DATA_USAGE, Calls.DATA_USAGE)208 sCallsProjectionMap.put(Calls.DATA_USAGE, Calls.DATA_USAGE); sCallsProjectionMap.put(Calls.TYPE, Calls.TYPE)209 sCallsProjectionMap.put(Calls.TYPE, Calls.TYPE); sCallsProjectionMap.put(Calls.FEATURES, Calls.FEATURES)210 sCallsProjectionMap.put(Calls.FEATURES, Calls.FEATURES); sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, Calls.PHONE_ACCOUNT_COMPONENT_NAME)211 sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, Calls.PHONE_ACCOUNT_COMPONENT_NAME); sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_ID, Calls.PHONE_ACCOUNT_ID)212 sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_ID, Calls.PHONE_ACCOUNT_ID); sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_HIDDEN, Calls.PHONE_ACCOUNT_HIDDEN)213 sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_HIDDEN, Calls.PHONE_ACCOUNT_HIDDEN); sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_ADDRESS, Calls.PHONE_ACCOUNT_ADDRESS)214 sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_ADDRESS, Calls.PHONE_ACCOUNT_ADDRESS); sCallsProjectionMap.put(Calls.NEW, Calls.NEW)215 sCallsProjectionMap.put(Calls.NEW, Calls.NEW); sCallsProjectionMap.put(Calls.VOICEMAIL_URI, Calls.VOICEMAIL_URI)216 sCallsProjectionMap.put(Calls.VOICEMAIL_URI, Calls.VOICEMAIL_URI); sCallsProjectionMap.put(Calls.TRANSCRIPTION, Calls.TRANSCRIPTION)217 sCallsProjectionMap.put(Calls.TRANSCRIPTION, Calls.TRANSCRIPTION); sCallsProjectionMap.put(Calls.TRANSCRIPTION_STATE, Calls.TRANSCRIPTION_STATE)218 sCallsProjectionMap.put(Calls.TRANSCRIPTION_STATE, Calls.TRANSCRIPTION_STATE); sCallsProjectionMap.put(Calls.IS_READ, Calls.IS_READ)219 sCallsProjectionMap.put(Calls.IS_READ, Calls.IS_READ); sCallsProjectionMap.put(Calls.CACHED_NAME, Calls.CACHED_NAME)220 sCallsProjectionMap.put(Calls.CACHED_NAME, Calls.CACHED_NAME); sCallsProjectionMap.put(Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_TYPE)221 sCallsProjectionMap.put(Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_TYPE); sCallsProjectionMap.put(Calls.CACHED_NUMBER_LABEL, Calls.CACHED_NUMBER_LABEL)222 sCallsProjectionMap.put(Calls.CACHED_NUMBER_LABEL, Calls.CACHED_NUMBER_LABEL); sCallsProjectionMap.put(Calls.COUNTRY_ISO, Calls.COUNTRY_ISO)223 sCallsProjectionMap.put(Calls.COUNTRY_ISO, Calls.COUNTRY_ISO); sCallsProjectionMap.put(Calls.GEOCODED_LOCATION, Calls.GEOCODED_LOCATION)224 sCallsProjectionMap.put(Calls.GEOCODED_LOCATION, Calls.GEOCODED_LOCATION); sCallsProjectionMap.put(Calls.CACHED_LOOKUP_URI, Calls.CACHED_LOOKUP_URI)225 sCallsProjectionMap.put(Calls.CACHED_LOOKUP_URI, Calls.CACHED_LOOKUP_URI); sCallsProjectionMap.put(Calls.CACHED_MATCHED_NUMBER, Calls.CACHED_MATCHED_NUMBER)226 sCallsProjectionMap.put(Calls.CACHED_MATCHED_NUMBER, Calls.CACHED_MATCHED_NUMBER); sCallsProjectionMap.put(Calls.CACHED_NORMALIZED_NUMBER, Calls.CACHED_NORMALIZED_NUMBER)227 sCallsProjectionMap.put(Calls.CACHED_NORMALIZED_NUMBER, Calls.CACHED_NORMALIZED_NUMBER); sCallsProjectionMap.put(Calls.CACHED_PHOTO_ID, Calls.CACHED_PHOTO_ID)228 sCallsProjectionMap.put(Calls.CACHED_PHOTO_ID, Calls.CACHED_PHOTO_ID); sCallsProjectionMap.put(Calls.CACHED_PHOTO_URI, Calls.CACHED_PHOTO_URI)229 sCallsProjectionMap.put(Calls.CACHED_PHOTO_URI, Calls.CACHED_PHOTO_URI); sCallsProjectionMap.put(Calls.CACHED_FORMATTED_NUMBER, Calls.CACHED_FORMATTED_NUMBER)230 sCallsProjectionMap.put(Calls.CACHED_FORMATTED_NUMBER, Calls.CACHED_FORMATTED_NUMBER); sCallsProjectionMap.put(Calls.ADD_FOR_ALL_USERS, Calls.ADD_FOR_ALL_USERS)231 sCallsProjectionMap.put(Calls.ADD_FOR_ALL_USERS, Calls.ADD_FOR_ALL_USERS); sCallsProjectionMap.put(Calls.LAST_MODIFIED, Calls.LAST_MODIFIED)232 sCallsProjectionMap.put(Calls.LAST_MODIFIED, Calls.LAST_MODIFIED); 233 sCallsProjectionMap put(Calls.CALL_SCREENING_COMPONENT_NAME, Calls.CALL_SCREENING_COMPONENT_NAME)234 .put(Calls.CALL_SCREENING_COMPONENT_NAME, Calls.CALL_SCREENING_COMPONENT_NAME); sCallsProjectionMap.put(Calls.CALL_SCREENING_APP_NAME, Calls.CALL_SCREENING_APP_NAME)235 sCallsProjectionMap.put(Calls.CALL_SCREENING_APP_NAME, Calls.CALL_SCREENING_APP_NAME); sCallsProjectionMap.put(Calls.BLOCK_REASON, Calls.BLOCK_REASON)236 sCallsProjectionMap.put(Calls.BLOCK_REASON, Calls.BLOCK_REASON); sCallsProjectionMap.put(Calls.MISSED_REASON, Calls.MISSED_REASON)237 sCallsProjectionMap.put(Calls.MISSED_REASON, Calls.MISSED_REASON); sCallsProjectionMap.put(Calls.PRIORITY, Calls.PRIORITY)238 sCallsProjectionMap.put(Calls.PRIORITY, Calls.PRIORITY); sCallsProjectionMap.put(Calls.COMPOSER_PHOTO_URI, Calls.COMPOSER_PHOTO_URI)239 sCallsProjectionMap.put(Calls.COMPOSER_PHOTO_URI, Calls.COMPOSER_PHOTO_URI); sCallsProjectionMap.put(Calls.SUBJECT, Calls.SUBJECT)240 sCallsProjectionMap.put(Calls.SUBJECT, Calls.SUBJECT); sCallsProjectionMap.put(Calls.LOCATION, Calls.LOCATION)241 sCallsProjectionMap.put(Calls.LOCATION, Calls.LOCATION); 242 } 243 244 private static final String ALLOWED_PACKAGE_FOR_TESTING = "com.android.providers.contacts"; 245 246 @VisibleForTesting 247 static final String PARAM_KEY_QUERY_FOR_TESTING = "query_for_testing"; 248 249 /** 250 * A long to override the clock used for timestamps, or "null" to reset to the system clock. 251 */ 252 @VisibleForTesting 253 static final String PARAM_KEY_SET_TIME_FOR_TESTING = "set_time_for_testing"; 254 255 private static Long sTimeForTestMillis; 256 257 private ContactsTaskScheduler mTaskScheduler; 258 259 private volatile CountDownLatch mReadAccessLatch; 260 261 private CallLogDatabaseHelper mDbHelper; 262 private DatabaseUtils.InsertHelper mCallsInserter; 263 private boolean mUseStrictPhoneNumberComparation; 264 private int mMinMatch; 265 private VoicemailPermissions mVoicemailPermissions; 266 private CallLogInsertionHelper mCallLogInsertionHelper; 267 268 private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<>(); 269 private final ThreadLocal<Integer> mCallingUid = new ThreadLocal<>(); 270 private final ProviderAccessStats mStats = new ProviderAccessStats(); 271 isShadow()272 protected boolean isShadow() { 273 return false; 274 } 275 getProviderName()276 protected final String getProviderName() { 277 return this.getClass().getSimpleName(); 278 } 279 280 @Override onCreate()281 public boolean onCreate() { 282 if (VERBOSE_LOGGING) { 283 Log.v(TAG, "onCreate: " + this.getClass().getSimpleName() 284 + " user=" + android.os.Process.myUserHandle().getIdentifier()); 285 } 286 287 setAppOps(AppOpsManager.OP_READ_CALL_LOG, AppOpsManager.OP_WRITE_CALL_LOG); 288 if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) { 289 Log.d(Constants.PERFORMANCE_TAG, getProviderName() + ".onCreate start"); 290 } 291 final Context context = getContext(); 292 mDbHelper = getDatabaseHelper(context); 293 mUseStrictPhoneNumberComparation = 294 context.getResources().getBoolean( 295 com.android.internal.R.bool.config_use_strict_phone_number_comparation); 296 mMinMatch = 297 context.getResources().getInteger( 298 com.android.internal.R.integer.config_phonenumber_compare_min_match); 299 mVoicemailPermissions = new VoicemailPermissions(context); 300 mCallLogInsertionHelper = createCallLogInsertionHelper(context); 301 302 mReadAccessLatch = new CountDownLatch(1); 303 304 mTaskScheduler = new ContactsTaskScheduler(getClass().getSimpleName()) { 305 @Override 306 public void onPerformTask(int taskId, Object arg) { 307 performBackgroundTask(taskId, arg); 308 } 309 }; 310 311 mTaskScheduler.scheduleTask(BACKGROUND_TASK_INITIALIZE, null); 312 313 if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) { 314 Log.d(Constants.PERFORMANCE_TAG, getProviderName() + ".onCreate finish"); 315 } 316 return true; 317 } 318 319 @VisibleForTesting createCallLogInsertionHelper(final Context context)320 protected CallLogInsertionHelper createCallLogInsertionHelper(final Context context) { 321 return DefaultCallLogInsertionHelper.getInstance(context); 322 } 323 324 @VisibleForTesting setMinMatchForTest(int minMatch)325 public void setMinMatchForTest(int minMatch) { 326 mMinMatch = minMatch; 327 } 328 329 @VisibleForTesting getMinMatchForTest()330 public int getMinMatchForTest() { 331 return mMinMatch; 332 } 333 getDatabaseHelper(final Context context)334 protected CallLogDatabaseHelper getDatabaseHelper(final Context context) { 335 return CallLogDatabaseHelper.getInstance(context); 336 } 337 applyingBatch()338 protected boolean applyingBatch() { 339 final Boolean applying = mApplyingBatch.get(); 340 return applying != null && applying; 341 } 342 343 @Override applyBatch(ArrayList<ContentProviderOperation> operations)344 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 345 throws OperationApplicationException { 346 final int callingUid = Binder.getCallingUid(); 347 mCallingUid.set(callingUid); 348 349 mStats.incrementBatchStats(callingUid); 350 mApplyingBatch.set(true); 351 try { 352 return super.applyBatch(operations); 353 } finally { 354 mApplyingBatch.set(false); 355 mStats.finishOperation(callingUid); 356 } 357 } 358 359 @Override bulkInsert(Uri uri, ContentValues[] values)360 public int bulkInsert(Uri uri, ContentValues[] values) { 361 final int callingUid = Binder.getCallingUid(); 362 mCallingUid.set(callingUid); 363 364 mStats.incrementBatchStats(callingUid); 365 mApplyingBatch.set(true); 366 try { 367 return super.bulkInsert(uri, values); 368 } finally { 369 mApplyingBatch.set(false); 370 mStats.finishOperation(callingUid); 371 } 372 } 373 374 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)375 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 376 String sortOrder) { 377 // Note don't use mCallingUid here. That's only used by mutation functions. 378 final int callingUid = Binder.getCallingUid(); 379 380 mStats.incrementQueryStats(callingUid); 381 try { 382 return queryInternal(uri, projection, selection, selectionArgs, sortOrder); 383 } finally { 384 mStats.finishOperation(callingUid); 385 } 386 } 387 queryInternal(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)388 private Cursor queryInternal(Uri uri, String[] projection, String selection, 389 String[] selectionArgs, String sortOrder) { 390 if (VERBOSE_LOGGING) { 391 Log.v(TAG, "query: uri=" + uri + " projection=" + Arrays.toString(projection) + 392 " selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) + 393 " order=[" + sortOrder + "] CPID=" + Binder.getCallingPid() + 394 " CUID=" + Binder.getCallingUid() + 395 " User=" + UserUtils.getCurrentUserHandle(getContext())); 396 } 397 398 queryForTesting(uri); 399 400 waitForAccess(mReadAccessLatch); 401 final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 402 qb.setTables(Tables.CALLS); 403 qb.setProjectionMap(sCallsProjectionMap); 404 qb.setStrict(true); 405 // If the caller doesn't have READ_VOICEMAIL, make sure they can't 406 // do any SQL shenanigans to get access to the voicemails. If the caller does have the 407 // READ_VOICEMAIL permission, then they have sufficient permissions to access any data in 408 // the database, so the strict check is unnecessary. 409 if (!mVoicemailPermissions.callerHasReadAccess(getCallingPackage())) { 410 qb.setStrictGrammar(true); 411 } 412 413 final SelectionBuilder selectionBuilder = new SelectionBuilder(selection); 414 checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, true /*isQuery*/); 415 selectionBuilder.addClause(EXCLUDE_HIDDEN_SELECTION); 416 417 final int match = sURIMatcher.match(uri); 418 switch (match) { 419 case CALLS: 420 break; 421 422 case CALLS_ID: { 423 selectionBuilder.addClause(getEqualityClause(Calls._ID, 424 parseCallIdFromUri(uri))); 425 break; 426 } 427 428 case CALLS_FILTER: { 429 List<String> pathSegments = uri.getPathSegments(); 430 String phoneNumber = pathSegments.size() >= 2 ? pathSegments.get(2) : null; 431 if (!TextUtils.isEmpty(phoneNumber)) { 432 qb.appendWhere("PHONE_NUMBERS_EQUAL(number, "); 433 qb.appendWhereEscapeString(phoneNumber); 434 qb.appendWhere(mUseStrictPhoneNumberComparation ? ", 1)" 435 : ", 0, " + mMinMatch + ")"); 436 } else { 437 qb.appendWhere(Calls.NUMBER_PRESENTATION + "!=" 438 + Calls.PRESENTATION_ALLOWED); 439 } 440 break; 441 } 442 443 default: 444 throw new IllegalArgumentException("Unknown URL " + uri); 445 } 446 447 final int limit = getIntParam(uri, Calls.LIMIT_PARAM_KEY, 0); 448 final int offset = getIntParam(uri, Calls.OFFSET_PARAM_KEY, 0); 449 String limitClause = null; 450 if (limit > 0) { 451 limitClause = offset + "," + limit; 452 } 453 454 final SQLiteDatabase db = mDbHelper.getReadableDatabase(); 455 final Cursor c = qb.query(db, projection, selectionBuilder.build(), selectionArgs, null, 456 null, sortOrder, limitClause); 457 if (c != null) { 458 c.setNotificationUri(getContext().getContentResolver(), CallLog.CONTENT_URI); 459 } 460 return c; 461 } 462 queryForTesting(Uri uri)463 private void queryForTesting(Uri uri) { 464 if (!uri.getBooleanQueryParameter(PARAM_KEY_QUERY_FOR_TESTING, false)) { 465 return; 466 } 467 if (!getCallingPackage().equals(ALLOWED_PACKAGE_FOR_TESTING)) { 468 throw new IllegalArgumentException("query_for_testing set from foreign package " 469 + getCallingPackage()); 470 } 471 472 String timeString = uri.getQueryParameter(PARAM_KEY_SET_TIME_FOR_TESTING); 473 if (timeString != null) { 474 if (timeString.equals("null")) { 475 sTimeForTestMillis = null; 476 } else { 477 sTimeForTestMillis = Long.parseLong(timeString); 478 } 479 } 480 } 481 482 @VisibleForTesting getTimeForTestMillis()483 static Long getTimeForTestMillis() { 484 return sTimeForTestMillis; 485 } 486 487 /** 488 * Gets an integer query parameter from a given uri. 489 * 490 * @param uri The uri to extract the query parameter from. 491 * @param key The query parameter key. 492 * @param defaultValue A default value to return if the query parameter does not exist. 493 * @return The value from the query parameter in the Uri. Or the default value if the parameter 494 * does not exist in the uri. 495 * @throws IllegalArgumentException when the value in the query parameter is not an integer. 496 */ getIntParam(Uri uri, String key, int defaultValue)497 private int getIntParam(Uri uri, String key, int defaultValue) { 498 String valueString = uri.getQueryParameter(key); 499 if (valueString == null) { 500 return defaultValue; 501 } 502 503 try { 504 return Integer.parseInt(valueString); 505 } catch (NumberFormatException e) { 506 String msg = "Integer required for " + key + " parameter but value '" + valueString + 507 "' was found instead."; 508 throw new IllegalArgumentException(msg, e); 509 } 510 } 511 512 @Override getType(Uri uri)513 public String getType(Uri uri) { 514 int match = sURIMatcher.match(uri); 515 switch (match) { 516 case CALLS: 517 return Calls.CONTENT_TYPE; 518 case CALLS_ID: 519 return Calls.CONTENT_ITEM_TYPE; 520 case CALLS_FILTER: 521 return Calls.CONTENT_TYPE; 522 case CALL_COMPOSER_NEW_PICTURE: 523 return null; // No type for newly created files 524 case CALL_COMPOSER_PICTURE: 525 // We don't know the exact image format, so this is as specific as we can be. 526 return "application/octet-stream"; 527 default: 528 throw new IllegalArgumentException("Unknown URI: " + uri); 529 } 530 } 531 532 @Override insert(Uri uri, ContentValues values)533 public Uri insert(Uri uri, ContentValues values) { 534 final int callingUid = 535 applyingBatch() ? mCallingUid.get() : Binder.getCallingUid(); 536 537 mStats.incrementInsertStats(callingUid, applyingBatch()); 538 try { 539 return insertInternal(uri, values); 540 } finally { 541 mStats.finishOperation(callingUid); 542 } 543 } 544 545 @Override update(Uri uri, ContentValues values, String selection, String[] selectionArgs)546 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 547 final int callingUid = 548 applyingBatch() ? mCallingUid.get() : Binder.getCallingUid(); 549 550 mStats.incrementInsertStats(callingUid, applyingBatch()); 551 try { 552 return updateInternal(uri, values, selection, selectionArgs); 553 } finally { 554 mStats.finishOperation(callingUid); 555 } 556 } 557 558 @Override delete(Uri uri, String selection, String[] selectionArgs)559 public int delete(Uri uri, String selection, String[] selectionArgs) { 560 final int callingUid = 561 applyingBatch() ? mCallingUid.get() : Binder.getCallingUid(); 562 563 mStats.incrementInsertStats(callingUid, applyingBatch()); 564 try { 565 return deleteInternal(uri, selection, selectionArgs); 566 } finally { 567 mStats.finishOperation(callingUid); 568 } 569 } 570 insertInternal(Uri uri, ContentValues values)571 private Uri insertInternal(Uri uri, ContentValues values) { 572 if (VERBOSE_LOGGING) { 573 Log.v(TAG, "insert: uri=" + uri + " values=[" + values + "]" + 574 " CPID=" + Binder.getCallingPid() + 575 " CUID=" + Binder.getCallingUid()); 576 } 577 waitForAccess(mReadAccessLatch); 578 int match = sURIMatcher.match(uri); 579 switch (match) { 580 case CALL_COMPOSER_PICTURE: { 581 String fileName = uri.getLastPathSegment(); 582 try { 583 return allocateNewCallComposerPicture(values, 584 CallLog.SHADOW_AUTHORITY.equals(uri.getAuthority()), 585 fileName); 586 } catch (IOException e) { 587 throw new ParcelableException(e); 588 } 589 } 590 case CALL_COMPOSER_NEW_PICTURE: { 591 try { 592 return allocateNewCallComposerPicture(values, 593 CallLog.SHADOW_AUTHORITY.equals(uri.getAuthority())); 594 } catch (IOException e) { 595 throw new ParcelableException(e); 596 } 597 } 598 default: 599 // Fall through and execute the rest of the method for ordinary call log insertions. 600 } 601 602 checkForSupportedColumns(sCallsProjectionMap, values); 603 // Inserting a voicemail record through call_log requires the voicemail 604 // permission and also requires the additional voicemail param set. 605 if (hasVoicemailValue(values)) { 606 checkIsAllowVoicemailRequest(uri); 607 mVoicemailPermissions.checkCallerHasWriteAccess(getCallingPackage()); 608 } 609 if (mCallsInserter == null) { 610 SQLiteDatabase db = mDbHelper.getWritableDatabase(); 611 mCallsInserter = new DatabaseUtils.InsertHelper(db, Tables.CALLS); 612 } 613 614 ContentValues copiedValues = new ContentValues(values); 615 616 // Add the computed fields to the copied values. 617 mCallLogInsertionHelper.addComputedValues(copiedValues); 618 619 long rowId = createDatabaseModifier(mCallsInserter).insert(copiedValues); 620 if (rowId > 0) { 621 return ContentUris.withAppendedId(uri, rowId); 622 } 623 return null; 624 } 625 626 @Override openFile(@onNull Uri uri, @NonNull String mode)627 public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) 628 throws FileNotFoundException { 629 int match = sURIMatcher.match(uri); 630 if (match != CALL_COMPOSER_PICTURE) { 631 throw new UnsupportedOperationException("The call log provider only supports opening" 632 + " call composer pictures."); 633 } 634 int modeInt; 635 switch (mode) { 636 case "r": 637 modeInt = ParcelFileDescriptor.MODE_READ_ONLY; 638 break; 639 case "w": 640 modeInt = ParcelFileDescriptor.MODE_WRITE_ONLY; 641 break; 642 default: 643 throw new UnsupportedOperationException("The call log does not support opening" 644 + " a call composer picture with mode " + mode); 645 } 646 647 try { 648 Path callComposerDir = getCallComposerPictureDirectory(getContext(), uri); 649 Path pictureFile = callComposerDir.resolve(uri.getLastPathSegment()); 650 if (Files.notExists(pictureFile)) { 651 throw new FileNotFoundException(uri.toString() 652 + " does not correspond to a valid file."); 653 } 654 return ParcelFileDescriptor.open(pictureFile.toFile(), modeInt); 655 } catch (IOException e) { 656 Log.e(TAG, "IOException while opening call composer file: " + e); 657 throw new RuntimeException(e); 658 } 659 } 660 661 @Override call(@onNull String method, @Nullable String arg, @Nullable Bundle extras)662 public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) { 663 Log.i(TAG, "Fetching list of Uris to sync"); 664 if (!UserHandle.isSameApp(android.os.Process.myUid(), Binder.getCallingUid())) { 665 throw new SecurityException("call() functionality reserved" 666 + " for internal use by the call log."); 667 } 668 if (!GET_CALL_COMPOSER_IMAGE_URIS.equals(method)) { 669 throw new UnsupportedOperationException("Invalid method passed to call(): " + method); 670 } 671 if (!extras.containsKey(EXTRA_SINCE_DATE)) { 672 throw new IllegalArgumentException("SINCE_DATE required"); 673 } 674 if (!extras.containsKey(EXTRA_IS_SHADOW)) { 675 throw new IllegalArgumentException("IS_SHADOW required"); 676 } 677 if (!extras.containsKey(EXTRA_ALL_USERS_ONLY)) { 678 throw new IllegalArgumentException("ALL_USERS_ONLY required"); 679 } 680 boolean isShadow = extras.getBoolean(EXTRA_IS_SHADOW); 681 boolean allUsers = extras.getBoolean(EXTRA_ALL_USERS_ONLY); 682 long sinceDate = extras.getLong(EXTRA_SINCE_DATE); 683 684 try { 685 Path queryDir = allUsers 686 ? getCallComposerAllUsersPictureDirectory(getContext(), isShadow) 687 : getCallComposerPictureDirectory(getContext(), isShadow); 688 List<Path> newestPics = new ArrayList<>(); 689 try (DirectoryStream<Path> dirStream = 690 Files.newDirectoryStream(queryDir, entry -> { 691 if (Files.isDirectory(entry)) { 692 return false; 693 } 694 FileTime createdAt = 695 (FileTime) Files.getAttribute(entry, "creationTime"); 696 return createdAt.toMillis() > sinceDate; 697 })) { 698 dirStream.forEach(newestPics::add); 699 } 700 List<Uri> fileUris = newestPics.stream().map((path) -> { 701 String fileName = path.getFileName().toString(); 702 // We don't need to worry about if it's for all users -- anything that's for 703 // all users is also stored in the regular location. 704 Uri base = isShadow ? CallLog.SHADOW_CALL_COMPOSER_PICTURE_URI 705 : CallLog.CALL_COMPOSER_PICTURE_URI; 706 return base.buildUpon().appendPath(fileName).build(); 707 }).collect(Collectors.toList()); 708 Bundle result = new Bundle(); 709 result.putParcelableList(EXTRA_RESULT_URIS, fileUris); 710 Log.i(TAG, "Will sync following Uris:" + fileUris); 711 return result; 712 } catch (IOException e) { 713 Log.e(TAG, "IOException while trying to fetch URI list: " + e); 714 return null; 715 } 716 } 717 getCallComposerPictureDirectory(Context context, Uri uri)718 private static @NonNull Path getCallComposerPictureDirectory(Context context, Uri uri) 719 throws IOException { 720 boolean isShadow = CallLog.SHADOW_AUTHORITY.equals(uri.getAuthority()); 721 return getCallComposerPictureDirectory(context, isShadow); 722 } 723 getCallComposerPictureDirectory(Context context, boolean isShadow)724 private static @NonNull Path getCallComposerPictureDirectory(Context context, boolean isShadow) 725 throws IOException { 726 if (isShadow) { 727 context = context.createDeviceProtectedStorageContext(); 728 } 729 Path path = context.getFilesDir().toPath().resolve(CALL_COMPOSER_PICTURE_DIRECTORY_NAME); 730 if (!Files.isDirectory(path)) { 731 Files.createDirectory(path); 732 } 733 return path; 734 } 735 getCallComposerAllUsersPictureDirectory( Context context, boolean isShadow)736 private static @NonNull Path getCallComposerAllUsersPictureDirectory( 737 Context context, boolean isShadow) throws IOException { 738 Path pathToCallComposerDir = getCallComposerPictureDirectory(context, isShadow); 739 Path path = pathToCallComposerDir.resolve(CALL_COMPOSER_ALL_USERS_DIRECTORY_NAME); 740 if (!Files.isDirectory(path)) { 741 Files.createDirectory(path); 742 } 743 return path; 744 } 745 allocateNewCallComposerPicture(ContentValues values, boolean isShadow)746 private Uri allocateNewCallComposerPicture(ContentValues values, boolean isShadow) 747 throws IOException { 748 return allocateNewCallComposerPicture(values, isShadow, UUID.randomUUID().toString()); 749 } 750 allocateNewCallComposerPicture(ContentValues values, boolean isShadow, String fileName)751 private Uri allocateNewCallComposerPicture(ContentValues values, 752 boolean isShadow, String fileName) throws IOException { 753 Uri baseUri = isShadow ? 754 CallLog.CALL_COMPOSER_PICTURE_URI.buildUpon() 755 .authority(CallLog.SHADOW_AUTHORITY).build() 756 : CallLog.CALL_COMPOSER_PICTURE_URI; 757 758 boolean forAllUsers = values.containsKey(Calls.ADD_FOR_ALL_USERS) 759 && (values.getAsInteger(Calls.ADD_FOR_ALL_USERS) == 1); 760 Path pathToCallComposerDir = getCallComposerPictureDirectory(getContext(), isShadow); 761 762 if (new StatFs(pathToCallComposerDir.toString()).getAvailableBytes() 763 < TelephonyManager.getMaximumCallComposerPictureSize()) { 764 return null; 765 } 766 Path pathToFile = pathToCallComposerDir.resolve(fileName); 767 Files.createFile(pathToFile); 768 769 if (forAllUsers) { 770 // Create a symlink in a subdirectory for copying later. 771 Path allUsersDir = getCallComposerAllUsersPictureDirectory(getContext(), isShadow); 772 Files.createSymbolicLink(allUsersDir.resolve(fileName), pathToFile); 773 } 774 return baseUri.buildUpon().appendPath(fileName).build(); 775 } 776 deleteCallComposerPicture(Uri uri)777 private int deleteCallComposerPicture(Uri uri) { 778 try { 779 Path pathToCallComposerDir = getCallComposerPictureDirectory(getContext(), uri); 780 String fileName = uri.getLastPathSegment(); 781 boolean successfulDelete = 782 Files.deleteIfExists(pathToCallComposerDir.resolve(fileName)); 783 return successfulDelete ? 1 : 0; 784 } catch (IOException e) { 785 Log.e(TAG, "IOException encountered deleting the call composer pics dir " + e); 786 return 0; 787 } 788 } 789 updateInternal(Uri uri, ContentValues values, String selection, String[] selectionArgs)790 private int updateInternal(Uri uri, ContentValues values, 791 String selection, String[] selectionArgs) { 792 if (VERBOSE_LOGGING) { 793 Log.v(TAG, "update: uri=" + uri + 794 " selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) + 795 " values=[" + values + "] CPID=" + Binder.getCallingPid() + 796 " CUID=" + Binder.getCallingUid() + 797 " User=" + UserUtils.getCurrentUserHandle(getContext())); 798 } 799 waitForAccess(mReadAccessLatch); 800 checkForSupportedColumns(sCallsProjectionMap, values); 801 // Request that involves changing record type to voicemail requires the 802 // voicemail param set in the uri. 803 if (hasVoicemailValue(values)) { 804 checkIsAllowVoicemailRequest(uri); 805 } 806 807 SelectionBuilder selectionBuilder = new SelectionBuilder(selection); 808 checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, false /*isQuery*/); 809 boolean hasReadVoicemailPermission = mVoicemailPermissions.callerHasReadAccess( 810 getCallingPackage()); 811 final SQLiteDatabase db = mDbHelper.getWritableDatabase(); 812 final int matchedUriId = sURIMatcher.match(uri); 813 switch (matchedUriId) { 814 case CALLS: 815 break; 816 817 case CALLS_ID: 818 selectionBuilder.addClause(getEqualityClause(Calls._ID, parseCallIdFromUri(uri))); 819 break; 820 821 default: 822 throw new UnsupportedOperationException("Cannot update URL: " + uri); 823 } 824 825 return createDatabaseModifier(db, hasReadVoicemailPermission).update(uri, Tables.CALLS, 826 values, selectionBuilder.build(), selectionArgs); 827 } 828 deleteInternal(Uri uri, String selection, String[] selectionArgs)829 private int deleteInternal(Uri uri, String selection, String[] selectionArgs) { 830 if (VERBOSE_LOGGING) { 831 Log.v(TAG, "delete: uri=" + uri + 832 " selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) + 833 " CPID=" + Binder.getCallingPid() + 834 " CUID=" + Binder.getCallingUid() + 835 " User=" + UserUtils.getCurrentUserHandle(getContext())); 836 } 837 waitForAccess(mReadAccessLatch); 838 SelectionBuilder selectionBuilder = new SelectionBuilder(selection); 839 checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, false /*isQuery*/); 840 841 boolean hasReadVoicemailPermission = 842 mVoicemailPermissions.callerHasReadAccess(getCallingPackage()); 843 final SQLiteDatabase db = mDbHelper.getWritableDatabase(); 844 final int matchedUriId = sURIMatcher.match(uri); 845 switch (matchedUriId) { 846 case CALLS: 847 return createDatabaseModifier(db, hasReadVoicemailPermission).delete(Tables.CALLS, 848 selectionBuilder.build(), selectionArgs); 849 case CALL_COMPOSER_PICTURE: 850 // TODO(hallliu): implement deletion of file when the corresponding calllog entry 851 // gets deleted as well. 852 return deleteCallComposerPicture(uri); 853 default: 854 throw new UnsupportedOperationException("Cannot delete that URL: " + uri); 855 } 856 } 857 adjustForNewPhoneAccount(PhoneAccountHandle handle)858 void adjustForNewPhoneAccount(PhoneAccountHandle handle) { 859 mTaskScheduler.scheduleTask(BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT, handle); 860 } 861 862 /** 863 * Returns a {@link DatabaseModifier} that takes care of sending necessary notifications 864 * after the operation is performed. 865 */ createDatabaseModifier(SQLiteDatabase db, boolean hasReadVoicemail)866 private DatabaseModifier createDatabaseModifier(SQLiteDatabase db, boolean hasReadVoicemail) { 867 return new DbModifierWithNotification(Tables.CALLS, db, null, hasReadVoicemail, 868 getContext()); 869 } 870 871 /** 872 * Same as {@link #createDatabaseModifier(SQLiteDatabase)} but used for insert helper operations 873 * only. 874 */ createDatabaseModifier(DatabaseUtils.InsertHelper insertHelper)875 private DatabaseModifier createDatabaseModifier(DatabaseUtils.InsertHelper insertHelper) { 876 return new DbModifierWithNotification(Tables.CALLS, insertHelper, getContext()); 877 } 878 879 private static final Integer VOICEMAIL_TYPE = new Integer(Calls.VOICEMAIL_TYPE); hasVoicemailValue(ContentValues values)880 private boolean hasVoicemailValue(ContentValues values) { 881 return VOICEMAIL_TYPE.equals(values.getAsInteger(Calls.TYPE)); 882 } 883 884 /** 885 * Checks if the supplied uri requests to include voicemails and take appropriate 886 * action. 887 * <p> If voicemail is requested, then check for voicemail permissions. Otherwise 888 * modify the selection to restrict to non-voicemail entries only. 889 */ checkVoicemailPermissionAndAddRestriction(Uri uri, SelectionBuilder selectionBuilder, boolean isQuery)890 private void checkVoicemailPermissionAndAddRestriction(Uri uri, 891 SelectionBuilder selectionBuilder, boolean isQuery) { 892 if (isAllowVoicemailRequest(uri)) { 893 if (isQuery) { 894 mVoicemailPermissions.checkCallerHasReadAccess(getCallingPackage()); 895 } else { 896 mVoicemailPermissions.checkCallerHasWriteAccess(getCallingPackage()); 897 } 898 } else { 899 selectionBuilder.addClause(EXCLUDE_VOICEMAIL_SELECTION); 900 } 901 } 902 903 /** 904 * Determines if the supplied uri has the request to allow voicemails to be 905 * included. 906 */ isAllowVoicemailRequest(Uri uri)907 private boolean isAllowVoicemailRequest(Uri uri) { 908 return uri.getBooleanQueryParameter(Calls.ALLOW_VOICEMAILS_PARAM_KEY, false); 909 } 910 911 /** 912 * Checks to ensure that the given uri has allow_voicemail set. Used by 913 * insert and update operations to check that ContentValues with voicemail 914 * call type must use the voicemail uri. 915 * @throws IllegalArgumentException if allow_voicemail is not set. 916 */ checkIsAllowVoicemailRequest(Uri uri)917 private void checkIsAllowVoicemailRequest(Uri uri) { 918 if (!isAllowVoicemailRequest(uri)) { 919 throw new IllegalArgumentException( 920 String.format("Uri %s cannot be used for voicemail record." + 921 " Please set '%s=true' in the uri.", uri, 922 Calls.ALLOW_VOICEMAILS_PARAM_KEY)); 923 } 924 } 925 926 /** 927 * Parses the call Id from the given uri, assuming that this is a uri that 928 * matches CALLS_ID. For other uri types the behaviour is undefined. 929 * @throws IllegalArgumentException if the id included in the Uri is not a valid long value. 930 */ parseCallIdFromUri(Uri uri)931 private long parseCallIdFromUri(Uri uri) { 932 try { 933 return Long.parseLong(uri.getPathSegments().get(1)); 934 } catch (NumberFormatException e) { 935 throw new IllegalArgumentException("Invalid call id in uri: " + uri, e); 936 } 937 } 938 939 /** 940 * Sync all calllog entries that were inserted 941 */ syncEntries()942 private void syncEntries() { 943 if (isShadow()) { 944 return; // It's the shadow provider itself. No copying. 945 } 946 947 final UserManager userManager = UserUtils.getUserManager(getContext()); 948 949 // TODO: http://b/24944959 950 if (!Calls.shouldHaveSharedCallLogEntries(getContext(), userManager, 951 userManager.getUserHandle())) { 952 return; 953 } 954 955 final int myUserId = userManager.getUserHandle(); 956 957 // See the comment in Calls.addCall() for the logic. 958 959 if (userManager.isSystemUser()) { 960 // If it's the system user, just copy from shadow. 961 syncEntriesFrom(UserHandle.USER_SYSTEM, /* sourceIsShadow = */ true, 962 /* forAllUsersOnly =*/ false); 963 } else { 964 // Otherwise, copy from system's real provider, as well as self's shadow. 965 syncEntriesFrom(UserHandle.USER_SYSTEM, /* sourceIsShadow = */ false, 966 /* forAllUsersOnly =*/ true); 967 syncEntriesFrom(myUserId, /* sourceIsShadow = */ true, 968 /* forAllUsersOnly =*/ false); 969 } 970 } 971 syncEntriesFrom(int sourceUserId, boolean sourceIsShadow, boolean forAllUsersOnly)972 private void syncEntriesFrom(int sourceUserId, boolean sourceIsShadow, 973 boolean forAllUsersOnly) { 974 975 final Uri sourceUri = sourceIsShadow ? Calls.SHADOW_CONTENT_URI : Calls.CONTENT_URI; 976 977 final long lastSyncTime = getLastSyncTime(sourceIsShadow); 978 979 final Uri uri = ContentProvider.maybeAddUserId(sourceUri, sourceUserId); 980 final long newestTimeStamp; 981 final ContentResolver cr = getContext().getContentResolver(); 982 983 final StringBuilder selection = new StringBuilder(); 984 985 selection.append( 986 "(" + EXCLUDE_VOICEMAIL_SELECTION + ") AND (" + MORE_RECENT_THAN_SELECTION + ")"); 987 988 if (forAllUsersOnly) { 989 selection.append(" AND (" + Calls.ADD_FOR_ALL_USERS + "=1)"); 990 } 991 992 final Cursor cursor = cr.query( 993 uri, 994 CALL_LOG_SYNC_PROJECTION, 995 selection.toString(), 996 new String[] {String.valueOf(lastSyncTime)}, 997 Calls.DATE + " ASC"); 998 if (cursor == null) { 999 return; 1000 } 1001 try { 1002 newestTimeStamp = copyEntriesFromCursor(cursor, lastSyncTime, sourceIsShadow); 1003 } finally { 1004 cursor.close(); 1005 } 1006 if (sourceIsShadow) { 1007 // delete all entries in shadow. 1008 cr.delete(uri, Calls.DATE + "<= ?", new String[] {String.valueOf(newestTimeStamp)}); 1009 } 1010 1011 try { 1012 syncCallComposerPics(sourceUserId, sourceIsShadow, forAllUsersOnly, lastSyncTime); 1013 } catch (Exception e) { 1014 // Catch any exceptions to make sure we don't bring down the entire process if something 1015 // goes wrong 1016 StringWriter w = new StringWriter(); 1017 PrintWriter pw = new PrintWriter(w); 1018 e.printStackTrace(pw); 1019 Log.e(TAG, "Caught exception syncing call composer pics: " + e 1020 + "\n" + pw.toString()); 1021 } 1022 } 1023 syncCallComposerPics(int sourceUserId, boolean sourceIsShadow, boolean forAllUsersOnly, long lastSyncTime)1024 private void syncCallComposerPics(int sourceUserId, boolean sourceIsShadow, 1025 boolean forAllUsersOnly, long lastSyncTime) { 1026 Log.i(TAG, "Syncing call composer pics -- source user=" + sourceUserId + "," 1027 + " isShadow=" + sourceIsShadow + ", forAllUser=" + forAllUsersOnly); 1028 ContentResolver contentResolver = getContext().getContentResolver(); 1029 Bundle args = new Bundle(); 1030 args.putLong(EXTRA_SINCE_DATE, lastSyncTime); 1031 args.putBoolean(EXTRA_ALL_USERS_ONLY, forAllUsersOnly); 1032 args.putBoolean(EXTRA_IS_SHADOW, sourceIsShadow); 1033 Uri queryUri = ContentProvider.maybeAddUserId( 1034 sourceIsShadow 1035 ? CallLog.SHADOW_CALL_COMPOSER_PICTURE_URI 1036 : CallLog.CALL_COMPOSER_PICTURE_URI, 1037 sourceUserId); 1038 Bundle result = contentResolver.call(queryUri, GET_CALL_COMPOSER_IMAGE_URIS, null, args); 1039 if (result == null || !result.containsKey(EXTRA_RESULT_URIS)) { 1040 Log.e(TAG, "Failed to sync call composer pics -- invalid return from call()"); 1041 return; 1042 } 1043 List<Uri> urisToCopy = result.getParcelableArrayList(EXTRA_RESULT_URIS); 1044 Log.i(TAG, "Syncing call composer pics -- got " + urisToCopy); 1045 for (Uri uri : urisToCopy) { 1046 try { 1047 Uri uriWithUser = ContentProvider.maybeAddUserId(uri, sourceUserId); 1048 Path newFilePath = getCallComposerPictureDirectory(getContext(), false) 1049 .resolve(uri.getLastPathSegment()); 1050 try (ParcelFileDescriptor remoteFile = contentResolver.openFile(uriWithUser, 1051 "r", null); 1052 OutputStream localOut = 1053 Files.newOutputStream(newFilePath, StandardOpenOption.CREATE_NEW)) { 1054 FileInputStream input = new FileInputStream(remoteFile.getFileDescriptor()); 1055 byte[] buffer = new byte[1 << 14]; // 16kb 1056 while (true) { 1057 int numRead = input.read(buffer); 1058 if (numRead < 0) { 1059 break; 1060 } 1061 localOut.write(buffer, 0, numRead); 1062 } 1063 } 1064 contentResolver.delete(uriWithUser, null); 1065 } catch (IOException e) { 1066 Log.e(TAG, "IOException while syncing call composer pics: " + e); 1067 // Keep going and get as many as we can. 1068 } 1069 } 1070 1071 } 1072 /** 1073 * Un-hides any hidden call log entries that are associated with the specified handle. 1074 * 1075 * @param handle The handle to the newly registered {@link android.telecom.PhoneAccount}. 1076 */ adjustForNewPhoneAccountInternal(PhoneAccountHandle handle)1077 private void adjustForNewPhoneAccountInternal(PhoneAccountHandle handle) { 1078 String[] handleArgs = 1079 new String[] { handle.getComponentName().flattenToString(), handle.getId() }; 1080 1081 // Check to see if any entries exist for this handle. If so (not empty), run the un-hiding 1082 // update. If not, then try to identify the call from the phone number. 1083 Cursor cursor = query(Calls.CONTENT_URI, MINIMAL_PROJECTION, 1084 Calls.PHONE_ACCOUNT_COMPONENT_NAME + " =? AND " + Calls.PHONE_ACCOUNT_ID + " =?", 1085 handleArgs, null); 1086 1087 if (cursor != null) { 1088 try { 1089 if (cursor.getCount() >= 1) { 1090 // run un-hiding process based on phone account 1091 mDbHelper.getWritableDatabase().execSQL( 1092 UNHIDE_BY_PHONE_ACCOUNT_QUERY, handleArgs); 1093 } else { 1094 TelecomManager tm = getContext().getSystemService(TelecomManager.class); 1095 if (tm != null) { 1096 PhoneAccount account = tm.getPhoneAccount(handle); 1097 if (account != null && account.getAddress() != null) { 1098 // We did not find any items for the specific phone account, so run the 1099 // query based on the phone number instead. 1100 mDbHelper.getWritableDatabase().execSQL(UNHIDE_BY_ADDRESS_QUERY, 1101 new String[] { account.getAddress().toString() }); 1102 } 1103 1104 } 1105 } 1106 } finally { 1107 cursor.close(); 1108 } 1109 } 1110 1111 } 1112 1113 /** 1114 * @param cursor to copy call log entries from 1115 */ 1116 @VisibleForTesting copyEntriesFromCursor(Cursor cursor, long lastSyncTime, boolean forShadow)1117 long copyEntriesFromCursor(Cursor cursor, long lastSyncTime, boolean forShadow) { 1118 long latestTimestamp = 0; 1119 final ContentValues values = new ContentValues(); 1120 final SQLiteDatabase db = mDbHelper.getWritableDatabase(); 1121 db.beginTransaction(); 1122 try { 1123 final String[] args = new String[2]; 1124 cursor.moveToPosition(-1); 1125 while (cursor.moveToNext()) { 1126 values.clear(); 1127 DatabaseUtils.cursorRowToContentValues(cursor, values); 1128 1129 final String startTime = values.getAsString(Calls.DATE); 1130 final String number = values.getAsString(Calls.NUMBER); 1131 1132 if (startTime == null || number == null) { 1133 continue; 1134 } 1135 1136 if (cursor.isLast()) { 1137 try { 1138 latestTimestamp = Long.valueOf(startTime); 1139 } catch (NumberFormatException e) { 1140 Log.e(TAG, "Call log entry does not contain valid start time: " 1141 + startTime); 1142 } 1143 } 1144 1145 // Avoid duplicating an already existing entry (which is uniquely identified by 1146 // the number, and the start time) 1147 args[0] = startTime; 1148 args[1] = number; 1149 if (DatabaseUtils.queryNumEntries(db, Tables.CALLS, 1150 Calls.DATE + " = ? AND " + Calls.NUMBER + " = ?", args) > 0) { 1151 continue; 1152 } 1153 1154 db.insert(Tables.CALLS, null, values); 1155 } 1156 1157 if (latestTimestamp > lastSyncTime) { 1158 setLastTimeSynced(latestTimestamp, forShadow); 1159 } 1160 1161 db.setTransactionSuccessful(); 1162 } finally { 1163 db.endTransaction(); 1164 } 1165 return latestTimestamp; 1166 } 1167 getLastSyncTimePropertyName(boolean forShadow)1168 private static String getLastSyncTimePropertyName(boolean forShadow) { 1169 return forShadow 1170 ? DbProperties.CALL_LOG_LAST_SYNCED_FOR_SHADOW 1171 : DbProperties.CALL_LOG_LAST_SYNCED; 1172 } 1173 1174 @VisibleForTesting getLastSyncTime(boolean forShadow)1175 long getLastSyncTime(boolean forShadow) { 1176 try { 1177 return Long.valueOf(mDbHelper.getProperty(getLastSyncTimePropertyName(forShadow), "0")); 1178 } catch (NumberFormatException e) { 1179 return 0; 1180 } 1181 } 1182 setLastTimeSynced(long time, boolean forShadow)1183 private void setLastTimeSynced(long time, boolean forShadow) { 1184 mDbHelper.setProperty(getLastSyncTimePropertyName(forShadow), String.valueOf(time)); 1185 } 1186 waitForAccess(CountDownLatch latch)1187 private static void waitForAccess(CountDownLatch latch) { 1188 if (latch == null) { 1189 return; 1190 } 1191 1192 while (true) { 1193 try { 1194 latch.await(); 1195 return; 1196 } catch (InterruptedException e) { 1197 Thread.currentThread().interrupt(); 1198 } 1199 } 1200 } 1201 performBackgroundTask(int task, Object arg)1202 private void performBackgroundTask(int task, Object arg) { 1203 if (task == BACKGROUND_TASK_INITIALIZE) { 1204 try { 1205 syncEntries(); 1206 } finally { 1207 mReadAccessLatch.countDown(); 1208 mReadAccessLatch = null; 1209 } 1210 } else if (task == BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT) { 1211 adjustForNewPhoneAccountInternal((PhoneAccountHandle) arg); 1212 } 1213 } 1214 1215 @Override shutdown()1216 public void shutdown() { 1217 mTaskScheduler.shutdownForTest(); 1218 } 1219 1220 @Override dump(FileDescriptor fd, PrintWriter writer, String[] args)1221 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 1222 mStats.dump(writer, " "); 1223 } 1224 } 1225