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