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