• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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