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