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