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