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.app.AppOpsManager;
24 import android.content.ContentProvider;
25 import android.content.ContentProviderOperation;
26 import android.content.ContentProviderResult;
27 import android.content.ContentResolver;
28 import android.content.ContentUris;
29 import android.content.ContentValues;
30 import android.content.Context;
31 import android.content.OperationApplicationException;
32 import android.content.UriMatcher;
33 import android.database.Cursor;
34 import android.database.DatabaseUtils;
35 import android.database.sqlite.SQLiteDatabase;
36 import android.database.sqlite.SQLiteQueryBuilder;
37 import android.net.Uri;
38 import android.os.Binder;
39 import android.os.UserHandle;
40 import android.os.UserManager;
41 import android.provider.CallLog;
42 import android.provider.CallLog.Calls;
43 import android.telecom.PhoneAccount;
44 import android.telecom.PhoneAccountHandle;
45 import android.telecom.TelecomManager;
46 import android.text.TextUtils;
47 import android.util.ArrayMap;
48 import android.util.Log;
49 
50 import com.android.internal.annotations.VisibleForTesting;
51 import com.android.internal.util.ProviderAccessStats;
52 import com.android.providers.contacts.CallLogDatabaseHelper.DbProperties;
53 import com.android.providers.contacts.CallLogDatabaseHelper.Tables;
54 import com.android.providers.contacts.util.SelectionBuilder;
55 import com.android.providers.contacts.util.UserUtils;
56 
57 import java.io.FileDescriptor;
58 import java.io.PrintWriter;
59 import java.util.ArrayList;
60 import java.util.Arrays;
61 import java.util.List;
62 import java.util.concurrent.CountDownLatch;
63 
64 /**
65  * Call log content provider.
66  */
67 public class CallLogProvider extends ContentProvider {
68     private static final String TAG = "CallLogProvider";
69 
70     public static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
71 
72     private static final int BACKGROUND_TASK_INITIALIZE = 0;
73     private static final int BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT = 1;
74 
75     /** Selection clause for selecting all calls that were made after a certain time */
76     private static final String MORE_RECENT_THAN_SELECTION = Calls.DATE + "> ?";
77     /** Selection clause to use to exclude voicemail records.  */
78     private static final String EXCLUDE_VOICEMAIL_SELECTION = getInequalityClause(
79             Calls.TYPE, Calls.VOICEMAIL_TYPE);
80     /** Selection clause to exclude hidden records. */
81     private static final String EXCLUDE_HIDDEN_SELECTION = getEqualityClause(
82             Calls.PHONE_ACCOUNT_HIDDEN, 0);
83 
84     @VisibleForTesting
85     static final String[] CALL_LOG_SYNC_PROJECTION = new String[] {
86         Calls.NUMBER,
87         Calls.NUMBER_PRESENTATION,
88         Calls.TYPE,
89         Calls.FEATURES,
90         Calls.DATE,
91         Calls.DURATION,
92         Calls.DATA_USAGE,
93         Calls.PHONE_ACCOUNT_COMPONENT_NAME,
94         Calls.PHONE_ACCOUNT_ID,
95         Calls.ADD_FOR_ALL_USERS
96     };
97 
98     static final String[] MINIMAL_PROJECTION = new String[] { Calls._ID };
99 
100     private static final int CALLS = 1;
101 
102     private static final int CALLS_ID = 2;
103 
104     private static final int CALLS_FILTER = 3;
105 
106     private static final String UNHIDE_BY_PHONE_ACCOUNT_QUERY =
107             "UPDATE " + Tables.CALLS + " SET " + Calls.PHONE_ACCOUNT_HIDDEN + "=0 WHERE " +
108             Calls.PHONE_ACCOUNT_COMPONENT_NAME + "=? AND " + Calls.PHONE_ACCOUNT_ID + "=?;";
109 
110     private static final String UNHIDE_BY_ADDRESS_QUERY =
111             "UPDATE " + Tables.CALLS + " SET " + Calls.PHONE_ACCOUNT_HIDDEN + "=0 WHERE " +
112             Calls.PHONE_ACCOUNT_ADDRESS + "=?;";
113 
114     private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
115     static {
sURIMatcher.addURI(CallLog.AUTHORITY, "calls", CALLS)116         sURIMatcher.addURI(CallLog.AUTHORITY, "calls", CALLS);
sURIMatcher.addURI(CallLog.AUTHORITY, "calls/#", CALLS_ID)117         sURIMatcher.addURI(CallLog.AUTHORITY, "calls/#", CALLS_ID);
sURIMatcher.addURI(CallLog.AUTHORITY, "calls/filter/*", CALLS_FILTER)118         sURIMatcher.addURI(CallLog.AUTHORITY, "calls/filter/*", CALLS_FILTER);
119 
120         // Shadow provider only supports "/calls".
sURIMatcher.addURI(CallLog.SHADOW_AUTHORITY, "calls", CALLS)121         sURIMatcher.addURI(CallLog.SHADOW_AUTHORITY, "calls", CALLS);
122     }
123 
124     private static final ArrayMap<String, String> sCallsProjectionMap;
125     static {
126 
127         // Calls projection map
128         sCallsProjectionMap = new ArrayMap<>();
sCallsProjectionMap.put(Calls._ID, Calls._ID)129         sCallsProjectionMap.put(Calls._ID, Calls._ID);
sCallsProjectionMap.put(Calls.NUMBER, Calls.NUMBER)130         sCallsProjectionMap.put(Calls.NUMBER, Calls.NUMBER);
sCallsProjectionMap.put(Calls.POST_DIAL_DIGITS, Calls.POST_DIAL_DIGITS)131         sCallsProjectionMap.put(Calls.POST_DIAL_DIGITS, Calls.POST_DIAL_DIGITS);
sCallsProjectionMap.put(Calls.VIA_NUMBER, Calls.VIA_NUMBER)132         sCallsProjectionMap.put(Calls.VIA_NUMBER, Calls.VIA_NUMBER);
sCallsProjectionMap.put(Calls.NUMBER_PRESENTATION, Calls.NUMBER_PRESENTATION)133         sCallsProjectionMap.put(Calls.NUMBER_PRESENTATION, Calls.NUMBER_PRESENTATION);
sCallsProjectionMap.put(Calls.DATE, Calls.DATE)134         sCallsProjectionMap.put(Calls.DATE, Calls.DATE);
sCallsProjectionMap.put(Calls.DURATION, Calls.DURATION)135         sCallsProjectionMap.put(Calls.DURATION, Calls.DURATION);
sCallsProjectionMap.put(Calls.DATA_USAGE, Calls.DATA_USAGE)136         sCallsProjectionMap.put(Calls.DATA_USAGE, Calls.DATA_USAGE);
sCallsProjectionMap.put(Calls.TYPE, Calls.TYPE)137         sCallsProjectionMap.put(Calls.TYPE, Calls.TYPE);
sCallsProjectionMap.put(Calls.FEATURES, Calls.FEATURES)138         sCallsProjectionMap.put(Calls.FEATURES, Calls.FEATURES);
sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, Calls.PHONE_ACCOUNT_COMPONENT_NAME)139         sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, Calls.PHONE_ACCOUNT_COMPONENT_NAME);
sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_ID, Calls.PHONE_ACCOUNT_ID)140         sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_ID, Calls.PHONE_ACCOUNT_ID);
sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_ADDRESS, Calls.PHONE_ACCOUNT_ADDRESS)141         sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_ADDRESS, Calls.PHONE_ACCOUNT_ADDRESS);
sCallsProjectionMap.put(Calls.NEW, Calls.NEW)142         sCallsProjectionMap.put(Calls.NEW, Calls.NEW);
sCallsProjectionMap.put(Calls.VOICEMAIL_URI, Calls.VOICEMAIL_URI)143         sCallsProjectionMap.put(Calls.VOICEMAIL_URI, Calls.VOICEMAIL_URI);
sCallsProjectionMap.put(Calls.TRANSCRIPTION, Calls.TRANSCRIPTION)144         sCallsProjectionMap.put(Calls.TRANSCRIPTION, Calls.TRANSCRIPTION);
sCallsProjectionMap.put(Calls.TRANSCRIPTION_STATE, Calls.TRANSCRIPTION_STATE)145         sCallsProjectionMap.put(Calls.TRANSCRIPTION_STATE, Calls.TRANSCRIPTION_STATE);
sCallsProjectionMap.put(Calls.IS_READ, Calls.IS_READ)146         sCallsProjectionMap.put(Calls.IS_READ, Calls.IS_READ);
sCallsProjectionMap.put(Calls.CACHED_NAME, Calls.CACHED_NAME)147         sCallsProjectionMap.put(Calls.CACHED_NAME, Calls.CACHED_NAME);
sCallsProjectionMap.put(Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_TYPE)148         sCallsProjectionMap.put(Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_TYPE);
sCallsProjectionMap.put(Calls.CACHED_NUMBER_LABEL, Calls.CACHED_NUMBER_LABEL)149         sCallsProjectionMap.put(Calls.CACHED_NUMBER_LABEL, Calls.CACHED_NUMBER_LABEL);
sCallsProjectionMap.put(Calls.COUNTRY_ISO, Calls.COUNTRY_ISO)150         sCallsProjectionMap.put(Calls.COUNTRY_ISO, Calls.COUNTRY_ISO);
sCallsProjectionMap.put(Calls.GEOCODED_LOCATION, Calls.GEOCODED_LOCATION)151         sCallsProjectionMap.put(Calls.GEOCODED_LOCATION, Calls.GEOCODED_LOCATION);
sCallsProjectionMap.put(Calls.CACHED_LOOKUP_URI, Calls.CACHED_LOOKUP_URI)152         sCallsProjectionMap.put(Calls.CACHED_LOOKUP_URI, Calls.CACHED_LOOKUP_URI);
sCallsProjectionMap.put(Calls.CACHED_MATCHED_NUMBER, Calls.CACHED_MATCHED_NUMBER)153         sCallsProjectionMap.put(Calls.CACHED_MATCHED_NUMBER, Calls.CACHED_MATCHED_NUMBER);
sCallsProjectionMap.put(Calls.CACHED_NORMALIZED_NUMBER, Calls.CACHED_NORMALIZED_NUMBER)154         sCallsProjectionMap.put(Calls.CACHED_NORMALIZED_NUMBER, Calls.CACHED_NORMALIZED_NUMBER);
sCallsProjectionMap.put(Calls.CACHED_PHOTO_ID, Calls.CACHED_PHOTO_ID)155         sCallsProjectionMap.put(Calls.CACHED_PHOTO_ID, Calls.CACHED_PHOTO_ID);
sCallsProjectionMap.put(Calls.CACHED_PHOTO_URI, Calls.CACHED_PHOTO_URI)156         sCallsProjectionMap.put(Calls.CACHED_PHOTO_URI, Calls.CACHED_PHOTO_URI);
sCallsProjectionMap.put(Calls.CACHED_FORMATTED_NUMBER, Calls.CACHED_FORMATTED_NUMBER)157         sCallsProjectionMap.put(Calls.CACHED_FORMATTED_NUMBER, Calls.CACHED_FORMATTED_NUMBER);
sCallsProjectionMap.put(Calls.ADD_FOR_ALL_USERS, Calls.ADD_FOR_ALL_USERS)158         sCallsProjectionMap.put(Calls.ADD_FOR_ALL_USERS, Calls.ADD_FOR_ALL_USERS);
sCallsProjectionMap.put(Calls.LAST_MODIFIED, Calls.LAST_MODIFIED)159         sCallsProjectionMap.put(Calls.LAST_MODIFIED, Calls.LAST_MODIFIED);
160         sCallsProjectionMap
put(Calls.CALL_SCREENING_COMPONENT_NAME, Calls.CALL_SCREENING_COMPONENT_NAME)161             .put(Calls.CALL_SCREENING_COMPONENT_NAME, Calls.CALL_SCREENING_COMPONENT_NAME);
sCallsProjectionMap.put(Calls.CALL_SCREENING_APP_NAME, Calls.CALL_SCREENING_APP_NAME)162         sCallsProjectionMap.put(Calls.CALL_SCREENING_APP_NAME, Calls.CALL_SCREENING_APP_NAME);
sCallsProjectionMap.put(Calls.BLOCK_REASON, Calls.BLOCK_REASON)163         sCallsProjectionMap.put(Calls.BLOCK_REASON, Calls.BLOCK_REASON);
164     }
165 
166     private static final String ALLOWED_PACKAGE_FOR_TESTING = "com.android.providers.contacts";
167 
168     @VisibleForTesting
169     static final String PARAM_KEY_QUERY_FOR_TESTING = "query_for_testing";
170 
171     /**
172      * A long to override the clock used for timestamps, or "null" to reset to the system clock.
173      */
174     @VisibleForTesting
175     static final String PARAM_KEY_SET_TIME_FOR_TESTING = "set_time_for_testing";
176 
177     private static Long sTimeForTestMillis;
178 
179     private ContactsTaskScheduler mTaskScheduler;
180 
181     private volatile CountDownLatch mReadAccessLatch;
182 
183     private CallLogDatabaseHelper mDbHelper;
184     private DatabaseUtils.InsertHelper mCallsInserter;
185     private boolean mUseStrictPhoneNumberComparation;
186     private int mMinMatch;
187     private VoicemailPermissions mVoicemailPermissions;
188     private CallLogInsertionHelper mCallLogInsertionHelper;
189 
190     private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<>();
191     private final ThreadLocal<Integer> mCallingUid = new ThreadLocal<>();
192     private final ProviderAccessStats mStats = new ProviderAccessStats();
193 
isShadow()194     protected boolean isShadow() {
195         return false;
196     }
197 
getProviderName()198     protected final String getProviderName() {
199         return this.getClass().getSimpleName();
200     }
201 
202     @Override
onCreate()203     public boolean onCreate() {
204         if (VERBOSE_LOGGING) {
205             Log.v(TAG, "onCreate: " + this.getClass().getSimpleName()
206                     + " user=" + android.os.Process.myUserHandle().getIdentifier());
207         }
208 
209         setAppOps(AppOpsManager.OP_READ_CALL_LOG, AppOpsManager.OP_WRITE_CALL_LOG);
210         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
211             Log.d(Constants.PERFORMANCE_TAG, getProviderName() + ".onCreate start");
212         }
213         final Context context = getContext();
214         mDbHelper = getDatabaseHelper(context);
215         mUseStrictPhoneNumberComparation =
216             context.getResources().getBoolean(
217                     com.android.internal.R.bool.config_use_strict_phone_number_comparation);
218         mMinMatch =
219             context.getResources().getInteger(
220                     com.android.internal.R.integer.config_phonenumber_compare_min_match);
221         mVoicemailPermissions = new VoicemailPermissions(context);
222         mCallLogInsertionHelper = createCallLogInsertionHelper(context);
223 
224         mReadAccessLatch = new CountDownLatch(1);
225 
226         mTaskScheduler = new ContactsTaskScheduler(getClass().getSimpleName()) {
227             @Override
228             public void onPerformTask(int taskId, Object arg) {
229                 performBackgroundTask(taskId, arg);
230             }
231         };
232 
233         mTaskScheduler.scheduleTask(BACKGROUND_TASK_INITIALIZE, null);
234 
235         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
236             Log.d(Constants.PERFORMANCE_TAG, getProviderName() + ".onCreate finish");
237         }
238         return true;
239     }
240 
241     @VisibleForTesting
createCallLogInsertionHelper(final Context context)242     protected CallLogInsertionHelper createCallLogInsertionHelper(final Context context) {
243         return DefaultCallLogInsertionHelper.getInstance(context);
244     }
245 
246     @VisibleForTesting
setMinMatchForTest(int minMatch)247     public void setMinMatchForTest(int minMatch) {
248         mMinMatch = minMatch;
249     }
250 
251     @VisibleForTesting
getMinMatchForTest()252     public int getMinMatchForTest() {
253         return mMinMatch;
254     }
255 
getDatabaseHelper(final Context context)256     protected CallLogDatabaseHelper getDatabaseHelper(final Context context) {
257         return CallLogDatabaseHelper.getInstance(context);
258     }
259 
applyingBatch()260     protected boolean applyingBatch() {
261         final Boolean applying =  mApplyingBatch.get();
262         return applying != null && applying;
263     }
264 
265     @Override
applyBatch(ArrayList<ContentProviderOperation> operations)266     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
267             throws OperationApplicationException {
268         final int callingUid = Binder.getCallingUid();
269         mCallingUid.set(callingUid);
270 
271         mStats.incrementBatchStats(callingUid);
272         mApplyingBatch.set(true);
273         try {
274             return super.applyBatch(operations);
275         } finally {
276             mApplyingBatch.set(false);
277             mStats.finishOperation(callingUid);
278         }
279     }
280 
281     @Override
bulkInsert(Uri uri, ContentValues[] values)282     public int bulkInsert(Uri uri, ContentValues[] values) {
283         final int callingUid = Binder.getCallingUid();
284         mCallingUid.set(callingUid);
285 
286         mStats.incrementBatchStats(callingUid);
287         mApplyingBatch.set(true);
288         try {
289             return super.bulkInsert(uri, values);
290         } finally {
291             mApplyingBatch.set(false);
292             mStats.finishOperation(callingUid);
293         }
294     }
295 
296     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)297     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
298             String sortOrder) {
299         // Note don't use mCallingUid here. That's only used by mutation functions.
300         final int callingUid = Binder.getCallingUid();
301 
302         mStats.incrementQueryStats(callingUid);
303         try {
304             return queryInternal(uri, projection, selection, selectionArgs, sortOrder);
305         } finally {
306             mStats.finishOperation(callingUid);
307         }
308     }
309 
queryInternal(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)310     private Cursor queryInternal(Uri uri, String[] projection, String selection,
311             String[] selectionArgs, String sortOrder) {
312         if (VERBOSE_LOGGING) {
313             Log.v(TAG, "query: uri=" + uri + "  projection=" + Arrays.toString(projection) +
314                     "  selection=[" + selection + "]  args=" + Arrays.toString(selectionArgs) +
315                     "  order=[" + sortOrder + "] CPID=" + Binder.getCallingPid() +
316                     " CUID=" + Binder.getCallingUid() +
317                     " User=" + UserUtils.getCurrentUserHandle(getContext()));
318         }
319 
320         queryForTesting(uri);
321 
322         waitForAccess(mReadAccessLatch);
323         final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
324         qb.setTables(Tables.CALLS);
325         qb.setProjectionMap(sCallsProjectionMap);
326         qb.setStrict(true);
327 
328         final SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
329         checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, true /*isQuery*/);
330         selectionBuilder.addClause(EXCLUDE_HIDDEN_SELECTION);
331 
332         final int match = sURIMatcher.match(uri);
333         switch (match) {
334             case CALLS:
335                 break;
336 
337             case CALLS_ID: {
338                 selectionBuilder.addClause(getEqualityClause(Calls._ID,
339                         parseCallIdFromUri(uri)));
340                 break;
341             }
342 
343             case CALLS_FILTER: {
344                 List<String> pathSegments = uri.getPathSegments();
345                 String phoneNumber = pathSegments.size() >= 2 ? pathSegments.get(2) : null;
346                 if (!TextUtils.isEmpty(phoneNumber)) {
347                     qb.appendWhere("PHONE_NUMBERS_EQUAL(number, ");
348                     qb.appendWhereEscapeString(phoneNumber);
349                     qb.appendWhere(mUseStrictPhoneNumberComparation ? ", 1)"
350                             : ", 0, " + mMinMatch + ")");
351                 } else {
352                     qb.appendWhere(Calls.NUMBER_PRESENTATION + "!="
353                             + Calls.PRESENTATION_ALLOWED);
354                 }
355                 break;
356             }
357 
358             default:
359                 throw new IllegalArgumentException("Unknown URL " + uri);
360         }
361 
362         final int limit = getIntParam(uri, Calls.LIMIT_PARAM_KEY, 0);
363         final int offset = getIntParam(uri, Calls.OFFSET_PARAM_KEY, 0);
364         String limitClause = null;
365         if (limit > 0) {
366             limitClause = offset + "," + limit;
367         }
368 
369         final SQLiteDatabase db = mDbHelper.getReadableDatabase();
370         final Cursor c = qb.query(db, projection, selectionBuilder.build(), selectionArgs, null,
371                 null, sortOrder, limitClause);
372         if (c != null) {
373             c.setNotificationUri(getContext().getContentResolver(), CallLog.CONTENT_URI);
374         }
375         return c;
376     }
377 
queryForTesting(Uri uri)378     private void queryForTesting(Uri uri) {
379         if (!uri.getBooleanQueryParameter(PARAM_KEY_QUERY_FOR_TESTING, false)) {
380             return;
381         }
382         if (!getCallingPackage().equals(ALLOWED_PACKAGE_FOR_TESTING)) {
383             throw new IllegalArgumentException("query_for_testing set from foreign package "
384                     + getCallingPackage());
385         }
386 
387         String timeString = uri.getQueryParameter(PARAM_KEY_SET_TIME_FOR_TESTING);
388         if (timeString != null) {
389             if (timeString.equals("null")) {
390                 sTimeForTestMillis = null;
391             } else {
392                 sTimeForTestMillis = Long.parseLong(timeString);
393             }
394         }
395     }
396 
397     @VisibleForTesting
getTimeForTestMillis()398     static Long getTimeForTestMillis() {
399         return sTimeForTestMillis;
400     }
401 
402     /**
403      * Gets an integer query parameter from a given uri.
404      *
405      * @param uri The uri to extract the query parameter from.
406      * @param key The query parameter key.
407      * @param defaultValue A default value to return if the query parameter does not exist.
408      * @return The value from the query parameter in the Uri.  Or the default value if the parameter
409      * does not exist in the uri.
410      * @throws IllegalArgumentException when the value in the query parameter is not an integer.
411      */
getIntParam(Uri uri, String key, int defaultValue)412     private int getIntParam(Uri uri, String key, int defaultValue) {
413         String valueString = uri.getQueryParameter(key);
414         if (valueString == null) {
415             return defaultValue;
416         }
417 
418         try {
419             return Integer.parseInt(valueString);
420         } catch (NumberFormatException e) {
421             String msg = "Integer required for " + key + " parameter but value '" + valueString +
422                     "' was found instead.";
423             throw new IllegalArgumentException(msg, e);
424         }
425     }
426 
427     @Override
getType(Uri uri)428     public String getType(Uri uri) {
429         int match = sURIMatcher.match(uri);
430         switch (match) {
431             case CALLS:
432                 return Calls.CONTENT_TYPE;
433             case CALLS_ID:
434                 return Calls.CONTENT_ITEM_TYPE;
435             case CALLS_FILTER:
436                 return Calls.CONTENT_TYPE;
437             default:
438                 throw new IllegalArgumentException("Unknown URI: " + uri);
439         }
440     }
441 
442     @Override
insert(Uri uri, ContentValues values)443     public Uri insert(Uri uri, ContentValues values) {
444         final int callingUid =
445                 applyingBatch() ? mCallingUid.get() : Binder.getCallingUid();
446 
447         mStats.incrementInsertStats(callingUid, applyingBatch());
448         try {
449             return insertInternal(uri, values);
450         } finally {
451             mStats.finishOperation(callingUid);
452         }
453     }
454 
455     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)456     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
457         final int callingUid =
458                 applyingBatch() ? mCallingUid.get() : Binder.getCallingUid();
459 
460         mStats.incrementInsertStats(callingUid, applyingBatch());
461         try {
462             return updateInternal(uri, values, selection, selectionArgs);
463         } finally {
464             mStats.finishOperation(callingUid);
465         }
466     }
467 
468     @Override
delete(Uri uri, String selection, String[] selectionArgs)469     public int delete(Uri uri, String selection, String[] selectionArgs) {
470         final int callingUid =
471                 applyingBatch() ? mCallingUid.get() : Binder.getCallingUid();
472 
473         mStats.incrementInsertStats(callingUid, applyingBatch());
474         try {
475             return deleteInternal(uri, selection, selectionArgs);
476         } finally {
477             mStats.finishOperation(callingUid);
478         }
479     }
480 
insertInternal(Uri uri, ContentValues values)481     private Uri insertInternal(Uri uri, ContentValues values) {
482         if (VERBOSE_LOGGING) {
483             Log.v(TAG, "insert: uri=" + uri + "  values=[" + values + "]" +
484                     " CPID=" + Binder.getCallingPid() +
485                     " CUID=" + Binder.getCallingUid());
486         }
487         waitForAccess(mReadAccessLatch);
488         checkForSupportedColumns(sCallsProjectionMap, values);
489         // Inserting a voicemail record through call_log requires the voicemail
490         // permission and also requires the additional voicemail param set.
491         if (hasVoicemailValue(values)) {
492             checkIsAllowVoicemailRequest(uri);
493             mVoicemailPermissions.checkCallerHasWriteAccess(getCallingPackage());
494         }
495         if (mCallsInserter == null) {
496             SQLiteDatabase db = mDbHelper.getWritableDatabase();
497             mCallsInserter = new DatabaseUtils.InsertHelper(db, Tables.CALLS);
498         }
499 
500         ContentValues copiedValues = new ContentValues(values);
501 
502         // Add the computed fields to the copied values.
503         mCallLogInsertionHelper.addComputedValues(copiedValues);
504 
505         long rowId = createDatabaseModifier(mCallsInserter).insert(copiedValues);
506         if (rowId > 0) {
507             return ContentUris.withAppendedId(uri, rowId);
508         }
509         return null;
510     }
511 
updateInternal(Uri uri, ContentValues values, String selection, String[] selectionArgs)512     private int updateInternal(Uri uri, ContentValues values,
513             String selection, String[] selectionArgs) {
514         if (VERBOSE_LOGGING) {
515             Log.v(TAG, "update: uri=" + uri +
516                     "  selection=[" + selection + "]  args=" + Arrays.toString(selectionArgs) +
517                     "  values=[" + values + "] CPID=" + Binder.getCallingPid() +
518                     " CUID=" + Binder.getCallingUid() +
519                     " User=" + UserUtils.getCurrentUserHandle(getContext()));
520         }
521         waitForAccess(mReadAccessLatch);
522         checkForSupportedColumns(sCallsProjectionMap, values);
523         // Request that involves changing record type to voicemail requires the
524         // voicemail param set in the uri.
525         if (hasVoicemailValue(values)) {
526             checkIsAllowVoicemailRequest(uri);
527         }
528 
529         SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
530         checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, false /*isQuery*/);
531 
532         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
533         final int matchedUriId = sURIMatcher.match(uri);
534         switch (matchedUriId) {
535             case CALLS:
536                 break;
537 
538             case CALLS_ID:
539                 selectionBuilder.addClause(getEqualityClause(Calls._ID, parseCallIdFromUri(uri)));
540                 break;
541 
542             default:
543                 throw new UnsupportedOperationException("Cannot update URL: " + uri);
544         }
545 
546         return createDatabaseModifier(db).update(uri, Tables.CALLS, values, selectionBuilder.build(),
547                 selectionArgs);
548     }
549 
deleteInternal(Uri uri, String selection, String[] selectionArgs)550     private int deleteInternal(Uri uri, String selection, String[] selectionArgs) {
551         if (VERBOSE_LOGGING) {
552             Log.v(TAG, "delete: uri=" + uri +
553                     "  selection=[" + selection + "]  args=" + Arrays.toString(selectionArgs) +
554                     " CPID=" + Binder.getCallingPid() +
555                     " CUID=" + Binder.getCallingUid() +
556                     " User=" + UserUtils.getCurrentUserHandle(getContext()));
557         }
558         waitForAccess(mReadAccessLatch);
559         SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
560         checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, false /*isQuery*/);
561 
562         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
563         final int matchedUriId = sURIMatcher.match(uri);
564         switch (matchedUriId) {
565             case CALLS:
566                 // TODO: Special case - We may want to forward the delete request on user 0 to the
567                 // shadow provider too.
568                 return createDatabaseModifier(db).delete(Tables.CALLS,
569                         selectionBuilder.build(), selectionArgs);
570             default:
571                 throw new UnsupportedOperationException("Cannot delete that URL: " + uri);
572         }
573     }
574 
adjustForNewPhoneAccount(PhoneAccountHandle handle)575     void adjustForNewPhoneAccount(PhoneAccountHandle handle) {
576         mTaskScheduler.scheduleTask(BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT, handle);
577     }
578 
579     /**
580      * Returns a {@link DatabaseModifier} that takes care of sending necessary notifications
581      * after the operation is performed.
582      */
createDatabaseModifier(SQLiteDatabase db)583     private DatabaseModifier createDatabaseModifier(SQLiteDatabase db) {
584         return new DbModifierWithNotification(Tables.CALLS, db, getContext());
585     }
586 
587     /**
588      * Same as {@link #createDatabaseModifier(SQLiteDatabase)} but used for insert helper operations
589      * only.
590      */
createDatabaseModifier(DatabaseUtils.InsertHelper insertHelper)591     private DatabaseModifier createDatabaseModifier(DatabaseUtils.InsertHelper insertHelper) {
592         return new DbModifierWithNotification(Tables.CALLS, insertHelper, getContext());
593     }
594 
595     private static final Integer VOICEMAIL_TYPE = new Integer(Calls.VOICEMAIL_TYPE);
hasVoicemailValue(ContentValues values)596     private boolean hasVoicemailValue(ContentValues values) {
597         return VOICEMAIL_TYPE.equals(values.getAsInteger(Calls.TYPE));
598     }
599 
600     /**
601      * Checks if the supplied uri requests to include voicemails and take appropriate
602      * action.
603      * <p> If voicemail is requested, then check for voicemail permissions. Otherwise
604      * modify the selection to restrict to non-voicemail entries only.
605      */
checkVoicemailPermissionAndAddRestriction(Uri uri, SelectionBuilder selectionBuilder, boolean isQuery)606     private void checkVoicemailPermissionAndAddRestriction(Uri uri,
607             SelectionBuilder selectionBuilder, boolean isQuery) {
608         if (isAllowVoicemailRequest(uri)) {
609             if (isQuery) {
610                 mVoicemailPermissions.checkCallerHasReadAccess(getCallingPackage());
611             } else {
612                 mVoicemailPermissions.checkCallerHasWriteAccess(getCallingPackage());
613             }
614         } else {
615             selectionBuilder.addClause(EXCLUDE_VOICEMAIL_SELECTION);
616         }
617     }
618 
619     /**
620      * Determines if the supplied uri has the request to allow voicemails to be
621      * included.
622      */
isAllowVoicemailRequest(Uri uri)623     private boolean isAllowVoicemailRequest(Uri uri) {
624         return uri.getBooleanQueryParameter(Calls.ALLOW_VOICEMAILS_PARAM_KEY, false);
625     }
626 
627     /**
628      * Checks to ensure that the given uri has allow_voicemail set. Used by
629      * insert and update operations to check that ContentValues with voicemail
630      * call type must use the voicemail uri.
631      * @throws IllegalArgumentException if allow_voicemail is not set.
632      */
checkIsAllowVoicemailRequest(Uri uri)633     private void checkIsAllowVoicemailRequest(Uri uri) {
634         if (!isAllowVoicemailRequest(uri)) {
635             throw new IllegalArgumentException(
636                     String.format("Uri %s cannot be used for voicemail record." +
637                             " Please set '%s=true' in the uri.", uri,
638                             Calls.ALLOW_VOICEMAILS_PARAM_KEY));
639         }
640     }
641 
642    /**
643     * Parses the call Id from the given uri, assuming that this is a uri that
644     * matches CALLS_ID. For other uri types the behaviour is undefined.
645     * @throws IllegalArgumentException if the id included in the Uri is not a valid long value.
646     */
parseCallIdFromUri(Uri uri)647     private long parseCallIdFromUri(Uri uri) {
648         try {
649             return Long.parseLong(uri.getPathSegments().get(1));
650         } catch (NumberFormatException e) {
651             throw new IllegalArgumentException("Invalid call id in uri: " + uri, e);
652         }
653     }
654 
655     /**
656      * Sync all calllog entries that were inserted
657      */
syncEntries()658     private void syncEntries() {
659         if (isShadow()) {
660             return; // It's the shadow provider itself.  No copying.
661         }
662 
663         final UserManager userManager = UserUtils.getUserManager(getContext());
664 
665         // TODO: http://b/24944959
666         if (!Calls.shouldHaveSharedCallLogEntries(getContext(), userManager,
667                 userManager.getUserHandle())) {
668             return;
669         }
670 
671         final int myUserId = userManager.getUserHandle();
672 
673         // See the comment in Calls.addCall() for the logic.
674 
675         if (userManager.isSystemUser()) {
676             // If it's the system user, just copy from shadow.
677             syncEntriesFrom(UserHandle.USER_SYSTEM, /* sourceIsShadow = */ true,
678                     /* forAllUsersOnly =*/ false);
679         } else {
680             // Otherwise, copy from system's real provider, as well as self's shadow.
681             syncEntriesFrom(UserHandle.USER_SYSTEM, /* sourceIsShadow = */ false,
682                     /* forAllUsersOnly =*/ true);
683             syncEntriesFrom(myUserId, /* sourceIsShadow = */ true,
684                     /* forAllUsersOnly =*/ false);
685         }
686     }
687 
syncEntriesFrom(int sourceUserId, boolean sourceIsShadow, boolean forAllUsersOnly)688     private void syncEntriesFrom(int sourceUserId, boolean sourceIsShadow,
689             boolean forAllUsersOnly) {
690 
691         final Uri sourceUri = sourceIsShadow ? Calls.SHADOW_CONTENT_URI : Calls.CONTENT_URI;
692 
693         final long lastSyncTime = getLastSyncTime(sourceIsShadow);
694 
695         final Uri uri = ContentProvider.maybeAddUserId(sourceUri, sourceUserId);
696         final long newestTimeStamp;
697         final ContentResolver cr = getContext().getContentResolver();
698 
699         final StringBuilder selection = new StringBuilder();
700 
701         selection.append(
702                 "(" + EXCLUDE_VOICEMAIL_SELECTION + ") AND (" + MORE_RECENT_THAN_SELECTION + ")");
703 
704         if (forAllUsersOnly) {
705             selection.append(" AND (" + Calls.ADD_FOR_ALL_USERS + "=1)");
706         }
707 
708         final Cursor cursor = cr.query(
709                 uri,
710                 CALL_LOG_SYNC_PROJECTION,
711                 selection.toString(),
712                 new String[] {String.valueOf(lastSyncTime)},
713                 Calls.DATE + " ASC");
714         if (cursor == null) {
715             return;
716         }
717         try {
718             newestTimeStamp = copyEntriesFromCursor(cursor, lastSyncTime, sourceIsShadow);
719         } finally {
720             cursor.close();
721         }
722         if (sourceIsShadow) {
723             // delete all entries in shadow.
724             cr.delete(uri, Calls.DATE + "<= ?", new String[] {String.valueOf(newestTimeStamp)});
725         }
726     }
727 
728     /**
729      * Un-hides any hidden call log entries that are associated with the specified handle.
730      *
731      * @param handle The handle to the newly registered {@link android.telecom.PhoneAccount}.
732      */
adjustForNewPhoneAccountInternal(PhoneAccountHandle handle)733     private void adjustForNewPhoneAccountInternal(PhoneAccountHandle handle) {
734         String[] handleArgs =
735                 new String[] { handle.getComponentName().flattenToString(), handle.getId() };
736 
737         // Check to see if any entries exist for this handle. If so (not empty), run the un-hiding
738         // update. If not, then try to identify the call from the phone number.
739         Cursor cursor = query(Calls.CONTENT_URI, MINIMAL_PROJECTION,
740                 Calls.PHONE_ACCOUNT_COMPONENT_NAME + " =? AND " + Calls.PHONE_ACCOUNT_ID + " =?",
741                 handleArgs, null);
742 
743         if (cursor != null) {
744             try {
745                 if (cursor.getCount() >= 1) {
746                     // run un-hiding process based on phone account
747                     mDbHelper.getWritableDatabase().execSQL(
748                             UNHIDE_BY_PHONE_ACCOUNT_QUERY, handleArgs);
749                 } else {
750                     TelecomManager tm = getContext().getSystemService(TelecomManager.class);
751                     if (tm != null) {
752                         PhoneAccount account = tm.getPhoneAccount(handle);
753                         if (account != null && account.getAddress() != null) {
754                             // We did not find any items for the specific phone account, so run the
755                             // query based on the phone number instead.
756                             mDbHelper.getWritableDatabase().execSQL(UNHIDE_BY_ADDRESS_QUERY,
757                                     new String[] { account.getAddress().toString() });
758                         }
759 
760                     }
761                 }
762             } finally {
763                 cursor.close();
764             }
765         }
766 
767     }
768 
769     /**
770      * @param cursor to copy call log entries from
771      */
772     @VisibleForTesting
copyEntriesFromCursor(Cursor cursor, long lastSyncTime, boolean forShadow)773     long copyEntriesFromCursor(Cursor cursor, long lastSyncTime, boolean forShadow) {
774         long latestTimestamp = 0;
775         final ContentValues values = new ContentValues();
776         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
777         db.beginTransaction();
778         try {
779             final String[] args = new String[2];
780             cursor.moveToPosition(-1);
781             while (cursor.moveToNext()) {
782                 values.clear();
783                 DatabaseUtils.cursorRowToContentValues(cursor, values);
784 
785                 final String startTime = values.getAsString(Calls.DATE);
786                 final String number = values.getAsString(Calls.NUMBER);
787 
788                 if (startTime == null || number == null) {
789                     continue;
790                 }
791 
792                 if (cursor.isLast()) {
793                     try {
794                         latestTimestamp = Long.valueOf(startTime);
795                     } catch (NumberFormatException e) {
796                         Log.e(TAG, "Call log entry does not contain valid start time: "
797                                 + startTime);
798                     }
799                 }
800 
801                 // Avoid duplicating an already existing entry (which is uniquely identified by
802                 // the number, and the start time)
803                 args[0] = startTime;
804                 args[1] = number;
805                 if (DatabaseUtils.queryNumEntries(db, Tables.CALLS,
806                         Calls.DATE + " = ? AND " + Calls.NUMBER + " = ?", args) > 0) {
807                     continue;
808                 }
809 
810                 db.insert(Tables.CALLS, null, values);
811             }
812 
813             if (latestTimestamp > lastSyncTime) {
814                 setLastTimeSynced(latestTimestamp, forShadow);
815             }
816 
817             db.setTransactionSuccessful();
818         } finally {
819             db.endTransaction();
820         }
821         return latestTimestamp;
822     }
823 
getLastSyncTimePropertyName(boolean forShadow)824     private static String getLastSyncTimePropertyName(boolean forShadow) {
825         return forShadow
826                 ? DbProperties.CALL_LOG_LAST_SYNCED_FOR_SHADOW
827                 : DbProperties.CALL_LOG_LAST_SYNCED;
828     }
829 
830     @VisibleForTesting
getLastSyncTime(boolean forShadow)831     long getLastSyncTime(boolean forShadow) {
832         try {
833             return Long.valueOf(mDbHelper.getProperty(getLastSyncTimePropertyName(forShadow), "0"));
834         } catch (NumberFormatException e) {
835             return 0;
836         }
837     }
838 
setLastTimeSynced(long time, boolean forShadow)839     private void setLastTimeSynced(long time, boolean forShadow) {
840         mDbHelper.setProperty(getLastSyncTimePropertyName(forShadow), String.valueOf(time));
841     }
842 
waitForAccess(CountDownLatch latch)843     private static void waitForAccess(CountDownLatch latch) {
844         if (latch == null) {
845             return;
846         }
847 
848         while (true) {
849             try {
850                 latch.await();
851                 return;
852             } catch (InterruptedException e) {
853                 Thread.currentThread().interrupt();
854             }
855         }
856     }
857 
performBackgroundTask(int task, Object arg)858     private void performBackgroundTask(int task, Object arg) {
859         if (task == BACKGROUND_TASK_INITIALIZE) {
860             try {
861                 syncEntries();
862             } finally {
863                 mReadAccessLatch.countDown();
864                 mReadAccessLatch = null;
865             }
866         } else if (task == BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT) {
867             adjustForNewPhoneAccountInternal((PhoneAccountHandle) arg);
868         }
869     }
870 
871     @Override
shutdown()872     public void shutdown() {
873         mTaskScheduler.shutdownForTest();
874     }
875 
876     @Override
dump(FileDescriptor fd, PrintWriter writer, String[] args)877     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
878         mStats.dump(writer, "  ");
879     }
880 }
881