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.ContentUris;
     26 import android.content.ContentValues;
     27 import android.content.Context;
     28 import android.content.UriMatcher;
     29 import android.database.Cursor;
     30 import android.database.DatabaseUtils;
     31 import android.database.sqlite.SQLiteDatabase;
     32 import android.database.sqlite.SQLiteQueryBuilder;
     33 import android.net.Uri;
     34 import android.os.UserHandle;
     35 import android.os.UserManager;
     36 import android.provider.CallLog;
     37 import android.provider.CallLog.Calls;
     38 import android.text.TextUtils;
     39 import android.util.Log;
     40 
     41 import com.android.providers.contacts.ContactsDatabaseHelper.DbProperties;
     42 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
     43 import com.android.providers.contacts.util.SelectionBuilder;
     44 import com.android.providers.contacts.util.UserUtils;
     45 
     46 import com.google.common.annotations.VisibleForTesting;
     47 
     48 import java.util.HashMap;
     49 import java.util.List;
     50 
     51 /**
     52  * Call log content provider.
     53  */
     54 public class CallLogProvider extends ContentProvider {
     55     private static final String TAG = CallLogProvider.class.getSimpleName();
     56 
     57     /** Selection clause for selecting all calls that were made after a certain time */
     58     private static final String MORE_RECENT_THAN_SELECTION = Calls.DATE + "> ?";
     59     /** Selection clause to use to exclude voicemail records.  */
     60     private static final String EXCLUDE_VOICEMAIL_SELECTION = getInequalityClause(
     61             Calls.TYPE, Calls.VOICEMAIL_TYPE);
     62 
     63     @VisibleForTesting
     64     static final String[] CALL_LOG_SYNC_PROJECTION = new String[] {
     65         Calls.NUMBER,
     66         Calls.NUMBER_PRESENTATION,
     67         Calls.TYPE,
     68         Calls.FEATURES,
     69         Calls.DATE,
     70         Calls.DURATION,
     71         Calls.DATA_USAGE,
     72         Calls.PHONE_ACCOUNT_COMPONENT_NAME,
     73         Calls.PHONE_ACCOUNT_ID
     74     };
     75 
     76     private static final int CALLS = 1;
     77 
     78     private static final int CALLS_ID = 2;
     79 
     80     private static final int CALLS_FILTER = 3;
     81 
     82     private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
     83     static {
     84         sURIMatcher.addURI(CallLog.AUTHORITY, "calls", CALLS);
     85         sURIMatcher.addURI(CallLog.AUTHORITY, "calls/#", CALLS_ID);
     86         sURIMatcher.addURI(CallLog.AUTHORITY, "calls/filter/*", CALLS_FILTER);
     87     }
     88 
     89     private static final HashMap<String, String> sCallsProjectionMap;
     90     static {
     91 
     92         // Calls projection map
     93         sCallsProjectionMap = new HashMap<String, String>();
     94         sCallsProjectionMap.put(Calls._ID, Calls._ID);
     95         sCallsProjectionMap.put(Calls.NUMBER, Calls.NUMBER);
     96         sCallsProjectionMap.put(Calls.NUMBER_PRESENTATION, Calls.NUMBER_PRESENTATION);
     97         sCallsProjectionMap.put(Calls.DATE, Calls.DATE);
     98         sCallsProjectionMap.put(Calls.DURATION, Calls.DURATION);
     99         sCallsProjectionMap.put(Calls.DATA_USAGE, Calls.DATA_USAGE);
    100         sCallsProjectionMap.put(Calls.TYPE, Calls.TYPE);
    101         sCallsProjectionMap.put(Calls.FEATURES, Calls.FEATURES);
    102         sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, Calls.PHONE_ACCOUNT_COMPONENT_NAME);
    103         sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_ID, Calls.PHONE_ACCOUNT_ID);
    104         sCallsProjectionMap.put(Calls.NEW, Calls.NEW);
    105         sCallsProjectionMap.put(Calls.VOICEMAIL_URI, Calls.VOICEMAIL_URI);
    106         sCallsProjectionMap.put(Calls.TRANSCRIPTION, Calls.TRANSCRIPTION);
    107         sCallsProjectionMap.put(Calls.IS_READ, Calls.IS_READ);
    108         sCallsProjectionMap.put(Calls.CACHED_NAME, Calls.CACHED_NAME);
    109         sCallsProjectionMap.put(Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_TYPE);
    110         sCallsProjectionMap.put(Calls.CACHED_NUMBER_LABEL, Calls.CACHED_NUMBER_LABEL);
    111         sCallsProjectionMap.put(Calls.COUNTRY_ISO, Calls.COUNTRY_ISO);
    112         sCallsProjectionMap.put(Calls.GEOCODED_LOCATION, Calls.GEOCODED_LOCATION);
    113         sCallsProjectionMap.put(Calls.CACHED_LOOKUP_URI, Calls.CACHED_LOOKUP_URI);
    114         sCallsProjectionMap.put(Calls.CACHED_MATCHED_NUMBER, Calls.CACHED_MATCHED_NUMBER);
    115         sCallsProjectionMap.put(Calls.CACHED_NORMALIZED_NUMBER, Calls.CACHED_NORMALIZED_NUMBER);
    116         sCallsProjectionMap.put(Calls.CACHED_PHOTO_ID, Calls.CACHED_PHOTO_ID);
    117         sCallsProjectionMap.put(Calls.CACHED_FORMATTED_NUMBER, Calls.CACHED_FORMATTED_NUMBER);
    118     }
    119 
    120     private ContactsDatabaseHelper mDbHelper;
    121     private DatabaseUtils.InsertHelper mCallsInserter;
    122     private boolean mUseStrictPhoneNumberComparation;
    123     private VoicemailPermissions mVoicemailPermissions;
    124     private CallLogInsertionHelper mCallLogInsertionHelper;
    125 
    126     @Override
    127     public boolean onCreate() {
    128         setAppOps(AppOpsManager.OP_READ_CALL_LOG, AppOpsManager.OP_WRITE_CALL_LOG);
    129         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
    130             Log.d(Constants.PERFORMANCE_TAG, "CallLogProvider.onCreate start");
    131         }
    132         final Context context = getContext();
    133         mDbHelper = getDatabaseHelper(context);
    134         mUseStrictPhoneNumberComparation =
    135             context.getResources().getBoolean(
    136                     com.android.internal.R.bool.config_use_strict_phone_number_comparation);
    137         mVoicemailPermissions = new VoicemailPermissions(context);
    138         mCallLogInsertionHelper = createCallLogInsertionHelper(context);
    139         final UserManager userManager = UserUtils.getUserManager(context);
    140         if (userManager != null &&
    141                 !userManager.hasUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS)) {
    142             syncEntriesFromPrimaryUser(userManager);
    143         }
    144 
    145         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
    146             Log.d(Constants.PERFORMANCE_TAG, "CallLogProvider.onCreate finish");
    147         }
    148         return true;
    149     }
    150 
    151     @VisibleForTesting
    152     protected CallLogInsertionHelper createCallLogInsertionHelper(final Context context) {
    153         return DefaultCallLogInsertionHelper.getInstance(context);
    154     }
    155 
    156     @VisibleForTesting
    157     protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
    158         return ContactsDatabaseHelper.getInstance(context);
    159     }
    160 
    161     @Override
    162     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
    163             String sortOrder) {
    164         final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
    165         qb.setTables(Tables.CALLS);
    166         qb.setProjectionMap(sCallsProjectionMap);
    167         qb.setStrict(true);
    168 
    169         final SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
    170         checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, true /*isQuery*/);
    171 
    172         final int match = sURIMatcher.match(uri);
    173         switch (match) {
    174             case CALLS:
    175                 break;
    176 
    177             case CALLS_ID: {
    178                 selectionBuilder.addClause(getEqualityClause(Calls._ID,
    179                         parseCallIdFromUri(uri)));
    180                 break;
    181             }
    182 
    183             case CALLS_FILTER: {
    184                 List<String> pathSegments = uri.getPathSegments();
    185                 String phoneNumber = pathSegments.size() >= 2 ? pathSegments.get(2) : null;
    186                 if (!TextUtils.isEmpty(phoneNumber)) {
    187                     qb.appendWhere("PHONE_NUMBERS_EQUAL(number, ");
    188                     qb.appendWhereEscapeString(phoneNumber);
    189                     qb.appendWhere(mUseStrictPhoneNumberComparation ? ", 1)" : ", 0)");
    190                 } else {
    191                     qb.appendWhere(Calls.NUMBER_PRESENTATION + "!="
    192                             + Calls.PRESENTATION_ALLOWED);
    193                 }
    194                 break;
    195             }
    196 
    197             default:
    198                 throw new IllegalArgumentException("Unknown URL " + uri);
    199         }
    200 
    201         final int limit = getIntParam(uri, Calls.LIMIT_PARAM_KEY, 0);
    202         final int offset = getIntParam(uri, Calls.OFFSET_PARAM_KEY, 0);
    203         String limitClause = null;
    204         if (limit > 0) {
    205             limitClause = offset + "," + limit;
    206         }
    207 
    208         final SQLiteDatabase db = mDbHelper.getReadableDatabase();
    209         final Cursor c = qb.query(db, projection, selectionBuilder.build(), selectionArgs, null,
    210                 null, sortOrder, limitClause);
    211         if (c != null) {
    212             c.setNotificationUri(getContext().getContentResolver(), CallLog.CONTENT_URI);
    213         }
    214         return c;
    215     }
    216 
    217     /**
    218      * Gets an integer query parameter from a given uri.
    219      *
    220      * @param uri The uri to extract the query parameter from.
    221      * @param key The query parameter key.
    222      * @param defaultValue A default value to return if the query parameter does not exist.
    223      * @return The value from the query parameter in the Uri.  Or the default value if the parameter
    224      * does not exist in the uri.
    225      * @throws IllegalArgumentException when the value in the query parameter is not an integer.
    226      */
    227     private int getIntParam(Uri uri, String key, int defaultValue) {
    228         String valueString = uri.getQueryParameter(key);
    229         if (valueString == null) {
    230             return defaultValue;
    231         }
    232 
    233         try {
    234             return Integer.parseInt(valueString);
    235         } catch (NumberFormatException e) {
    236             String msg = "Integer required for " + key + " parameter but value '" + valueString +
    237                     "' was found instead.";
    238             throw new IllegalArgumentException(msg, e);
    239         }
    240     }
    241 
    242     @Override
    243     public String getType(Uri uri) {
    244         int match = sURIMatcher.match(uri);
    245         switch (match) {
    246             case CALLS:
    247                 return Calls.CONTENT_TYPE;
    248             case CALLS_ID:
    249                 return Calls.CONTENT_ITEM_TYPE;
    250             case CALLS_FILTER:
    251                 return Calls.CONTENT_TYPE;
    252             default:
    253                 throw new IllegalArgumentException("Unknown URI: " + uri);
    254         }
    255     }
    256 
    257     @Override
    258     public Uri insert(Uri uri, ContentValues values) {
    259         checkForSupportedColumns(sCallsProjectionMap, values);
    260         // Inserting a voicemail record through call_log requires the voicemail
    261         // permission and also requires the additional voicemail param set.
    262         if (hasVoicemailValue(values)) {
    263             checkIsAllowVoicemailRequest(uri);
    264             mVoicemailPermissions.checkCallerHasWriteAccess();
    265         }
    266         if (mCallsInserter == null) {
    267             SQLiteDatabase db = mDbHelper.getWritableDatabase();
    268             mCallsInserter = new DatabaseUtils.InsertHelper(db, Tables.CALLS);
    269         }
    270 
    271         ContentValues copiedValues = new ContentValues(values);
    272 
    273         // Add the computed fields to the copied values.
    274         mCallLogInsertionHelper.addComputedValues(copiedValues);
    275 
    276         long rowId = getDatabaseModifier(mCallsInserter).insert(copiedValues);
    277         if (rowId > 0) {
    278             return ContentUris.withAppendedId(uri, rowId);
    279         }
    280         return null;
    281     }
    282 
    283     @Override
    284     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    285         checkForSupportedColumns(sCallsProjectionMap, values);
    286         // Request that involves changing record type to voicemail requires the
    287         // voicemail param set in the uri.
    288         if (hasVoicemailValue(values)) {
    289             checkIsAllowVoicemailRequest(uri);
    290         }
    291 
    292         SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
    293         checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, false /*isQuery*/);
    294 
    295         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
    296         final int matchedUriId = sURIMatcher.match(uri);
    297         switch (matchedUriId) {
    298             case CALLS:
    299                 break;
    300 
    301             case CALLS_ID:
    302                 selectionBuilder.addClause(getEqualityClause(Calls._ID, parseCallIdFromUri(uri)));
    303                 break;
    304 
    305             default:
    306                 throw new UnsupportedOperationException("Cannot update URL: " + uri);
    307         }
    308 
    309         return getDatabaseModifier(db).update(Tables.CALLS, values, selectionBuilder.build(),
    310                 selectionArgs);
    311     }
    312 
    313     @Override
    314     public int delete(Uri uri, String selection, String[] selectionArgs) {
    315         SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
    316         checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, false /*isQuery*/);
    317 
    318         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
    319         final int matchedUriId = sURIMatcher.match(uri);
    320         switch (matchedUriId) {
    321             case CALLS:
    322                 return getDatabaseModifier(db).delete(Tables.CALLS,
    323                         selectionBuilder.build(), selectionArgs);
    324             default:
    325                 throw new UnsupportedOperationException("Cannot delete that URL: " + uri);
    326         }
    327     }
    328 
    329     // Work around to let the test code override the context. getContext() is final so cannot be
    330     // overridden.
    331     protected Context context() {
    332         return getContext();
    333     }
    334 
    335     /**
    336      * Returns a {@link DatabaseModifier} that takes care of sending necessary notifications
    337      * after the operation is performed.
    338      */
    339     private DatabaseModifier getDatabaseModifier(SQLiteDatabase db) {
    340         return new DbModifierWithNotification(Tables.CALLS, db, context());
    341     }
    342 
    343     /**
    344      * Same as {@link #getDatabaseModifier(SQLiteDatabase)} but used for insert helper operations
    345      * only.
    346      */
    347     private DatabaseModifier getDatabaseModifier(DatabaseUtils.InsertHelper insertHelper) {
    348         return new DbModifierWithNotification(Tables.CALLS, insertHelper, context());
    349     }
    350 
    351     private static final Integer VOICEMAIL_TYPE = new Integer(Calls.VOICEMAIL_TYPE);
    352     private boolean hasVoicemailValue(ContentValues values) {
    353         return VOICEMAIL_TYPE.equals(values.getAsInteger(Calls.TYPE));
    354     }
    355 
    356     /**
    357      * Checks if the supplied uri requests to include voicemails and take appropriate
    358      * action.
    359      * <p> If voicemail is requested, then check for voicemail permissions. Otherwise
    360      * modify the selection to restrict to non-voicemail entries only.
    361      */
    362     private void checkVoicemailPermissionAndAddRestriction(Uri uri,
    363             SelectionBuilder selectionBuilder, boolean isQuery) {
    364         if (isAllowVoicemailRequest(uri)) {
    365             if (isQuery) {
    366                 mVoicemailPermissions.checkCallerHasReadAccess();
    367             } else {
    368                 mVoicemailPermissions.checkCallerHasWriteAccess();
    369             }
    370         } else {
    371             selectionBuilder.addClause(EXCLUDE_VOICEMAIL_SELECTION);
    372         }
    373     }
    374 
    375     /**
    376      * Determines if the supplied uri has the request to allow voicemails to be
    377      * included.
    378      */
    379     private boolean isAllowVoicemailRequest(Uri uri) {
    380         return uri.getBooleanQueryParameter(Calls.ALLOW_VOICEMAILS_PARAM_KEY, false);
    381     }
    382 
    383     /**
    384      * Checks to ensure that the given uri has allow_voicemail set. Used by
    385      * insert and update operations to check that ContentValues with voicemail
    386      * call type must use the voicemail uri.
    387      * @throws IllegalArgumentException if allow_voicemail is not set.
    388      */
    389     private void checkIsAllowVoicemailRequest(Uri uri) {
    390         if (!isAllowVoicemailRequest(uri)) {
    391             throw new IllegalArgumentException(
    392                     String.format("Uri %s cannot be used for voicemail record." +
    393                             " Please set '%s=true' in the uri.", uri,
    394                             Calls.ALLOW_VOICEMAILS_PARAM_KEY));
    395         }
    396     }
    397 
    398    /**
    399     * Parses the call Id from the given uri, assuming that this is a uri that
    400     * matches CALLS_ID. For other uri types the behaviour is undefined.
    401     * @throws IllegalArgumentException if the id included in the Uri is not a valid long value.
    402     */
    403     private long parseCallIdFromUri(Uri uri) {
    404         try {
    405             return Long.parseLong(uri.getPathSegments().get(1));
    406         } catch (NumberFormatException e) {
    407             throw new IllegalArgumentException("Invalid call id in uri: " + uri, e);
    408         }
    409     }
    410 
    411     /**
    412      * Syncs any unique call log entries that have been inserted into the primary user's call log
    413      * since the last time the last sync occurred.
    414      */
    415     private void syncEntriesFromPrimaryUser(UserManager userManager) {
    416         final int userHandle = userManager.getUserHandle();
    417         if (userHandle == UserHandle.USER_OWNER
    418                 || userManager.getUserInfo(userHandle).isManagedProfile()) {
    419             return;
    420         }
    421 
    422         final long lastSyncTime = getLastSyncTime();
    423         final Uri uri = ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI,
    424                 UserHandle.USER_OWNER);
    425         final Cursor cursor = getContext().getContentResolver().query(
    426                 uri,
    427                 CALL_LOG_SYNC_PROJECTION,
    428                 EXCLUDE_VOICEMAIL_SELECTION + " AND " + MORE_RECENT_THAN_SELECTION,
    429                 new String[] {String.valueOf(lastSyncTime)},
    430                 Calls.DATE + " DESC");
    431         if (cursor == null) {
    432             return;
    433         }
    434         try {
    435             final long lastSyncedEntryTime = copyEntriesFromCursor(cursor);
    436             if (lastSyncedEntryTime > lastSyncTime) {
    437                 setLastTimeSynced(lastSyncedEntryTime);
    438             }
    439         } finally {
    440             cursor.close();
    441         }
    442     }
    443 
    444     /**
    445      * @param cursor to copy call log entries from
    446      *
    447      * @return the timestamp of the last synced entry.
    448      */
    449     @VisibleForTesting
    450     long copyEntriesFromCursor(Cursor cursor) {
    451         long lastSynced = 0;
    452         final ContentValues values = new ContentValues();
    453         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
    454         db.beginTransaction();
    455         try {
    456             final String[] args = new String[2];
    457             cursor.moveToPosition(-1);
    458             while (cursor.moveToNext()) {
    459                 values.clear();
    460                 DatabaseUtils.cursorRowToContentValues(cursor, values);
    461                 final String startTime = values.getAsString(Calls.DATE);
    462                 final String number = values.getAsString(Calls.NUMBER);
    463 
    464                 if (startTime == null || number == null) {
    465                     continue;
    466                 }
    467 
    468                 if (cursor.isLast()) {
    469                     try {
    470                         lastSynced = Long.valueOf(startTime);
    471                     } catch (NumberFormatException e) {
    472                         Log.e(TAG, "Call log entry does not contain valid start time: "
    473                                 + startTime);
    474                     }
    475                 }
    476 
    477                 // Avoid duplicating an already existing entry (which is uniquely identified by
    478                 // the number, and the start time)
    479                 args[0] = startTime;
    480                 args[1] = number;
    481                 if (DatabaseUtils.queryNumEntries(db, Tables.CALLS,
    482                         Calls.DATE + " = ? AND " + Calls.NUMBER + " = ?", args) > 0) {
    483                     continue;
    484                 }
    485 
    486                 db.insert(Tables.CALLS, null, values);
    487             }
    488             db.setTransactionSuccessful();
    489         } finally {
    490             db.endTransaction();
    491         }
    492         return lastSynced;
    493     }
    494 
    495     private long getLastSyncTime() {
    496         try {
    497             return Long.valueOf(mDbHelper.getProperty(DbProperties.CALL_LOG_LAST_SYNCED, "0"));
    498         } catch (NumberFormatException e) {
    499             return 0;
    500         }
    501     }
    502 
    503     private void setLastTimeSynced(long time) {
    504         mDbHelper.setProperty(DbProperties.CALL_LOG_LAST_SYNCED, String.valueOf(time));
    505     }
    506 }
    507