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