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