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.phonelookup.database; 18 19 import android.content.ContentProvider; 20 import android.content.ContentProviderOperation; 21 import android.content.ContentProviderResult; 22 import android.content.ContentValues; 23 import android.content.OperationApplicationException; 24 import android.content.UriMatcher; 25 import android.database.Cursor; 26 import android.database.DatabaseUtils; 27 import android.database.sqlite.SQLiteDatabase; 28 import android.database.sqlite.SQLiteQueryBuilder; 29 import android.net.Uri; 30 import android.support.annotation.IntDef; 31 import android.support.annotation.NonNull; 32 import android.support.annotation.Nullable; 33 import com.android.dialer.common.Assert; 34 import com.android.dialer.common.LogUtil; 35 import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract; 36 import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract.PhoneLookupHistory; 37 import java.lang.annotation.Retention; 38 import java.lang.annotation.RetentionPolicy; 39 import java.util.ArrayList; 40 import java.util.List; 41 42 /** 43 * {@link ContentProvider} for the PhoneLookupHistory. 44 * 45 * <p>Operations may run against the entire table using the URI: 46 * 47 * <pre> 48 * content://com.android.dialer.phonelookuphistory/PhoneLookupHistory 49 * </pre> 50 * 51 * <p>Or against an individual row keyed by normalized number where the number is the last component 52 * in the URI path, for example: 53 * 54 * <pre> 55 * content://com.android.dialer.phonelookuphistory/PhoneLookupHistory/+11234567890 56 * </pre> 57 */ 58 public class PhoneLookupHistoryContentProvider extends ContentProvider { 59 60 /** 61 * Can't use {@link UriMatcher} because it doesn't support empty values, and numbers can be empty. 62 */ 63 @Retention(RetentionPolicy.SOURCE) 64 @IntDef({UriType.PHONE_LOOKUP_HISTORY_TABLE_CODE, UriType.PHONE_LOOKUP_HISTORY_TABLE_ID_CODE}) 65 private @interface UriType { 66 // For operations against: content://com.android.dialer.phonelookuphistory/PhoneLookupHistory 67 int PHONE_LOOKUP_HISTORY_TABLE_CODE = 1; 68 // For operations against: 69 // content://com.android.dialer.phonelookuphistory/PhoneLookupHistory?number=123 70 int PHONE_LOOKUP_HISTORY_TABLE_ID_CODE = 2; 71 } 72 73 private PhoneLookupHistoryDatabaseHelper databaseHelper; 74 75 private final ThreadLocal<Boolean> applyingBatch = new ThreadLocal<>(); 76 77 /** Ensures that only a single notification is generated from {@link #applyBatch(ArrayList)}. */ isApplyingBatch()78 private boolean isApplyingBatch() { 79 return applyingBatch.get() != null && applyingBatch.get(); 80 } 81 82 @Override onCreate()83 public boolean onCreate() { 84 databaseHelper = 85 PhoneLookupDatabaseComponent.get(getContext()).phoneLookupHistoryDatabaseHelper(); 86 return true; 87 } 88 89 @Nullable 90 @Override query( @onNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder)91 public Cursor query( 92 @NonNull Uri uri, 93 @Nullable String[] projection, 94 @Nullable String selection, 95 @Nullable String[] selectionArgs, 96 @Nullable String sortOrder) { 97 SQLiteDatabase db = databaseHelper.getReadableDatabase(); 98 SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); 99 queryBuilder.setTables(PhoneLookupHistory.TABLE); 100 @UriType int uriType = uriType(uri); 101 switch (uriType) { 102 case UriType.PHONE_LOOKUP_HISTORY_TABLE_ID_CODE: 103 queryBuilder.appendWhere( 104 PhoneLookupHistory.NORMALIZED_NUMBER 105 + "=" 106 + DatabaseUtils.sqlEscapeString( 107 Uri.decode(uri.getQueryParameter(PhoneLookupHistory.NUMBER_QUERY_PARAM)))); 108 // fall through 109 case UriType.PHONE_LOOKUP_HISTORY_TABLE_CODE: 110 Cursor cursor = 111 queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder); 112 if (cursor == null) { 113 LogUtil.w("PhoneLookupHistoryContentProvider.query", "cursor was null"); 114 return null; 115 } 116 cursor.setNotificationUri( 117 getContext().getContentResolver(), PhoneLookupHistory.CONTENT_URI); 118 return cursor; 119 default: 120 throw new IllegalArgumentException("Unknown uri: " + uri); 121 } 122 } 123 124 @Nullable 125 @Override getType(@onNull Uri uri)126 public String getType(@NonNull Uri uri) { 127 return PhoneLookupHistory.CONTENT_ITEM_TYPE; 128 } 129 130 @Nullable 131 @Override insert(@onNull Uri uri, @Nullable ContentValues values)132 public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { 133 // Javadoc states values is not nullable, even though it is annotated as such (a bug)! 134 Assert.checkArgument(values != null); 135 136 SQLiteDatabase database = databaseHelper.getWritableDatabase(); 137 @UriType int uriType = uriType(uri); 138 switch (uriType) { 139 case UriType.PHONE_LOOKUP_HISTORY_TABLE_CODE: 140 Assert.checkArgument( 141 values.getAsString(PhoneLookupHistory.NORMALIZED_NUMBER) != null, 142 "You must specify a normalized number when inserting"); 143 break; 144 case UriType.PHONE_LOOKUP_HISTORY_TABLE_ID_CODE: 145 String normalizedNumberFromUri = 146 Uri.decode(uri.getQueryParameter(PhoneLookupHistory.NUMBER_QUERY_PARAM)); 147 String normalizedNumberFromValues = 148 values.getAsString(PhoneLookupHistory.NORMALIZED_NUMBER); 149 Assert.checkArgument( 150 normalizedNumberFromValues == null 151 || normalizedNumberFromValues.equals(normalizedNumberFromUri), 152 "NORMALIZED_NUMBER from values %s does not match normalized number from URI: %s", 153 LogUtil.sanitizePhoneNumber(normalizedNumberFromValues), 154 LogUtil.sanitizePhoneNumber(normalizedNumberFromUri)); 155 if (normalizedNumberFromValues == null) { 156 values.put(PhoneLookupHistory.NORMALIZED_NUMBER, normalizedNumberFromUri); 157 } 158 break; 159 default: 160 throw new IllegalArgumentException("Unknown uri: " + uri); 161 } 162 // Note: The id returned for a successful insert isn't actually part of the table. 163 long id = database.insert(PhoneLookupHistory.TABLE, null, values); 164 if (id < 0) { 165 LogUtil.w( 166 "PhoneLookupHistoryContentProvider.insert", 167 "error inserting row with number: %s", 168 LogUtil.sanitizePhoneNumber(values.getAsString(PhoneLookupHistory.NORMALIZED_NUMBER))); 169 return null; 170 } 171 Uri insertedUri = 172 PhoneLookupHistory.contentUriForNumber( 173 values.getAsString(PhoneLookupHistory.NORMALIZED_NUMBER)); 174 if (!isApplyingBatch()) { 175 notifyChange(insertedUri); 176 } 177 return insertedUri; 178 } 179 180 @Override delete( @onNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs)181 public int delete( 182 @NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { 183 SQLiteDatabase database = databaseHelper.getWritableDatabase(); 184 @UriType int uriType = uriType(uri); 185 switch (uriType) { 186 case UriType.PHONE_LOOKUP_HISTORY_TABLE_CODE: 187 break; 188 case UriType.PHONE_LOOKUP_HISTORY_TABLE_ID_CODE: 189 Assert.checkArgument(selection == null, "Do not specify selection when deleting by number"); 190 Assert.checkArgument( 191 selectionArgs == null, "Do not specify selection args when deleting by number"); 192 String number = Uri.decode(uri.getQueryParameter(PhoneLookupHistory.NUMBER_QUERY_PARAM)); 193 Assert.checkArgument( 194 number != null, "error parsing number from uri: %s", LogUtil.sanitizePii(uri)); 195 selection = PhoneLookupHistory.NORMALIZED_NUMBER + "= ?"; 196 selectionArgs = new String[] {number}; 197 break; 198 default: 199 throw new IllegalArgumentException("Unknown uri: " + uri); 200 } 201 int rows = database.delete(PhoneLookupHistory.TABLE, selection, selectionArgs); 202 if (rows == 0) { 203 LogUtil.w("PhoneLookupHistoryContentProvider.delete", "no rows deleted"); 204 return rows; 205 } 206 if (!isApplyingBatch()) { 207 notifyChange(uri); 208 } 209 return rows; 210 } 211 212 /** 213 * Note: If the normalized number is included as part of the URI (for example, 214 * "content://com.android.dialer.phonelookuphistory/PhoneLookupHistory/+123") then the update 215 * operation will actually be a "replace" operation, inserting a new row if one does not already 216 * exist. 217 * 218 * <p>All columns in an existing row will be replaced which means you must specify all required 219 * columns in {@code values} when using this method. 220 */ 221 @Override update( @onNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs)222 public int update( 223 @NonNull Uri uri, 224 @Nullable ContentValues values, 225 @Nullable String selection, 226 @Nullable String[] selectionArgs) { 227 // Javadoc states values is not nullable, even though it is annotated as such (a bug)! 228 Assert.checkArgument(values != null); 229 230 SQLiteDatabase database = databaseHelper.getWritableDatabase(); 231 @UriType int uriType = uriType(uri); 232 switch (uriType) { 233 case UriType.PHONE_LOOKUP_HISTORY_TABLE_CODE: 234 int rows = database.update(PhoneLookupHistory.TABLE, values, selection, selectionArgs); 235 if (rows == 0) { 236 LogUtil.w("PhoneLookupHistoryContentProvider.update", "no rows updated"); 237 return rows; 238 } 239 if (!isApplyingBatch()) { 240 notifyChange(uri); 241 } 242 return rows; 243 case UriType.PHONE_LOOKUP_HISTORY_TABLE_ID_CODE: 244 Assert.checkArgument( 245 !values.containsKey(PhoneLookupHistory.NORMALIZED_NUMBER), 246 "Do not specify number in values when updating by number"); 247 Assert.checkArgument(selection == null, "Do not specify selection when updating by ID"); 248 Assert.checkArgument( 249 selectionArgs == null, "Do not specify selection args when updating by ID"); 250 251 String normalizedNumber = 252 Uri.decode(uri.getQueryParameter(PhoneLookupHistory.NUMBER_QUERY_PARAM)); 253 values.put(PhoneLookupHistory.NORMALIZED_NUMBER, normalizedNumber); 254 long result = database.replace(PhoneLookupHistory.TABLE, null, values); 255 Assert.checkArgument(result != -1, "replacing PhoneLookupHistory row failed"); 256 if (!isApplyingBatch()) { 257 notifyChange(uri); 258 } 259 return 1; 260 default: 261 throw new IllegalArgumentException("Unknown uri: " + uri); 262 } 263 } 264 265 /** 266 * {@inheritDoc} 267 * 268 * <p>Note: When applyBatch is used with the PhoneLookupHistory, only a single notification for 269 * the content URI is generated, not individual notifications for each affected URI. 270 */ 271 @NonNull 272 @Override applyBatch(@onNull ArrayList<ContentProviderOperation> operations)273 public ContentProviderResult[] applyBatch(@NonNull ArrayList<ContentProviderOperation> operations) 274 throws OperationApplicationException { 275 ContentProviderResult[] results = new ContentProviderResult[operations.size()]; 276 if (operations.isEmpty()) { 277 return results; 278 } 279 280 SQLiteDatabase database = databaseHelper.getWritableDatabase(); 281 try { 282 applyingBatch.set(true); 283 database.beginTransaction(); 284 for (int i = 0; i < operations.size(); i++) { 285 ContentProviderOperation operation = operations.get(i); 286 @UriType int uriType = uriType(operation.getUri()); 287 switch (uriType) { 288 case UriType.PHONE_LOOKUP_HISTORY_TABLE_CODE: 289 case UriType.PHONE_LOOKUP_HISTORY_TABLE_ID_CODE: 290 ContentProviderResult result = operation.apply(this, results, i); 291 if (operation.isInsert()) { 292 if (result.uri == null) { 293 throw new OperationApplicationException("error inserting row"); 294 } 295 } else if (result.count == 0) { 296 throw new OperationApplicationException("error applying operation"); 297 } 298 results[i] = result; 299 break; 300 default: 301 throw new IllegalArgumentException("Unknown uri: " + operation.getUri()); 302 } 303 } 304 database.setTransactionSuccessful(); 305 } finally { 306 applyingBatch.set(false); 307 database.endTransaction(); 308 } 309 notifyChange(PhoneLookupHistory.CONTENT_URI); 310 return results; 311 } 312 notifyChange(Uri uri)313 private void notifyChange(Uri uri) { 314 getContext().getContentResolver().notifyChange(uri, null); 315 } 316 317 @UriType uriType(Uri uri)318 private int uriType(Uri uri) { 319 Assert.checkArgument(uri.getAuthority().equals(PhoneLookupHistoryContract.AUTHORITY)); 320 List<String> pathSegments = uri.getPathSegments(); 321 Assert.checkArgument(pathSegments.size() == 1); 322 Assert.checkArgument(pathSegments.get(0).equals(PhoneLookupHistory.TABLE)); 323 return uri.getQueryParameter(PhoneLookupHistory.NUMBER_QUERY_PARAM) == null 324 ? UriType.PHONE_LOOKUP_HISTORY_TABLE_CODE 325 : UriType.PHONE_LOOKUP_HISTORY_TABLE_ID_CODE; 326 } 327 } 328