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