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
17package com.android.providers.contacts;
18
19import static com.android.providers.contacts.util.DbQueryUtils.checkForSupportedColumns;
20import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause;
21import static com.android.providers.contacts.util.DbQueryUtils.getInequalityClause;
22
23import android.app.AppOpsManager;
24import android.content.ContentProvider;
25import android.content.ContentProviderOperation;
26import android.content.ContentProviderResult;
27import android.content.ContentResolver;
28import android.content.ContentUris;
29import android.content.ContentValues;
30import android.content.Context;
31import android.content.OperationApplicationException;
32import android.content.UriMatcher;
33import android.database.Cursor;
34import android.database.DatabaseUtils;
35import android.database.sqlite.SQLiteDatabase;
36import android.database.sqlite.SQLiteQueryBuilder;
37import android.net.Uri;
38import android.os.Binder;
39import android.os.UserHandle;
40import android.os.UserManager;
41import android.provider.CallLog;
42import android.provider.CallLog.Calls;
43import android.telecom.PhoneAccount;
44import android.telecom.PhoneAccountHandle;
45import android.telecom.TelecomManager;
46import android.text.TextUtils;
47import android.util.ArrayMap;
48import android.util.Log;
49
50import com.android.internal.annotations.VisibleForTesting;
51import com.android.internal.util.ProviderAccessStats;
52import com.android.providers.contacts.CallLogDatabaseHelper.DbProperties;
53import com.android.providers.contacts.CallLogDatabaseHelper.Tables;
54import com.android.providers.contacts.util.SelectionBuilder;
55import com.android.providers.contacts.util.UserUtils;
56
57import java.io.FileDescriptor;
58import java.io.PrintWriter;
59import java.util.ArrayList;
60import java.util.Arrays;
61import java.util.List;
62import java.util.concurrent.CountDownLatch;
63
64/**
65 * Call log content provider.
66 */
67public 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