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