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