Home | History | Annotate | Download | only in contacts
      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 {
    111         sURIMatcher.addURI(CallLog.AUTHORITY, "calls", CALLS);
    112         sURIMatcher.addURI(CallLog.AUTHORITY, "calls/#", CALLS_ID);
    113         sURIMatcher.addURI(CallLog.AUTHORITY, "calls/filter/*", CALLS_FILTER);
    114 
    115         // Shadow provider only supports "/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>();
    124         sCallsProjectionMap.put(Calls._ID, Calls._ID);
    125         sCallsProjectionMap.put(Calls.NUMBER, Calls.NUMBER);
    126         sCallsProjectionMap.put(Calls.POST_DIAL_DIGITS, Calls.POST_DIAL_DIGITS);
    127         sCallsProjectionMap.put(Calls.VIA_NUMBER, Calls.VIA_NUMBER);
    128         sCallsProjectionMap.put(Calls.NUMBER_PRESENTATION, Calls.NUMBER_PRESENTATION);
    129         sCallsProjectionMap.put(Calls.DATE, Calls.DATE);
    130         sCallsProjectionMap.put(Calls.DURATION, Calls.DURATION);
    131         sCallsProjectionMap.put(Calls.DATA_USAGE, Calls.DATA_USAGE);
    132         sCallsProjectionMap.put(Calls.TYPE, Calls.TYPE);
    133         sCallsProjectionMap.put(Calls.FEATURES, Calls.FEATURES);
    134         sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, Calls.PHONE_ACCOUNT_COMPONENT_NAME);
    135         sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_ID, Calls.PHONE_ACCOUNT_ID);
    136         sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_ADDRESS, Calls.PHONE_ACCOUNT_ADDRESS);
    137         sCallsProjectionMap.put(Calls.NEW, Calls.NEW);
    138         sCallsProjectionMap.put(Calls.VOICEMAIL_URI, Calls.VOICEMAIL_URI);
    139         sCallsProjectionMap.put(Calls.TRANSCRIPTION, Calls.TRANSCRIPTION);
    140         sCallsProjectionMap.put(Calls.IS_READ, Calls.IS_READ);
    141         sCallsProjectionMap.put(Calls.CACHED_NAME, Calls.CACHED_NAME);
    142         sCallsProjectionMap.put(Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_TYPE);
    143         sCallsProjectionMap.put(Calls.CACHED_NUMBER_LABEL, Calls.CACHED_NUMBER_LABEL);
    144         sCallsProjectionMap.put(Calls.COUNTRY_ISO, Calls.COUNTRY_ISO);
    145         sCallsProjectionMap.put(Calls.GEOCODED_LOCATION, Calls.GEOCODED_LOCATION);
    146         sCallsProjectionMap.put(Calls.CACHED_LOOKUP_URI, Calls.CACHED_LOOKUP_URI);
    147         sCallsProjectionMap.put(Calls.CACHED_MATCHED_NUMBER, Calls.CACHED_MATCHED_NUMBER);
    148         sCallsProjectionMap.put(Calls.CACHED_NORMALIZED_NUMBER, Calls.CACHED_NORMALIZED_NUMBER);
    149         sCallsProjectionMap.put(Calls.CACHED_PHOTO_ID, Calls.CACHED_PHOTO_ID);
    150         sCallsProjectionMap.put(Calls.CACHED_PHOTO_URI, Calls.CACHED_PHOTO_URI);
    151         sCallsProjectionMap.put(Calls.CACHED_FORMATTED_NUMBER, Calls.CACHED_FORMATTED_NUMBER);
    152         sCallsProjectionMap.put(Calls.ADD_FOR_ALL_USERS, Calls.ADD_FOR_ALL_USERS);
    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 
    166     protected boolean isShadow() {
    167         return false;
    168     }
    169 
    170     protected final String getProviderName() {
    171         return this.getClass().getSimpleName();
    172     }
    173 
    174     @Override
    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
    209     protected CallLogInsertionHelper createCallLogInsertionHelper(final Context context) {
    210         return DefaultCallLogInsertionHelper.getInstance(context);
    211     }
    212 
    213     protected CallLogDatabaseHelper getDatabaseHelper(final Context context) {
    214         return CallLogDatabaseHelper.getInstance(context);
    215     }
    216 
    217     @Override
    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      */
    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
    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
    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
    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
    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 
    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      */
    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      */
    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);
    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      */
    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      */
    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      */
    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     */
    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      */
    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 
    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      */
    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
    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 
    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
    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 
    679     private void setLastTimeSynced(long time, boolean forShadow) {
    680         mDbHelper.setProperty(getLastSyncTimePropertyName(forShadow), String.valueOf(time));
    681     }
    682 
    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 
    698     private void scheduleBackgroundTask(int task, Object arg) {
    699         mBackgroundHandler.obtainMessage(task, arg).sendToTarget();
    700     }
    701 
    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