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