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.content.ContentProvider; 24import android.content.ContentUris; 25import android.content.ContentValues; 26import android.content.Context; 27import android.content.UriMatcher; 28import android.database.Cursor; 29import android.database.DatabaseUtils; 30import android.database.sqlite.SQLiteDatabase; 31import android.database.sqlite.SQLiteQueryBuilder; 32import android.net.Uri; 33import android.provider.CallLog; 34import android.provider.CallLog.Calls; 35import android.util.Log; 36 37import com.android.providers.contacts.ContactsDatabaseHelper.Tables; 38import com.android.providers.contacts.util.SelectionBuilder; 39import com.google.common.annotations.VisibleForTesting; 40 41import java.util.HashMap; 42 43/** 44 * Call log content provider. 45 */ 46public 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, 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 { 59 sURIMatcher.addURI(CallLog.AUTHORITY, "calls", CALLS); 60 sURIMatcher.addURI(CallLog.AUTHORITY, "calls/#", CALLS_ID); 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>(); 69 sCallsProjectionMap.put(Calls._ID, Calls._ID); 70 sCallsProjectionMap.put(Calls.NUMBER, Calls.NUMBER); 71 sCallsProjectionMap.put(Calls.DATE, Calls.DATE); 72 sCallsProjectionMap.put(Calls.DURATION, Calls.DURATION); 73 sCallsProjectionMap.put(Calls.TYPE, Calls.TYPE); 74 sCallsProjectionMap.put(Calls.NEW, Calls.NEW); 75 sCallsProjectionMap.put(Calls.VOICEMAIL_URI, Calls.VOICEMAIL_URI); 76 sCallsProjectionMap.put(Calls.IS_READ, Calls.IS_READ); 77 sCallsProjectionMap.put(Calls.CACHED_NAME, Calls.CACHED_NAME); 78 sCallsProjectionMap.put(Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_TYPE); 79 sCallsProjectionMap.put(Calls.CACHED_NUMBER_LABEL, Calls.CACHED_NUMBER_LABEL); 80 sCallsProjectionMap.put(Calls.COUNTRY_ISO, Calls.COUNTRY_ISO); 81 sCallsProjectionMap.put(Calls.GEOCODED_LOCATION, Calls.GEOCODED_LOCATION); 82 sCallsProjectionMap.put(Calls.CACHED_LOOKUP_URI, Calls.CACHED_LOOKUP_URI); 83 sCallsProjectionMap.put(Calls.CACHED_MATCHED_NUMBER, Calls.CACHED_MATCHED_NUMBER); 84 sCallsProjectionMap.put(Calls.CACHED_NORMALIZED_NUMBER, Calls.CACHED_NORMALIZED_NUMBER); 85 sCallsProjectionMap.put(Calls.CACHED_PHOTO_ID, Calls.CACHED_PHOTO_ID); 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 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 114 protected CallLogInsertionHelper createCallLogInsertionHelper(final Context context) { 115 return DefaultCallLogInsertionHelper.getInstance(context); 116 } 117 118 @VisibleForTesting 119 protected ContactsDatabaseHelper getDatabaseHelper(final Context context) { 120 return ContactsDatabaseHelper.getInstance(context); 121 } 122 123 @Override 124 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 125 String sortOrder) { 126 final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 127 qb.setTables(Tables.CALLS); 128 qb.setProjectionMap(sCallsProjectionMap); 129 qb.setStrict(true); 130 131 final SelectionBuilder selectionBuilder = new SelectionBuilder(selection); 132 checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder); 133 134 final 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 int limit = getIntParam(uri, Calls.LIMIT_PARAM_KEY, 0); 158 final int offset = getIntParam(uri, Calls.OFFSET_PARAM_KEY, 0); 159 String limitClause = null; 160 if (limit > 0) { 161 limitClause = offset + "," + limit; 162 } 163 164 final SQLiteDatabase db = mDbHelper.getReadableDatabase(); 165 final Cursor c = qb.query(db, projection, selectionBuilder.build(), selectionArgs, null, 166 null, sortOrder, limitClause); 167 if (c != null) { 168 c.setNotificationUri(getContext().getContentResolver(), CallLog.CONTENT_URI); 169 } 170 return c; 171 } 172 173 /** 174 * Gets an integer query parameter from a given uri. 175 * 176 * @param uri The uri to extract the query parameter from. 177 * @param key The query parameter key. 178 * @param defaultValue A default value to return if the query parameter does not exist. 179 * @return The value from the query parameter in the Uri. Or the default value if the parameter 180 * does not exist in the uri. 181 * @throws IllegalArgumentException when the value in the query parameter is not an integer. 182 */ 183 private int getIntParam(Uri uri, String key, int defaultValue) { 184 String valueString = uri.getQueryParameter(key); 185 if (valueString == null) { 186 return defaultValue; 187 } 188 189 try { 190 return Integer.parseInt(valueString); 191 } catch (NumberFormatException e) { 192 String msg = "Integer required for " + key + " parameter but value '" + valueString + 193 "' was found instead."; 194 throw new IllegalArgumentException(msg, e); 195 } 196 } 197 198 @Override 199 public String getType(Uri uri) { 200 int match = sURIMatcher.match(uri); 201 switch (match) { 202 case CALLS: 203 return Calls.CONTENT_TYPE; 204 case CALLS_ID: 205 return Calls.CONTENT_ITEM_TYPE; 206 case CALLS_FILTER: 207 return Calls.CONTENT_TYPE; 208 default: 209 throw new IllegalArgumentException("Unknown URI: " + uri); 210 } 211 } 212 213 @Override 214 public Uri insert(Uri uri, ContentValues values) { 215 checkForSupportedColumns(sCallsProjectionMap, values); 216 // Inserting a voicemail record through call_log requires the voicemail 217 // permission and also requires the additional voicemail param set. 218 if (hasVoicemailValue(values)) { 219 checkIsAllowVoicemailRequest(uri); 220 mVoicemailPermissions.checkCallerHasFullAccess(); 221 } 222 if (mCallsInserter == null) { 223 SQLiteDatabase db = mDbHelper.getWritableDatabase(); 224 mCallsInserter = new DatabaseUtils.InsertHelper(db, Tables.CALLS); 225 } 226 227 ContentValues copiedValues = new ContentValues(values); 228 229 // Add the computed fields to the copied values. 230 mCallLogInsertionHelper.addComputedValues(copiedValues); 231 232 long rowId = getDatabaseModifier(mCallsInserter).insert(copiedValues); 233 if (rowId > 0) { 234 return ContentUris.withAppendedId(uri, rowId); 235 } 236 return null; 237 } 238 239 @Override 240 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 241 checkForSupportedColumns(sCallsProjectionMap, values); 242 // Request that involves changing record type to voicemail requires the 243 // voicemail param set in the uri. 244 if (hasVoicemailValue(values)) { 245 checkIsAllowVoicemailRequest(uri); 246 } 247 248 SelectionBuilder selectionBuilder = new SelectionBuilder(selection); 249 checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder); 250 251 final SQLiteDatabase db = mDbHelper.getWritableDatabase(); 252 final int matchedUriId = sURIMatcher.match(uri); 253 switch (matchedUriId) { 254 case CALLS: 255 break; 256 257 case CALLS_ID: 258 selectionBuilder.addClause(getEqualityClause(Calls._ID, parseCallIdFromUri(uri))); 259 break; 260 261 default: 262 throw new UnsupportedOperationException("Cannot update URL: " + uri); 263 } 264 265 return getDatabaseModifier(db).update(Tables.CALLS, values, selectionBuilder.build(), 266 selectionArgs); 267 } 268 269 @Override 270 public int delete(Uri uri, String selection, String[] selectionArgs) { 271 SelectionBuilder selectionBuilder = new SelectionBuilder(selection); 272 checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder); 273 274 final SQLiteDatabase db = mDbHelper.getWritableDatabase(); 275 final int matchedUriId = sURIMatcher.match(uri); 276 switch (matchedUriId) { 277 case CALLS: 278 return getDatabaseModifier(db).delete(Tables.CALLS, 279 selectionBuilder.build(), selectionArgs); 280 default: 281 throw new UnsupportedOperationException("Cannot delete that URL: " + uri); 282 } 283 } 284 285 // Work around to let the test code override the context. getContext() is final so cannot be 286 // overridden. 287 protected Context context() { 288 return getContext(); 289 } 290 291 /** 292 * Returns a {@link DatabaseModifier} that takes care of sending necessary notifications 293 * after the operation is performed. 294 */ 295 private DatabaseModifier getDatabaseModifier(SQLiteDatabase db) { 296 return new DbModifierWithNotification(Tables.CALLS, db, context()); 297 } 298 299 /** 300 * Same as {@link #getDatabaseModifier(SQLiteDatabase)} but used for insert helper operations 301 * only. 302 */ 303 private DatabaseModifier getDatabaseModifier(DatabaseUtils.InsertHelper insertHelper) { 304 return new DbModifierWithNotification(Tables.CALLS, insertHelper, context()); 305 } 306 307 private boolean hasVoicemailValue(ContentValues values) { 308 return values.containsKey(Calls.TYPE) && 309 values.getAsInteger(Calls.TYPE).equals(Calls.VOICEMAIL_TYPE); 310 } 311 312 /** 313 * Checks if the supplied uri requests to include voicemails and take appropriate 314 * action. 315 * <p> If voicemail is requested, then check for voicemail permissions. Otherwise 316 * modify the selection to restrict to non-voicemail entries only. 317 */ 318 private void checkVoicemailPermissionAndAddRestriction(Uri uri, 319 SelectionBuilder selectionBuilder) { 320 if (isAllowVoicemailRequest(uri)) { 321 mVoicemailPermissions.checkCallerHasFullAccess(); 322 } else { 323 selectionBuilder.addClause(EXCLUDE_VOICEMAIL_SELECTION); 324 } 325 } 326 327 /** 328 * Determines if the supplied uri has the request to allow voicemails to be 329 * included. 330 */ 331 private boolean isAllowVoicemailRequest(Uri uri) { 332 return uri.getBooleanQueryParameter(Calls.ALLOW_VOICEMAILS_PARAM_KEY, false); 333 } 334 335 /** 336 * Checks to ensure that the given uri has allow_voicemail set. Used by 337 * insert and update operations to check that ContentValues with voicemail 338 * call type must use the voicemail uri. 339 * @throws IllegalArgumentException if allow_voicemail is not set. 340 */ 341 private void checkIsAllowVoicemailRequest(Uri uri) { 342 if (!isAllowVoicemailRequest(uri)) { 343 throw new IllegalArgumentException( 344 String.format("Uri %s cannot be used for voicemail record." + 345 " Please set '%s=true' in the uri.", uri, 346 Calls.ALLOW_VOICEMAILS_PARAM_KEY)); 347 } 348 } 349 350 /** 351 * Parses the call Id from the given uri, assuming that this is a uri that 352 * matches CALLS_ID. For other uri types the behaviour is undefined. 353 * @throws IllegalArgumentException if the id included in the Uri is not a valid long value. 354 */ 355 private long parseCallIdFromUri(Uri uri) { 356 try { 357 return Long.parseLong(uri.getPathSegments().get(1)); 358 } catch (NumberFormatException e) { 359 throw new IllegalArgumentException("Invalid call id in uri: " + uri, e); 360 } 361 } 362} 363