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                     " User=" + UserUtils.getCurrentUserHandle(getContext()));
317         }
318 
319         queryForTesting(uri);
320 
321         waitForAccess(mReadAccessLatch);
322         final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
323         qb.setTables(Tables.CALLS);
324         qb.setProjectionMap(sCallsProjectionMap);
325         qb.setStrict(true);
326 
327         final SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
328         checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, true /*isQuery*/);
329         selectionBuilder.addClause(EXCLUDE_HIDDEN_SELECTION);
330 
331         final int match = sURIMatcher.match(uri);
332         switch (match) {
333             case CALLS:
334                 break;
335 
336             case CALLS_ID: {
337                 selectionBuilder.addClause(getEqualityClause(Calls._ID,
338                         parseCallIdFromUri(uri)));
339                 break;
340             }
341 
342             case CALLS_FILTER: {
343                 List<String> pathSegments = uri.getPathSegments();
344                 String phoneNumber = pathSegments.size() >= 2 ? pathSegments.get(2) : null;
345                 if (!TextUtils.isEmpty(phoneNumber)) {
346                     qb.appendWhere("PHONE_NUMBERS_EQUAL(number, ");
347                     qb.appendWhereEscapeString(phoneNumber);
348                     qb.appendWhere(mUseStrictPhoneNumberComparation ? ", 1)"
349                             : ", 0, " + mMinMatch + ")");
350                 } else {
351                     qb.appendWhere(Calls.NUMBER_PRESENTATION + "!="
352                             + Calls.PRESENTATION_ALLOWED);
353                 }
354                 break;
355             }
356 
357             default:
358                 throw new IllegalArgumentException("Unknown URL " + uri);
359         }
360 
361         final int limit = getIntParam(uri, Calls.LIMIT_PARAM_KEY, 0);
362         final int offset = getIntParam(uri, Calls.OFFSET_PARAM_KEY, 0);
363         String limitClause = null;
364         if (limit > 0) {
365             limitClause = offset + "," + limit;
366         }
367 
368         final SQLiteDatabase db = mDbHelper.getReadableDatabase();
369         final Cursor c = qb.query(db, projection, selectionBuilder.build(), selectionArgs, null,
370                 null, sortOrder, limitClause);
371         if (c != null) {
372             c.setNotificationUri(getContext().getContentResolver(), CallLog.CONTENT_URI);
373         }
374         return c;
375     }
376 
queryForTesting(Uri uri)377     private void queryForTesting(Uri uri) {
378         if (!uri.getBooleanQueryParameter(PARAM_KEY_QUERY_FOR_TESTING, false)) {
379             return;
380         }
381         if (!getCallingPackage().equals(ALLOWED_PACKAGE_FOR_TESTING)) {
382             throw new IllegalArgumentException("query_for_testing set from foreign package "
383                     + getCallingPackage());
384         }
385 
386         String timeString = uri.getQueryParameter(PARAM_KEY_SET_TIME_FOR_TESTING);
387         if (timeString != null) {
388             if (timeString.equals("null")) {
389                 sTimeForTestMillis = null;
390             } else {
391                 sTimeForTestMillis = Long.parseLong(timeString);
392             }
393         }
394     }
395 
396     @VisibleForTesting
getTimeForTestMillis()397     static Long getTimeForTestMillis() {
398         return sTimeForTestMillis;
399     }
400 
401     /**
402      * Gets an integer query parameter from a given uri.
403      *
404      * @param uri The uri to extract the query parameter from.
405      * @param key The query parameter key.
406      * @param defaultValue A default value to return if the query parameter does not exist.
407      * @return The value from the query parameter in the Uri.  Or the default value if the parameter
408      * does not exist in the uri.
409      * @throws IllegalArgumentException when the value in the query parameter is not an integer.
410      */
getIntParam(Uri uri, String key, int defaultValue)411     private int getIntParam(Uri uri, String key, int defaultValue) {
412         String valueString = uri.getQueryParameter(key);
413         if (valueString == null) {
414             return defaultValue;
415         }
416 
417         try {
418             return Integer.parseInt(valueString);
419         } catch (NumberFormatException e) {
420             String msg = "Integer required for " + key + " parameter but value '" + valueString +
421                     "' was found instead.";
422             throw new IllegalArgumentException(msg, e);
423         }
424     }
425 
426     @Override
getType(Uri uri)427     public String getType(Uri uri) {
428         int match = sURIMatcher.match(uri);
429         switch (match) {
430             case CALLS:
431                 return Calls.CONTENT_TYPE;
432             case CALLS_ID:
433                 return Calls.CONTENT_ITEM_TYPE;
434             case CALLS_FILTER:
435                 return Calls.CONTENT_TYPE;
436             default:
437                 throw new IllegalArgumentException("Unknown URI: " + uri);
438         }
439     }
440 
441     @Override
insert(Uri uri, ContentValues values)442     public Uri insert(Uri uri, ContentValues values) {
443         final int callingUid =
444                 applyingBatch() ? mCallingUid.get() : Binder.getCallingUid();
445 
446         mStats.incrementInsertStats(callingUid, applyingBatch());
447         try {
448             return insertInternal(uri, values);
449         } finally {
450             mStats.finishOperation(callingUid);
451         }
452     }
453 
454     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)455     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
456         final int callingUid =
457                 applyingBatch() ? mCallingUid.get() : Binder.getCallingUid();
458 
459         mStats.incrementInsertStats(callingUid, applyingBatch());
460         try {
461             return updateInternal(uri, values, selection, selectionArgs);
462         } finally {
463             mStats.finishOperation(callingUid);
464         }
465     }
466 
467     @Override
delete(Uri uri, String selection, String[] selectionArgs)468     public int delete(Uri uri, String selection, String[] selectionArgs) {
469         final int callingUid =
470                 applyingBatch() ? mCallingUid.get() : Binder.getCallingUid();
471 
472         mStats.incrementInsertStats(callingUid, applyingBatch());
473         try {
474             return deleteInternal(uri, selection, selectionArgs);
475         } finally {
476             mStats.finishOperation(callingUid);
477         }
478     }
479 
insertInternal(Uri uri, ContentValues values)480     private Uri insertInternal(Uri uri, ContentValues values) {
481         if (VERBOSE_LOGGING) {
482             Log.v(TAG, "insert: uri=" + uri + "  values=[" + values + "]" +
483                     " CPID=" + Binder.getCallingPid());
484         }
485         waitForAccess(mReadAccessLatch);
486         checkForSupportedColumns(sCallsProjectionMap, values);
487         // Inserting a voicemail record through call_log requires the voicemail
488         // permission and also requires the additional voicemail param set.
489         if (hasVoicemailValue(values)) {
490             checkIsAllowVoicemailRequest(uri);
491             mVoicemailPermissions.checkCallerHasWriteAccess(getCallingPackage());
492         }
493         if (mCallsInserter == null) {
494             SQLiteDatabase db = mDbHelper.getWritableDatabase();
495             mCallsInserter = new DatabaseUtils.InsertHelper(db, Tables.CALLS);
496         }
497 
498         ContentValues copiedValues = new ContentValues(values);
499 
500         // Add the computed fields to the copied values.
501         mCallLogInsertionHelper.addComputedValues(copiedValues);
502 
503         long rowId = createDatabaseModifier(mCallsInserter).insert(copiedValues);
504         if (rowId > 0) {
505             return ContentUris.withAppendedId(uri, rowId);
506         }
507         return null;
508     }
509 
updateInternal(Uri uri, ContentValues values, String selection, String[] selectionArgs)510     private int updateInternal(Uri uri, ContentValues values,
511             String selection, String[] selectionArgs) {
512         if (VERBOSE_LOGGING) {
513             Log.v(TAG, "update: uri=" + uri +
514                     "  selection=[" + selection + "]  args=" + Arrays.toString(selectionArgs) +
515                     "  values=[" + values + "] CPID=" + Binder.getCallingPid() +
516                     " User=" + UserUtils.getCurrentUserHandle(getContext()));
517         }
518         waitForAccess(mReadAccessLatch);
519         checkForSupportedColumns(sCallsProjectionMap, values);
520         // Request that involves changing record type to voicemail requires the
521         // voicemail param set in the uri.
522         if (hasVoicemailValue(values)) {
523             checkIsAllowVoicemailRequest(uri);
524         }
525 
526         SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
527         checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, false /*isQuery*/);
528 
529         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
530         final int matchedUriId = sURIMatcher.match(uri);
531         switch (matchedUriId) {
532             case CALLS:
533                 break;
534 
535             case CALLS_ID:
536                 selectionBuilder.addClause(getEqualityClause(Calls._ID, parseCallIdFromUri(uri)));
537                 break;
538 
539             default:
540                 throw new UnsupportedOperationException("Cannot update URL: " + uri);
541         }
542 
543         return createDatabaseModifier(db).update(uri, Tables.CALLS, values, selectionBuilder.build(),
544                 selectionArgs);
545     }
546 
deleteInternal(Uri uri, String selection, String[] selectionArgs)547     private int deleteInternal(Uri uri, String selection, String[] selectionArgs) {
548         if (VERBOSE_LOGGING) {
549             Log.v(TAG, "delete: uri=" + uri +
550                     "  selection=[" + selection + "]  args=" + Arrays.toString(selectionArgs) +
551                     " CPID=" + Binder.getCallingPid() +
552                     " User=" + UserUtils.getCurrentUserHandle(getContext()));
553         }
554         waitForAccess(mReadAccessLatch);
555         SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
556         checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, false /*isQuery*/);
557 
558         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
559         final int matchedUriId = sURIMatcher.match(uri);
560         switch (matchedUriId) {
561             case CALLS:
562                 // TODO: Special case - We may want to forward the delete request on user 0 to the
563                 // shadow provider too.
564                 return createDatabaseModifier(db).delete(Tables.CALLS,
565                         selectionBuilder.build(), selectionArgs);
566             default:
567                 throw new UnsupportedOperationException("Cannot delete that URL: " + uri);
568         }
569     }
570 
adjustForNewPhoneAccount(PhoneAccountHandle handle)571     void adjustForNewPhoneAccount(PhoneAccountHandle handle) {
572         mTaskScheduler.scheduleTask(BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT, handle);
573     }
574 
575     /**
576      * Returns a {@link DatabaseModifier} that takes care of sending necessary notifications
577      * after the operation is performed.
578      */
createDatabaseModifier(SQLiteDatabase db)579     private DatabaseModifier createDatabaseModifier(SQLiteDatabase db) {
580         return new DbModifierWithNotification(Tables.CALLS, db, getContext());
581     }
582 
583     /**
584      * Same as {@link #createDatabaseModifier(SQLiteDatabase)} but used for insert helper operations
585      * only.
586      */
createDatabaseModifier(DatabaseUtils.InsertHelper insertHelper)587     private DatabaseModifier createDatabaseModifier(DatabaseUtils.InsertHelper insertHelper) {
588         return new DbModifierWithNotification(Tables.CALLS, insertHelper, getContext());
589     }
590 
591     private static final Integer VOICEMAIL_TYPE = new Integer(Calls.VOICEMAIL_TYPE);
hasVoicemailValue(ContentValues values)592     private boolean hasVoicemailValue(ContentValues values) {
593         return VOICEMAIL_TYPE.equals(values.getAsInteger(Calls.TYPE));
594     }
595 
596     /**
597      * Checks if the supplied uri requests to include voicemails and take appropriate
598      * action.
599      * <p> If voicemail is requested, then check for voicemail permissions. Otherwise
600      * modify the selection to restrict to non-voicemail entries only.
601      */
checkVoicemailPermissionAndAddRestriction(Uri uri, SelectionBuilder selectionBuilder, boolean isQuery)602     private void checkVoicemailPermissionAndAddRestriction(Uri uri,
603             SelectionBuilder selectionBuilder, boolean isQuery) {
604         if (isAllowVoicemailRequest(uri)) {
605             if (isQuery) {
606                 mVoicemailPermissions.checkCallerHasReadAccess(getCallingPackage());
607             } else {
608                 mVoicemailPermissions.checkCallerHasWriteAccess(getCallingPackage());
609             }
610         } else {
611             selectionBuilder.addClause(EXCLUDE_VOICEMAIL_SELECTION);
612         }
613     }
614 
615     /**
616      * Determines if the supplied uri has the request to allow voicemails to be
617      * included.
618      */
isAllowVoicemailRequest(Uri uri)619     private boolean isAllowVoicemailRequest(Uri uri) {
620         return uri.getBooleanQueryParameter(Calls.ALLOW_VOICEMAILS_PARAM_KEY, false);
621     }
622 
623     /**
624      * Checks to ensure that the given uri has allow_voicemail set. Used by
625      * insert and update operations to check that ContentValues with voicemail
626      * call type must use the voicemail uri.
627      * @throws IllegalArgumentException if allow_voicemail is not set.
628      */
checkIsAllowVoicemailRequest(Uri uri)629     private void checkIsAllowVoicemailRequest(Uri uri) {
630         if (!isAllowVoicemailRequest(uri)) {
631             throw new IllegalArgumentException(
632                     String.format("Uri %s cannot be used for voicemail record." +
633                             " Please set '%s=true' in the uri.", uri,
634                             Calls.ALLOW_VOICEMAILS_PARAM_KEY));
635         }
636     }
637 
638    /**
639     * Parses the call Id from the given uri, assuming that this is a uri that
640     * matches CALLS_ID. For other uri types the behaviour is undefined.
641     * @throws IllegalArgumentException if the id included in the Uri is not a valid long value.
642     */
parseCallIdFromUri(Uri uri)643     private long parseCallIdFromUri(Uri uri) {
644         try {
645             return Long.parseLong(uri.getPathSegments().get(1));
646         } catch (NumberFormatException e) {
647             throw new IllegalArgumentException("Invalid call id in uri: " + uri, e);
648         }
649     }
650 
651     /**
652      * Sync all calllog entries that were inserted
653      */
syncEntries()654     private void syncEntries() {
655         if (isShadow()) {
656             return; // It's the shadow provider itself.  No copying.
657         }
658 
659         final UserManager userManager = UserUtils.getUserManager(getContext());
660 
661         // TODO: http://b/24944959
662         if (!Calls.shouldHaveSharedCallLogEntries(getContext(), userManager,
663                 userManager.getUserHandle())) {
664             return;
665         }
666 
667         final int myUserId = userManager.getUserHandle();
668 
669         // See the comment in Calls.addCall() for the logic.
670 
671         if (userManager.isSystemUser()) {
672             // If it's the system user, just copy from shadow.
673             syncEntriesFrom(UserHandle.USER_SYSTEM, /* sourceIsShadow = */ true,
674                     /* forAllUsersOnly =*/ false);
675         } else {
676             // Otherwise, copy from system's real provider, as well as self's shadow.
677             syncEntriesFrom(UserHandle.USER_SYSTEM, /* sourceIsShadow = */ false,
678                     /* forAllUsersOnly =*/ true);
679             syncEntriesFrom(myUserId, /* sourceIsShadow = */ true,
680                     /* forAllUsersOnly =*/ false);
681         }
682     }
683 
syncEntriesFrom(int sourceUserId, boolean sourceIsShadow, boolean forAllUsersOnly)684     private void syncEntriesFrom(int sourceUserId, boolean sourceIsShadow,
685             boolean forAllUsersOnly) {
686 
687         final Uri sourceUri = sourceIsShadow ? Calls.SHADOW_CONTENT_URI : Calls.CONTENT_URI;
688 
689         final long lastSyncTime = getLastSyncTime(sourceIsShadow);
690 
691         final Uri uri = ContentProvider.maybeAddUserId(sourceUri, sourceUserId);
692         final long newestTimeStamp;
693         final ContentResolver cr = getContext().getContentResolver();
694 
695         final StringBuilder selection = new StringBuilder();
696 
697         selection.append(
698                 "(" + EXCLUDE_VOICEMAIL_SELECTION + ") AND (" + MORE_RECENT_THAN_SELECTION + ")");
699 
700         if (forAllUsersOnly) {
701             selection.append(" AND (" + Calls.ADD_FOR_ALL_USERS + "=1)");
702         }
703 
704         final Cursor cursor = cr.query(
705                 uri,
706                 CALL_LOG_SYNC_PROJECTION,
707                 selection.toString(),
708                 new String[] {String.valueOf(lastSyncTime)},
709                 Calls.DATE + " ASC");
710         if (cursor == null) {
711             return;
712         }
713         try {
714             newestTimeStamp = copyEntriesFromCursor(cursor, lastSyncTime, sourceIsShadow);
715         } finally {
716             cursor.close();
717         }
718         if (sourceIsShadow) {
719             // delete all entries in shadow.
720             cr.delete(uri, Calls.DATE + "<= ?", new String[] {String.valueOf(newestTimeStamp)});
721         }
722     }
723 
724     /**
725      * Un-hides any hidden call log entries that are associated with the specified handle.
726      *
727      * @param handle The handle to the newly registered {@link android.telecom.PhoneAccount}.
728      */
adjustForNewPhoneAccountInternal(PhoneAccountHandle handle)729     private void adjustForNewPhoneAccountInternal(PhoneAccountHandle handle) {
730         String[] handleArgs =
731                 new String[] { handle.getComponentName().flattenToString(), handle.getId() };
732 
733         // Check to see if any entries exist for this handle. If so (not empty), run the un-hiding
734         // update. If not, then try to identify the call from the phone number.
735         Cursor cursor = query(Calls.CONTENT_URI, MINIMAL_PROJECTION,
736                 Calls.PHONE_ACCOUNT_COMPONENT_NAME + " =? AND " + Calls.PHONE_ACCOUNT_ID + " =?",
737                 handleArgs, null);
738 
739         if (cursor != null) {
740             try {
741                 if (cursor.getCount() >= 1) {
742                     // run un-hiding process based on phone account
743                     mDbHelper.getWritableDatabase().execSQL(
744                             UNHIDE_BY_PHONE_ACCOUNT_QUERY, handleArgs);
745                 } else {
746                     TelecomManager tm = getContext().getSystemService(TelecomManager.class);
747                     if (tm != null) {
748                         PhoneAccount account = tm.getPhoneAccount(handle);
749                         if (account != null && account.getAddress() != null) {
750                             // We did not find any items for the specific phone account, so run the
751                             // query based on the phone number instead.
752                             mDbHelper.getWritableDatabase().execSQL(UNHIDE_BY_ADDRESS_QUERY,
753                                     new String[] { account.getAddress().toString() });
754                         }
755 
756                     }
757                 }
758             } finally {
759                 cursor.close();
760             }
761         }
762 
763     }
764 
765     /**
766      * @param cursor to copy call log entries from
767      */
768     @VisibleForTesting
copyEntriesFromCursor(Cursor cursor, long lastSyncTime, boolean forShadow)769     long copyEntriesFromCursor(Cursor cursor, long lastSyncTime, boolean forShadow) {
770         long latestTimestamp = 0;
771         final ContentValues values = new ContentValues();
772         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
773         db.beginTransaction();
774         try {
775             final String[] args = new String[2];
776             cursor.moveToPosition(-1);
777             while (cursor.moveToNext()) {
778                 values.clear();
779                 DatabaseUtils.cursorRowToContentValues(cursor, values);
780 
781                 final String startTime = values.getAsString(Calls.DATE);
782                 final String number = values.getAsString(Calls.NUMBER);
783 
784                 if (startTime == null || number == null) {
785                     continue;
786                 }
787 
788                 if (cursor.isLast()) {
789                     try {
790                         latestTimestamp = Long.valueOf(startTime);
791                     } catch (NumberFormatException e) {
792                         Log.e(TAG, "Call log entry does not contain valid start time: "
793                                 + startTime);
794                     }
795                 }
796 
797                 // Avoid duplicating an already existing entry (which is uniquely identified by
798                 // the number, and the start time)
799                 args[0] = startTime;
800                 args[1] = number;
801                 if (DatabaseUtils.queryNumEntries(db, Tables.CALLS,
802                         Calls.DATE + " = ? AND " + Calls.NUMBER + " = ?", args) > 0) {
803                     continue;
804                 }
805 
806                 db.insert(Tables.CALLS, null, values);
807             }
808 
809             if (latestTimestamp > lastSyncTime) {
810                 setLastTimeSynced(latestTimestamp, forShadow);
811             }
812 
813             db.setTransactionSuccessful();
814         } finally {
815             db.endTransaction();
816         }
817         return latestTimestamp;
818     }
819 
getLastSyncTimePropertyName(boolean forShadow)820     private static String getLastSyncTimePropertyName(boolean forShadow) {
821         return forShadow
822                 ? DbProperties.CALL_LOG_LAST_SYNCED_FOR_SHADOW
823                 : DbProperties.CALL_LOG_LAST_SYNCED;
824     }
825 
826     @VisibleForTesting
getLastSyncTime(boolean forShadow)827     long getLastSyncTime(boolean forShadow) {
828         try {
829             return Long.valueOf(mDbHelper.getProperty(getLastSyncTimePropertyName(forShadow), "0"));
830         } catch (NumberFormatException e) {
831             return 0;
832         }
833     }
834 
setLastTimeSynced(long time, boolean forShadow)835     private void setLastTimeSynced(long time, boolean forShadow) {
836         mDbHelper.setProperty(getLastSyncTimePropertyName(forShadow), String.valueOf(time));
837     }
838 
waitForAccess(CountDownLatch latch)839     private static void waitForAccess(CountDownLatch latch) {
840         if (latch == null) {
841             return;
842         }
843 
844         while (true) {
845             try {
846                 latch.await();
847                 return;
848             } catch (InterruptedException e) {
849                 Thread.currentThread().interrupt();
850             }
851         }
852     }
853 
performBackgroundTask(int task, Object arg)854     private void performBackgroundTask(int task, Object arg) {
855         if (task == BACKGROUND_TASK_INITIALIZE) {
856             try {
857                 syncEntries();
858             } finally {
859                 mReadAccessLatch.countDown();
860                 mReadAccessLatch = null;
861             }
862         } else if (task == BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT) {
863             adjustForNewPhoneAccountInternal((PhoneAccountHandle) arg);
864         }
865     }
866 
867     @Override
shutdown()868     public void shutdown() {
869         mTaskScheduler.shutdownForTest();
870     }
871 
872     @Override
dump(FileDescriptor fd, PrintWriter writer, String[] args)873     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
874         mStats.dump(writer, "  ");
875     }
876 }
877