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 com.android.providers.contacts.ContactsDatabaseHelper.Tables; 24 import com.android.providers.contacts.util.SelectionBuilder; 25 import com.google.common.annotations.VisibleForTesting; 26 27 import android.content.ContentProvider; 28 import android.content.ContentUris; 29 import android.content.ContentValues; 30 import android.content.Context; 31 import android.content.UriMatcher; 32 import android.database.Cursor; 33 import android.database.DatabaseUtils; 34 import android.database.sqlite.SQLiteDatabase; 35 import android.database.sqlite.SQLiteQueryBuilder; 36 import android.net.Uri; 37 import android.provider.CallLog; 38 import android.provider.CallLog.Calls; 39 import android.util.Log; 40 41 import java.util.HashMap; 42 43 /** 44 * Call log content provider. 45 */ 46 public class CallLogProvider extends ContentProvider { 47 /** Selection clause to use to exclude voicemail records. */ 48 private static final String EXCLUDE_VOICEMAIL_SELECTION = getInequalityClause( 49 Calls.TYPE, Integer.toString(Calls.VOICEMAIL_TYPE)); 50 51 private static final int CALLS = 1; 52 53 private static final int CALLS_ID = 2; 54 55 private static final int CALLS_FILTER = 3; 56 57 private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); 58 static { sURIMatcher.addURI(CallLog.AUTHORITY, "calls", CALLS)59 sURIMatcher.addURI(CallLog.AUTHORITY, "calls", CALLS); sURIMatcher.addURI(CallLog.AUTHORITY, "calls/#", CALLS_ID)60 sURIMatcher.addURI(CallLog.AUTHORITY, "calls/#", CALLS_ID); sURIMatcher.addURI(CallLog.AUTHORITY, "calls/filter/*", CALLS_FILTER)61 sURIMatcher.addURI(CallLog.AUTHORITY, "calls/filter/*", CALLS_FILTER); 62 } 63 64 private static final HashMap<String, String> sCallsProjectionMap; 65 static { 66 67 // Calls projection map 68 sCallsProjectionMap = new HashMap<String, String>(); sCallsProjectionMap.put(Calls._ID, Calls._ID)69 sCallsProjectionMap.put(Calls._ID, Calls._ID); sCallsProjectionMap.put(Calls.NUMBER, Calls.NUMBER)70 sCallsProjectionMap.put(Calls.NUMBER, Calls.NUMBER); sCallsProjectionMap.put(Calls.DATE, Calls.DATE)71 sCallsProjectionMap.put(Calls.DATE, Calls.DATE); sCallsProjectionMap.put(Calls.DURATION, Calls.DURATION)72 sCallsProjectionMap.put(Calls.DURATION, Calls.DURATION); sCallsProjectionMap.put(Calls.TYPE, Calls.TYPE)73 sCallsProjectionMap.put(Calls.TYPE, Calls.TYPE); sCallsProjectionMap.put(Calls.NEW, Calls.NEW)74 sCallsProjectionMap.put(Calls.NEW, Calls.NEW); sCallsProjectionMap.put(Calls.VOICEMAIL_URI, Calls.VOICEMAIL_URI)75 sCallsProjectionMap.put(Calls.VOICEMAIL_URI, Calls.VOICEMAIL_URI); sCallsProjectionMap.put(Calls.IS_READ, Calls.IS_READ)76 sCallsProjectionMap.put(Calls.IS_READ, Calls.IS_READ); sCallsProjectionMap.put(Calls.CACHED_NAME, Calls.CACHED_NAME)77 sCallsProjectionMap.put(Calls.CACHED_NAME, Calls.CACHED_NAME); sCallsProjectionMap.put(Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_TYPE)78 sCallsProjectionMap.put(Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_TYPE); sCallsProjectionMap.put(Calls.CACHED_NUMBER_LABEL, Calls.CACHED_NUMBER_LABEL)79 sCallsProjectionMap.put(Calls.CACHED_NUMBER_LABEL, Calls.CACHED_NUMBER_LABEL); sCallsProjectionMap.put(Calls.COUNTRY_ISO, Calls.COUNTRY_ISO)80 sCallsProjectionMap.put(Calls.COUNTRY_ISO, Calls.COUNTRY_ISO); sCallsProjectionMap.put(Calls.GEOCODED_LOCATION, Calls.GEOCODED_LOCATION)81 sCallsProjectionMap.put(Calls.GEOCODED_LOCATION, Calls.GEOCODED_LOCATION); sCallsProjectionMap.put(Calls.CACHED_LOOKUP_URI, Calls.CACHED_LOOKUP_URI)82 sCallsProjectionMap.put(Calls.CACHED_LOOKUP_URI, Calls.CACHED_LOOKUP_URI); sCallsProjectionMap.put(Calls.CACHED_MATCHED_NUMBER, Calls.CACHED_MATCHED_NUMBER)83 sCallsProjectionMap.put(Calls.CACHED_MATCHED_NUMBER, Calls.CACHED_MATCHED_NUMBER); sCallsProjectionMap.put(Calls.CACHED_NORMALIZED_NUMBER, Calls.CACHED_NORMALIZED_NUMBER)84 sCallsProjectionMap.put(Calls.CACHED_NORMALIZED_NUMBER, Calls.CACHED_NORMALIZED_NUMBER); sCallsProjectionMap.put(Calls.CACHED_PHOTO_ID, Calls.CACHED_PHOTO_ID)85 sCallsProjectionMap.put(Calls.CACHED_PHOTO_ID, Calls.CACHED_PHOTO_ID); sCallsProjectionMap.put(Calls.CACHED_FORMATTED_NUMBER, Calls.CACHED_FORMATTED_NUMBER)86 sCallsProjectionMap.put(Calls.CACHED_FORMATTED_NUMBER, Calls.CACHED_FORMATTED_NUMBER); 87 } 88 89 private ContactsDatabaseHelper mDbHelper; 90 private DatabaseUtils.InsertHelper mCallsInserter; 91 private boolean mUseStrictPhoneNumberComparation; 92 private VoicemailPermissions mVoicemailPermissions; 93 private CallLogInsertionHelper mCallLogInsertionHelper; 94 95 @Override onCreate()96 public boolean onCreate() { 97 if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) { 98 Log.d(Constants.PERFORMANCE_TAG, "CallLogProvider.onCreate start"); 99 } 100 final Context context = getContext(); 101 mDbHelper = getDatabaseHelper(context); 102 mUseStrictPhoneNumberComparation = 103 context.getResources().getBoolean( 104 com.android.internal.R.bool.config_use_strict_phone_number_comparation); 105 mVoicemailPermissions = new VoicemailPermissions(context); 106 mCallLogInsertionHelper = createCallLogInsertionHelper(context); 107 if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) { 108 Log.d(Constants.PERFORMANCE_TAG, "CallLogProvider.onCreate finish"); 109 } 110 return true; 111 } 112 113 @VisibleForTesting createCallLogInsertionHelper(final Context context)114 protected CallLogInsertionHelper createCallLogInsertionHelper(final Context context) { 115 return DefaultCallLogInsertionHelper.getInstance(context); 116 } 117 118 @VisibleForTesting getDatabaseHelper(final Context context)119 protected ContactsDatabaseHelper getDatabaseHelper(final Context context) { 120 return ContactsDatabaseHelper.getInstance(context); 121 } 122 123 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)124 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 125 String sortOrder) { 126 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 127 qb.setTables(Tables.CALLS); 128 qb.setProjectionMap(sCallsProjectionMap); 129 qb.setStrict(true); 130 131 SelectionBuilder selectionBuilder = new SelectionBuilder(selection); 132 checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder); 133 134 int match = sURIMatcher.match(uri); 135 switch (match) { 136 case CALLS: 137 break; 138 139 case CALLS_ID: { 140 selectionBuilder.addClause(getEqualityClause(Calls._ID, 141 parseCallIdFromUri(uri))); 142 break; 143 } 144 145 case CALLS_FILTER: { 146 String phoneNumber = uri.getPathSegments().get(2); 147 qb.appendWhere("PHONE_NUMBERS_EQUAL(number, "); 148 qb.appendWhereEscapeString(phoneNumber); 149 qb.appendWhere(mUseStrictPhoneNumberComparation ? ", 1)" : ", 0)"); 150 break; 151 } 152 153 default: 154 throw new IllegalArgumentException("Unknown URL " + uri); 155 } 156 157 final SQLiteDatabase db = mDbHelper.getReadableDatabase(); 158 Cursor c = qb.query(db, projection, selectionBuilder.build(), selectionArgs, null, null, 159 sortOrder, null); 160 if (c != null) { 161 c.setNotificationUri(getContext().getContentResolver(), CallLog.CONTENT_URI); 162 } 163 return c; 164 } 165 166 @Override getType(Uri uri)167 public String getType(Uri uri) { 168 int match = sURIMatcher.match(uri); 169 switch (match) { 170 case CALLS: 171 return Calls.CONTENT_TYPE; 172 case CALLS_ID: 173 return Calls.CONTENT_ITEM_TYPE; 174 case CALLS_FILTER: 175 return Calls.CONTENT_TYPE; 176 default: 177 throw new IllegalArgumentException("Unknown URI: " + uri); 178 } 179 } 180 181 @Override insert(Uri uri, ContentValues values)182 public Uri insert(Uri uri, ContentValues values) { 183 checkForSupportedColumns(sCallsProjectionMap, values); 184 // Inserting a voicemail record through call_log requires the voicemail 185 // permission and also requires the additional voicemail param set. 186 if (hasVoicemailValue(values)) { 187 checkIsAllowVoicemailRequest(uri); 188 mVoicemailPermissions.checkCallerHasFullAccess(); 189 } 190 if (mCallsInserter == null) { 191 SQLiteDatabase db = mDbHelper.getWritableDatabase(); 192 mCallsInserter = new DatabaseUtils.InsertHelper(db, Tables.CALLS); 193 } 194 195 ContentValues copiedValues = new ContentValues(values); 196 197 // Add the computed fields to the copied values. 198 mCallLogInsertionHelper.addComputedValues(copiedValues); 199 200 long rowId = getDatabaseModifier(mCallsInserter).insert(copiedValues); 201 if (rowId > 0) { 202 return ContentUris.withAppendedId(uri, rowId); 203 } 204 return null; 205 } 206 207 @Override update(Uri uri, ContentValues values, String selection, String[] selectionArgs)208 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 209 checkForSupportedColumns(sCallsProjectionMap, values); 210 // Request that involves changing record type to voicemail requires the 211 // voicemail param set in the uri. 212 if (hasVoicemailValue(values)) { 213 checkIsAllowVoicemailRequest(uri); 214 } 215 216 SelectionBuilder selectionBuilder = new SelectionBuilder(selection); 217 checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder); 218 219 final SQLiteDatabase db = mDbHelper.getWritableDatabase(); 220 final int matchedUriId = sURIMatcher.match(uri); 221 switch (matchedUriId) { 222 case CALLS: 223 break; 224 225 case CALLS_ID: 226 selectionBuilder.addClause(getEqualityClause(Calls._ID, parseCallIdFromUri(uri))); 227 break; 228 229 default: 230 throw new UnsupportedOperationException("Cannot update URL: " + uri); 231 } 232 233 return getDatabaseModifier(db).update(Tables.CALLS, values, selectionBuilder.build(), 234 selectionArgs); 235 } 236 237 @Override delete(Uri uri, String selection, String[] selectionArgs)238 public int delete(Uri uri, String selection, String[] selectionArgs) { 239 SelectionBuilder selectionBuilder = new SelectionBuilder(selection); 240 checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder); 241 242 final SQLiteDatabase db = mDbHelper.getWritableDatabase(); 243 final int matchedUriId = sURIMatcher.match(uri); 244 switch (matchedUriId) { 245 case CALLS: 246 return getDatabaseModifier(db).delete(Tables.CALLS, 247 selectionBuilder.build(), selectionArgs); 248 default: 249 throw new UnsupportedOperationException("Cannot delete that URL: " + uri); 250 } 251 } 252 253 // Work around to let the test code override the context. getContext() is final so cannot be 254 // overridden. context()255 protected Context context() { 256 return getContext(); 257 } 258 259 /** 260 * Returns a {@link DatabaseModifier} that takes care of sending necessary notifications 261 * after the operation is performed. 262 */ getDatabaseModifier(SQLiteDatabase db)263 private DatabaseModifier getDatabaseModifier(SQLiteDatabase db) { 264 return new DbModifierWithNotification(Tables.CALLS, db, context()); 265 } 266 267 /** 268 * Same as {@link #getDatabaseModifier(SQLiteDatabase)} but used for insert helper operations 269 * only. 270 */ getDatabaseModifier(DatabaseUtils.InsertHelper insertHelper)271 private DatabaseModifier getDatabaseModifier(DatabaseUtils.InsertHelper insertHelper) { 272 return new DbModifierWithNotification(Tables.CALLS, insertHelper, context()); 273 } 274 hasVoicemailValue(ContentValues values)275 private boolean hasVoicemailValue(ContentValues values) { 276 return values.containsKey(Calls.TYPE) && 277 values.getAsInteger(Calls.TYPE).equals(Calls.VOICEMAIL_TYPE); 278 } 279 280 /** 281 * Checks if the supplied uri requests to include voicemails and take appropriate 282 * action. 283 * <p> If voicemail is requested, then check for voicemail permissions. Otherwise 284 * modify the selection to restrict to non-voicemail entries only. 285 */ checkVoicemailPermissionAndAddRestriction(Uri uri, SelectionBuilder selectionBuilder)286 private void checkVoicemailPermissionAndAddRestriction(Uri uri, 287 SelectionBuilder selectionBuilder) { 288 if (isAllowVoicemailRequest(uri)) { 289 mVoicemailPermissions.checkCallerHasFullAccess(); 290 } else { 291 selectionBuilder.addClause(EXCLUDE_VOICEMAIL_SELECTION); 292 } 293 } 294 295 /** 296 * Determines if the supplied uri has the request to allow voicemails to be 297 * included. 298 */ isAllowVoicemailRequest(Uri uri)299 private boolean isAllowVoicemailRequest(Uri uri) { 300 return uri.getBooleanQueryParameter(Calls.ALLOW_VOICEMAILS_PARAM_KEY, false); 301 } 302 303 /** 304 * Checks to ensure that the given uri has allow_voicemail set. Used by 305 * insert and update operations to check that ContentValues with voicemail 306 * call type must use the voicemail uri. 307 * @throws IllegalArgumentException if allow_voicemail is not set. 308 */ checkIsAllowVoicemailRequest(Uri uri)309 private void checkIsAllowVoicemailRequest(Uri uri) { 310 if (!isAllowVoicemailRequest(uri)) { 311 throw new IllegalArgumentException( 312 String.format("Uri %s cannot be used for voicemail record." + 313 " Please set '%s=true' in the uri.", uri, 314 Calls.ALLOW_VOICEMAILS_PARAM_KEY)); 315 } 316 } 317 318 /** 319 * Parses the call Id from the given uri, assuming that this is a uri that 320 * matches CALLS_ID. For other uri types the behaviour is undefined. 321 * @throws IllegalArgumentException if the id included in the Uri is not a valid long value. 322 */ parseCallIdFromUri(Uri uri)323 private String parseCallIdFromUri(Uri uri) { 324 try { 325 Long id = Long.valueOf(uri.getPathSegments().get(1)); 326 return id.toString(); 327 } catch (NumberFormatException e) { 328 throw new IllegalArgumentException("Invalid call id in uri: " + uri, e); 329 } 330 } 331 } 332