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