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