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