1 /* 2 * Copyright (C) 2017 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.dialer.calllog.database; 18 19 import android.annotation.TargetApi; 20 import android.content.ContentProvider; 21 import android.content.ContentProviderOperation; 22 import android.content.ContentProviderResult; 23 import android.content.ContentUris; 24 import android.content.ContentValues; 25 import android.content.OperationApplicationException; 26 import android.content.UriMatcher; 27 import android.database.Cursor; 28 import android.database.sqlite.SQLiteDatabase; 29 import android.database.sqlite.SQLiteQueryBuilder; 30 import android.net.Uri; 31 import android.os.Build; 32 import android.provider.CallLog.Calls; 33 import android.support.annotation.NonNull; 34 import android.support.annotation.Nullable; 35 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract; 36 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog; 37 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.CoalescedAnnotatedCallLog; 38 import com.android.dialer.common.Assert; 39 import com.android.dialer.common.LogUtil; 40 import com.android.dialer.metrics.Metrics; 41 import com.android.dialer.metrics.MetricsComponent; 42 import java.util.ArrayList; 43 import java.util.Arrays; 44 45 /** {@link ContentProvider} for the annotated call log. */ 46 public class AnnotatedCallLogContentProvider extends ContentProvider { 47 48 /** 49 * We sometimes run queries where we potentially pass every ID into a where clause using the 50 * (?,?,?,...) syntax. The maximum number of host parameters is 999, so that's the maximum size 51 * this table can be. See https://www.sqlite.org/limits.html for more details. 52 */ 53 private static final int MAX_ROWS = 999; 54 55 private static final int ANNOTATED_CALL_LOG_TABLE_CODE = 1; 56 private static final int ANNOTATED_CALL_LOG_TABLE_ID_CODE = 2; 57 private static final int ANNOTATED_CALL_LOG_TABLE_DISTINCT_NUMBER_CODE = 3; 58 private static final int COALESCED_ANNOTATED_CALL_LOG_TABLE_CODE = 4; 59 60 private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 61 62 static { uriMatcher.addURI( AnnotatedCallLogContract.AUTHORITY, AnnotatedCallLog.TABLE, ANNOTATED_CALL_LOG_TABLE_CODE)63 uriMatcher.addURI( 64 AnnotatedCallLogContract.AUTHORITY, AnnotatedCallLog.TABLE, ANNOTATED_CALL_LOG_TABLE_CODE); uriMatcher.addURI( AnnotatedCallLogContract.AUTHORITY, AnnotatedCallLog.TABLE + "/#", ANNOTATED_CALL_LOG_TABLE_ID_CODE)65 uriMatcher.addURI( 66 AnnotatedCallLogContract.AUTHORITY, 67 AnnotatedCallLog.TABLE + "/#", 68 ANNOTATED_CALL_LOG_TABLE_ID_CODE); uriMatcher.addURI( AnnotatedCallLogContract.AUTHORITY, AnnotatedCallLog.DISTINCT_PHONE_NUMBERS, ANNOTATED_CALL_LOG_TABLE_DISTINCT_NUMBER_CODE)69 uriMatcher.addURI( 70 AnnotatedCallLogContract.AUTHORITY, 71 AnnotatedCallLog.DISTINCT_PHONE_NUMBERS, 72 ANNOTATED_CALL_LOG_TABLE_DISTINCT_NUMBER_CODE); uriMatcher.addURI( AnnotatedCallLogContract.AUTHORITY, CoalescedAnnotatedCallLog.TABLE, COALESCED_ANNOTATED_CALL_LOG_TABLE_CODE)73 uriMatcher.addURI( 74 AnnotatedCallLogContract.AUTHORITY, 75 CoalescedAnnotatedCallLog.TABLE, 76 COALESCED_ANNOTATED_CALL_LOG_TABLE_CODE); 77 } 78 79 private AnnotatedCallLogDatabaseHelper databaseHelper; 80 81 private final ThreadLocal<Boolean> applyingBatch = new ThreadLocal<>(); 82 83 /** Ensures that only a single notification is generated from {@link #applyBatch(ArrayList)}. */ isApplyingBatch()84 private boolean isApplyingBatch() { 85 return applyingBatch.get() != null && applyingBatch.get(); 86 } 87 88 @Override onCreate()89 public boolean onCreate() { 90 databaseHelper = new AnnotatedCallLogDatabaseHelper(getContext(), MAX_ROWS); 91 92 // Note: As this method is called before Application#onCreate, we must *not* initialize objects 93 // that require preparation work done in Application#onCreate. 94 // One example is to avoid obtaining an instance that depends on Google's proprietary config, 95 // which is initialized in Application#onCreate. 96 97 return true; 98 } 99 100 @TargetApi(Build.VERSION_CODES.M) // Uses try-with-resources 101 @Nullable 102 @Override query( @onNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder)103 public Cursor query( 104 @NonNull Uri uri, 105 @Nullable String[] projection, 106 @Nullable String selection, 107 @Nullable String[] selectionArgs, 108 @Nullable String sortOrder) { 109 SQLiteDatabase db = databaseHelper.getReadableDatabase(); 110 SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); 111 queryBuilder.setTables(AnnotatedCallLog.TABLE); 112 int match = uriMatcher.match(uri); 113 switch (match) { 114 case ANNOTATED_CALL_LOG_TABLE_ID_CODE: 115 queryBuilder.appendWhere(AnnotatedCallLog._ID + "=" + ContentUris.parseId(uri)); 116 Cursor cursor = 117 queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder); 118 if (cursor != null) { 119 cursor.setNotificationUri( 120 getContext().getContentResolver(), AnnotatedCallLog.CONTENT_URI); 121 } else { 122 LogUtil.w("AnnotatedCallLogContentProvider.query", "cursor was null"); 123 } 124 return cursor; 125 case ANNOTATED_CALL_LOG_TABLE_CODE: 126 cursor = 127 queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder); 128 if (cursor != null) { 129 cursor.setNotificationUri( 130 getContext().getContentResolver(), AnnotatedCallLog.CONTENT_URI); 131 } else { 132 LogUtil.w("AnnotatedCallLogContentProvider.query", "cursor was null"); 133 } 134 return cursor; 135 case ANNOTATED_CALL_LOG_TABLE_DISTINCT_NUMBER_CODE: 136 Assert.checkArgument( 137 Arrays.equals(projection, new String[] {AnnotatedCallLog.NUMBER}), 138 "only NUMBER supported for projection for distinct phone number query, got: %s", 139 Arrays.toString(projection)); 140 queryBuilder.setDistinct(true); 141 cursor = 142 queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder); 143 if (cursor != null) { 144 cursor.setNotificationUri( 145 getContext().getContentResolver(), AnnotatedCallLog.CONTENT_URI); 146 } else { 147 LogUtil.w("AnnotatedCallLogContentProvider.query", "cursor was null"); 148 } 149 return cursor; 150 case COALESCED_ANNOTATED_CALL_LOG_TABLE_CODE: 151 Assert.checkArgument( 152 projection == CoalescedAnnotatedCallLog.ALL_COLUMNS, 153 "only ALL_COLUMNS projection supported for coalesced call log"); 154 Assert.checkArgument(selection == null, "selection not supported for coalesced call log"); 155 Assert.checkArgument( 156 selectionArgs == null, "selection args not supported for coalesced call log"); 157 Assert.checkArgument(sortOrder == null, "sort order not supported for coalesced call log"); 158 MetricsComponent.get(getContext()).metrics().startTimer(Metrics.NEW_CALL_LOG_COALESCE); 159 try (Cursor allAnnotatedCallLogRows = 160 queryBuilder.query( 161 db, 162 null, 163 String.format("%s != ?", CoalescedAnnotatedCallLog.CALL_TYPE), 164 new String[] {Integer.toString(Calls.VOICEMAIL_TYPE)}, 165 null, 166 null, 167 AnnotatedCallLog.TIMESTAMP + " DESC")) { 168 Cursor coalescedRows = 169 CallLogDatabaseComponent.get(getContext()) 170 .coalescer() 171 .coalesce(allAnnotatedCallLogRows); 172 coalescedRows.setNotificationUri( 173 getContext().getContentResolver(), CoalescedAnnotatedCallLog.CONTENT_URI); 174 MetricsComponent.get(getContext()).metrics().stopTimer(Metrics.NEW_CALL_LOG_COALESCE); 175 return coalescedRows; 176 } 177 default: 178 throw new IllegalArgumentException("Unknown uri: " + uri); 179 } 180 } 181 182 @Nullable 183 @Override getType(@onNull Uri uri)184 public String getType(@NonNull Uri uri) { 185 return AnnotatedCallLog.CONTENT_ITEM_TYPE; 186 } 187 188 @Nullable 189 @Override insert(@onNull Uri uri, @Nullable ContentValues values)190 public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { 191 // Javadoc states values is not nullable, even though it is annotated as such (a bug)! 192 Assert.checkArgument(values != null); 193 194 SQLiteDatabase database = databaseHelper.getWritableDatabase(); 195 int match = uriMatcher.match(uri); 196 switch (match) { 197 case ANNOTATED_CALL_LOG_TABLE_CODE: 198 Assert.checkArgument( 199 values.get(AnnotatedCallLog._ID) != null, "You must specify an _ID when inserting"); 200 break; 201 case ANNOTATED_CALL_LOG_TABLE_ID_CODE: 202 Long idFromUri = ContentUris.parseId(uri); 203 Long idFromValues = values.getAsLong(AnnotatedCallLog._ID); 204 Assert.checkArgument( 205 idFromValues == null || idFromValues.equals(idFromUri), 206 "_ID from values %d does not match ID from URI: %s", 207 idFromValues, 208 uri); 209 if (idFromValues == null) { 210 values.put(AnnotatedCallLog._ID, idFromUri); 211 } 212 break; 213 case ANNOTATED_CALL_LOG_TABLE_DISTINCT_NUMBER_CODE: 214 throw new UnsupportedOperationException(); 215 case COALESCED_ANNOTATED_CALL_LOG_TABLE_CODE: 216 throw new UnsupportedOperationException("coalesced call log does not support inserting"); 217 default: 218 throw new IllegalArgumentException("Unknown uri: " + uri); 219 } 220 long id = database.insert(AnnotatedCallLog.TABLE, null, values); 221 if (id < 0) { 222 LogUtil.w( 223 "AnnotatedCallLogContentProvider.insert", 224 "error inserting row with id: %d", 225 values.get(AnnotatedCallLog._ID)); 226 return null; 227 } 228 Uri insertedUri = ContentUris.withAppendedId(AnnotatedCallLog.CONTENT_URI, id); 229 if (!isApplyingBatch()) { 230 notifyChange(insertedUri); 231 } 232 return insertedUri; 233 } 234 235 @Override delete( @onNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs)236 public int delete( 237 @NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { 238 SQLiteDatabase database = databaseHelper.getWritableDatabase(); 239 final int match = uriMatcher.match(uri); 240 switch (match) { 241 case ANNOTATED_CALL_LOG_TABLE_CODE: 242 break; 243 case ANNOTATED_CALL_LOG_TABLE_ID_CODE: 244 Assert.checkArgument(selection == null, "Do not specify selection when deleting by ID"); 245 Assert.checkArgument( 246 selectionArgs == null, "Do not specify selection args when deleting by ID"); 247 long id = ContentUris.parseId(uri); 248 Assert.checkArgument(id != -1, "error parsing id from uri %s", uri); 249 selection = getSelectionWithId(id); 250 break; 251 case ANNOTATED_CALL_LOG_TABLE_DISTINCT_NUMBER_CODE: 252 throw new UnsupportedOperationException(); 253 case COALESCED_ANNOTATED_CALL_LOG_TABLE_CODE: 254 throw new UnsupportedOperationException("coalesced call log does not support deleting"); 255 default: 256 throw new IllegalArgumentException("Unknown uri: " + uri); 257 } 258 int rows = database.delete(AnnotatedCallLog.TABLE, selection, selectionArgs); 259 if (rows == 0) { 260 LogUtil.w("AnnotatedCallLogContentProvider.delete", "no rows deleted"); 261 return rows; 262 } 263 if (!isApplyingBatch()) { 264 notifyChange(uri); 265 } 266 return rows; 267 } 268 269 @Override update( @onNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs)270 public int update( 271 @NonNull Uri uri, 272 @Nullable ContentValues values, 273 @Nullable String selection, 274 @Nullable String[] selectionArgs) { 275 // Javadoc states values is not nullable, even though it is annotated as such (a bug)! 276 Assert.checkArgument(values != null); 277 278 SQLiteDatabase database = databaseHelper.getWritableDatabase(); 279 int match = uriMatcher.match(uri); 280 switch (match) { 281 case ANNOTATED_CALL_LOG_TABLE_CODE: 282 int rows = database.update(AnnotatedCallLog.TABLE, values, selection, selectionArgs); 283 if (rows == 0) { 284 LogUtil.w("AnnotatedCallLogContentProvider.update", "no rows updated"); 285 return rows; 286 } 287 if (!isApplyingBatch()) { 288 notifyChange(uri); 289 } 290 return rows; 291 case ANNOTATED_CALL_LOG_TABLE_ID_CODE: 292 Assert.checkArgument( 293 !values.containsKey(AnnotatedCallLog._ID), "Do not specify _ID when updating by ID"); 294 Assert.checkArgument(selection == null, "Do not specify selection when updating by ID"); 295 Assert.checkArgument( 296 selectionArgs == null, "Do not specify selection args when updating by ID"); 297 selection = getSelectionWithId(ContentUris.parseId(uri)); 298 rows = database.update(AnnotatedCallLog.TABLE, values, selection, selectionArgs); 299 if (rows == 0) { 300 LogUtil.w("AnnotatedCallLogContentProvider.update", "no rows updated"); 301 return rows; 302 } 303 if (!isApplyingBatch()) { 304 notifyChange(uri); 305 } 306 return rows; 307 case ANNOTATED_CALL_LOG_TABLE_DISTINCT_NUMBER_CODE: 308 case COALESCED_ANNOTATED_CALL_LOG_TABLE_CODE: 309 throw new UnsupportedOperationException(); 310 default: 311 throw new IllegalArgumentException("Unknown uri: " + uri); 312 } 313 } 314 315 /** 316 * {@inheritDoc} 317 * 318 * <p>Note: When applyBatch is used with the AnnotatedCallLog, only a single notification for the 319 * content URI is generated, not individual notifications for each affected URI. 320 */ 321 @NonNull 322 @Override applyBatch(@onNull ArrayList<ContentProviderOperation> operations)323 public ContentProviderResult[] applyBatch(@NonNull ArrayList<ContentProviderOperation> operations) 324 throws OperationApplicationException { 325 ContentProviderResult[] results = new ContentProviderResult[operations.size()]; 326 if (operations.isEmpty()) { 327 return results; 328 } 329 330 SQLiteDatabase database = databaseHelper.getWritableDatabase(); 331 try { 332 applyingBatch.set(true); 333 database.beginTransaction(); 334 for (int i = 0; i < operations.size(); i++) { 335 ContentProviderOperation operation = operations.get(i); 336 int match = uriMatcher.match(operation.getUri()); 337 switch (match) { 338 case ANNOTATED_CALL_LOG_TABLE_CODE: 339 case ANNOTATED_CALL_LOG_TABLE_ID_CODE: 340 // These are allowed values, continue. 341 break; 342 case ANNOTATED_CALL_LOG_TABLE_DISTINCT_NUMBER_CODE: 343 throw new UnsupportedOperationException(); 344 case COALESCED_ANNOTATED_CALL_LOG_TABLE_CODE: 345 throw new UnsupportedOperationException( 346 "coalesced call log does not support applyBatch"); 347 default: 348 throw new IllegalArgumentException("Unknown uri: " + operation.getUri()); 349 } 350 ContentProviderResult result = operation.apply(this, results, i); 351 if (operations.get(i).isInsert()) { 352 if (result.uri == null) { 353 throw new OperationApplicationException("error inserting row"); 354 } 355 } else if (result.count == 0) { 356 /* 357 * The batches built by MutationApplier happen to contain operations in order of: 358 * 359 * 1. Inserts 360 * 2. Updates 361 * 3. Deletes 362 * 363 * Let's say the last row in the table is row Z, and MutationApplier wishes to update it, 364 * as well as insert row A. When row A gets inserted, row Z will be deleted via the 365 * trigger if the table is full. Then later, when we try to process the update for row Z, 366 * it won't exist. 367 */ 368 LogUtil.w( 369 "AnnotatedCallLogContentProvider.applyBatch", 370 "update or delete failed, possibly because row got cleaned up"); 371 } 372 results[i] = result; 373 } 374 database.setTransactionSuccessful(); 375 } finally { 376 applyingBatch.set(false); 377 database.endTransaction(); 378 } 379 notifyChange(AnnotatedCallLog.CONTENT_URI); 380 return results; 381 } 382 getSelectionWithId(long id)383 private String getSelectionWithId(long id) { 384 return AnnotatedCallLog._ID + "=" + id; 385 } 386 notifyChange(Uri uri)387 private void notifyChange(Uri uri) { 388 getContext().getContentResolver().notifyChange(uri, null); 389 // Any time the annotated call log changes, we need to also notify observers of the 390 // CoalescedAnnotatedCallLog, since that is just a massaged in-memory view of the real annotated 391 // call log table. 392 getContext().getContentResolver().notifyChange(CoalescedAnnotatedCallLog.CONTENT_URI, null); 393 } 394 } 395