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