• 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 = new PhoneLookupHistoryDatabaseHelper(getContext());
85     return true;
86   }
87 
88   @Nullable
89   @Override
query( @onNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder)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
getType(@onNull Uri uri)125   public String getType(@NonNull Uri uri) {
126     return PhoneLookupHistory.CONTENT_ITEM_TYPE;
127   }
128 
129   @Nullable
130   @Override
insert(@onNull Uri uri, @Nullable ContentValues values)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
delete( @onNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs)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
update( @onNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs)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
applyBatch(@onNull ArrayList<ContentProviderOperation> operations)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 
notifyChange(Uri uri)312   private void notifyChange(Uri uri) {
313     getContext().getContentResolver().notifyChange(uri, null);
314   }
315 
316   @UriType
uriType(Uri uri)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