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