• 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.calllog.database;
18 
19 import android.annotation.TargetApi;
20 import android.content.ContentProvider;
21 import android.content.ContentProviderOperation;
22 import android.content.ContentProviderResult;
23 import android.content.ContentUris;
24 import android.content.ContentValues;
25 import android.content.OperationApplicationException;
26 import android.content.UriMatcher;
27 import android.database.Cursor;
28 import android.database.sqlite.SQLiteDatabase;
29 import android.database.sqlite.SQLiteQueryBuilder;
30 import android.net.Uri;
31 import android.os.Build;
32 import android.provider.CallLog.Calls;
33 import android.support.annotation.NonNull;
34 import android.support.annotation.Nullable;
35 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract;
36 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog;
37 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.CoalescedAnnotatedCallLog;
38 import com.android.dialer.common.Assert;
39 import com.android.dialer.common.LogUtil;
40 import com.android.dialer.metrics.Metrics;
41 import com.android.dialer.metrics.MetricsComponent;
42 import java.util.ArrayList;
43 import java.util.Arrays;
44 
45 /** {@link ContentProvider} for the annotated call log. */
46 public class AnnotatedCallLogContentProvider extends ContentProvider {
47 
48   /**
49    * We sometimes run queries where we potentially pass every ID into a where clause using the
50    * (?,?,?,...) syntax. The maximum number of host parameters is 999, so that's the maximum size
51    * this table can be. See https://www.sqlite.org/limits.html for more details.
52    */
53   private static final int MAX_ROWS = 999;
54 
55   private static final int ANNOTATED_CALL_LOG_TABLE_CODE = 1;
56   private static final int ANNOTATED_CALL_LOG_TABLE_ID_CODE = 2;
57   private static final int ANNOTATED_CALL_LOG_TABLE_DISTINCT_NUMBER_CODE = 3;
58   private static final int COALESCED_ANNOTATED_CALL_LOG_TABLE_CODE = 4;
59 
60   private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
61 
62   static {
uriMatcher.addURI( AnnotatedCallLogContract.AUTHORITY, AnnotatedCallLog.TABLE, ANNOTATED_CALL_LOG_TABLE_CODE)63     uriMatcher.addURI(
64         AnnotatedCallLogContract.AUTHORITY, AnnotatedCallLog.TABLE, ANNOTATED_CALL_LOG_TABLE_CODE);
uriMatcher.addURI( AnnotatedCallLogContract.AUTHORITY, AnnotatedCallLog.TABLE + "/#", ANNOTATED_CALL_LOG_TABLE_ID_CODE)65     uriMatcher.addURI(
66         AnnotatedCallLogContract.AUTHORITY,
67         AnnotatedCallLog.TABLE + "/#",
68         ANNOTATED_CALL_LOG_TABLE_ID_CODE);
uriMatcher.addURI( AnnotatedCallLogContract.AUTHORITY, AnnotatedCallLog.DISTINCT_PHONE_NUMBERS, ANNOTATED_CALL_LOG_TABLE_DISTINCT_NUMBER_CODE)69     uriMatcher.addURI(
70         AnnotatedCallLogContract.AUTHORITY,
71         AnnotatedCallLog.DISTINCT_PHONE_NUMBERS,
72         ANNOTATED_CALL_LOG_TABLE_DISTINCT_NUMBER_CODE);
uriMatcher.addURI( AnnotatedCallLogContract.AUTHORITY, CoalescedAnnotatedCallLog.TABLE, COALESCED_ANNOTATED_CALL_LOG_TABLE_CODE)73     uriMatcher.addURI(
74         AnnotatedCallLogContract.AUTHORITY,
75         CoalescedAnnotatedCallLog.TABLE,
76         COALESCED_ANNOTATED_CALL_LOG_TABLE_CODE);
77   }
78 
79   private AnnotatedCallLogDatabaseHelper databaseHelper;
80 
81   private final ThreadLocal<Boolean> applyingBatch = new ThreadLocal<>();
82 
83   /** Ensures that only a single notification is generated from {@link #applyBatch(ArrayList)}. */
isApplyingBatch()84   private boolean isApplyingBatch() {
85     return applyingBatch.get() != null && applyingBatch.get();
86   }
87 
88   @Override
onCreate()89   public boolean onCreate() {
90     databaseHelper = new AnnotatedCallLogDatabaseHelper(getContext(), MAX_ROWS);
91 
92     // Note: As this method is called before Application#onCreate, we must *not* initialize objects
93     // that require preparation work done in Application#onCreate.
94     // One example is to avoid obtaining an instance that depends on Google's proprietary config,
95     // which is initialized in Application#onCreate.
96 
97     return true;
98   }
99 
100   @TargetApi(Build.VERSION_CODES.M) // Uses try-with-resources
101   @Nullable
102   @Override
query( @onNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder)103   public Cursor query(
104       @NonNull Uri uri,
105       @Nullable String[] projection,
106       @Nullable String selection,
107       @Nullable String[] selectionArgs,
108       @Nullable String sortOrder) {
109     SQLiteDatabase db = databaseHelper.getReadableDatabase();
110     SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
111     queryBuilder.setTables(AnnotatedCallLog.TABLE);
112     int match = uriMatcher.match(uri);
113     switch (match) {
114       case ANNOTATED_CALL_LOG_TABLE_ID_CODE:
115         queryBuilder.appendWhere(AnnotatedCallLog._ID + "=" + ContentUris.parseId(uri));
116         Cursor cursor =
117             queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
118         if (cursor != null) {
119           cursor.setNotificationUri(
120               getContext().getContentResolver(), AnnotatedCallLog.CONTENT_URI);
121         } else {
122           LogUtil.w("AnnotatedCallLogContentProvider.query", "cursor was null");
123         }
124         return cursor;
125       case ANNOTATED_CALL_LOG_TABLE_CODE:
126         cursor =
127             queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
128         if (cursor != null) {
129           cursor.setNotificationUri(
130               getContext().getContentResolver(), AnnotatedCallLog.CONTENT_URI);
131         } else {
132           LogUtil.w("AnnotatedCallLogContentProvider.query", "cursor was null");
133         }
134         return cursor;
135       case ANNOTATED_CALL_LOG_TABLE_DISTINCT_NUMBER_CODE:
136         Assert.checkArgument(
137             Arrays.equals(projection, new String[] {AnnotatedCallLog.NUMBER}),
138             "only NUMBER supported for projection for distinct phone number query, got: %s",
139             Arrays.toString(projection));
140         queryBuilder.setDistinct(true);
141         cursor =
142             queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
143         if (cursor != null) {
144           cursor.setNotificationUri(
145               getContext().getContentResolver(), AnnotatedCallLog.CONTENT_URI);
146         } else {
147           LogUtil.w("AnnotatedCallLogContentProvider.query", "cursor was null");
148         }
149         return cursor;
150       case COALESCED_ANNOTATED_CALL_LOG_TABLE_CODE:
151         Assert.checkArgument(
152             projection == CoalescedAnnotatedCallLog.ALL_COLUMNS,
153             "only ALL_COLUMNS projection supported for coalesced call log");
154         Assert.checkArgument(selection == null, "selection not supported for coalesced call log");
155         Assert.checkArgument(
156             selectionArgs == null, "selection args not supported for coalesced call log");
157         Assert.checkArgument(sortOrder == null, "sort order not supported for coalesced call log");
158         MetricsComponent.get(getContext()).metrics().startTimer(Metrics.NEW_CALL_LOG_COALESCE);
159         try (Cursor allAnnotatedCallLogRows =
160             queryBuilder.query(
161                 db,
162                 null,
163                 String.format("%s != ?", CoalescedAnnotatedCallLog.CALL_TYPE),
164                 new String[] {Integer.toString(Calls.VOICEMAIL_TYPE)},
165                 null,
166                 null,
167                 AnnotatedCallLog.TIMESTAMP + " DESC")) {
168           Cursor coalescedRows =
169               CallLogDatabaseComponent.get(getContext())
170                   .coalescer()
171                   .coalesce(allAnnotatedCallLogRows);
172           coalescedRows.setNotificationUri(
173               getContext().getContentResolver(), CoalescedAnnotatedCallLog.CONTENT_URI);
174           MetricsComponent.get(getContext()).metrics().stopTimer(Metrics.NEW_CALL_LOG_COALESCE);
175           return coalescedRows;
176         }
177       default:
178         throw new IllegalArgumentException("Unknown uri: " + uri);
179     }
180   }
181 
182   @Nullable
183   @Override
getType(@onNull Uri uri)184   public String getType(@NonNull Uri uri) {
185     return AnnotatedCallLog.CONTENT_ITEM_TYPE;
186   }
187 
188   @Nullable
189   @Override
insert(@onNull Uri uri, @Nullable ContentValues values)190   public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
191     // Javadoc states values is not nullable, even though it is annotated as such (a bug)!
192     Assert.checkArgument(values != null);
193 
194     SQLiteDatabase database = databaseHelper.getWritableDatabase();
195     int match = uriMatcher.match(uri);
196     switch (match) {
197       case ANNOTATED_CALL_LOG_TABLE_CODE:
198         Assert.checkArgument(
199             values.get(AnnotatedCallLog._ID) != null, "You must specify an _ID when inserting");
200         break;
201       case ANNOTATED_CALL_LOG_TABLE_ID_CODE:
202         Long idFromUri = ContentUris.parseId(uri);
203         Long idFromValues = values.getAsLong(AnnotatedCallLog._ID);
204         Assert.checkArgument(
205             idFromValues == null || idFromValues.equals(idFromUri),
206             "_ID from values %d does not match ID from URI: %s",
207             idFromValues,
208             uri);
209         if (idFromValues == null) {
210           values.put(AnnotatedCallLog._ID, idFromUri);
211         }
212         break;
213       case ANNOTATED_CALL_LOG_TABLE_DISTINCT_NUMBER_CODE:
214         throw new UnsupportedOperationException();
215       case COALESCED_ANNOTATED_CALL_LOG_TABLE_CODE:
216         throw new UnsupportedOperationException("coalesced call log does not support inserting");
217       default:
218         throw new IllegalArgumentException("Unknown uri: " + uri);
219     }
220     long id = database.insert(AnnotatedCallLog.TABLE, null, values);
221     if (id < 0) {
222       LogUtil.w(
223           "AnnotatedCallLogContentProvider.insert",
224           "error inserting row with id: %d",
225           values.get(AnnotatedCallLog._ID));
226       return null;
227     }
228     Uri insertedUri = ContentUris.withAppendedId(AnnotatedCallLog.CONTENT_URI, id);
229     if (!isApplyingBatch()) {
230       notifyChange(insertedUri);
231     }
232     return insertedUri;
233   }
234 
235   @Override
delete( @onNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs)236   public int delete(
237       @NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
238     SQLiteDatabase database = databaseHelper.getWritableDatabase();
239     final int match = uriMatcher.match(uri);
240     switch (match) {
241       case ANNOTATED_CALL_LOG_TABLE_CODE:
242         break;
243       case ANNOTATED_CALL_LOG_TABLE_ID_CODE:
244         Assert.checkArgument(selection == null, "Do not specify selection when deleting by ID");
245         Assert.checkArgument(
246             selectionArgs == null, "Do not specify selection args when deleting by ID");
247         long id = ContentUris.parseId(uri);
248         Assert.checkArgument(id != -1, "error parsing id from uri %s", uri);
249         selection = getSelectionWithId(id);
250         break;
251       case ANNOTATED_CALL_LOG_TABLE_DISTINCT_NUMBER_CODE:
252         throw new UnsupportedOperationException();
253       case COALESCED_ANNOTATED_CALL_LOG_TABLE_CODE:
254         throw new UnsupportedOperationException("coalesced call log does not support deleting");
255       default:
256         throw new IllegalArgumentException("Unknown uri: " + uri);
257     }
258     int rows = database.delete(AnnotatedCallLog.TABLE, selection, selectionArgs);
259     if (rows == 0) {
260       LogUtil.w("AnnotatedCallLogContentProvider.delete", "no rows deleted");
261       return rows;
262     }
263     if (!isApplyingBatch()) {
264       notifyChange(uri);
265     }
266     return rows;
267   }
268 
269   @Override
update( @onNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs)270   public int update(
271       @NonNull Uri uri,
272       @Nullable ContentValues values,
273       @Nullable String selection,
274       @Nullable String[] selectionArgs) {
275     // Javadoc states values is not nullable, even though it is annotated as such (a bug)!
276     Assert.checkArgument(values != null);
277 
278     SQLiteDatabase database = databaseHelper.getWritableDatabase();
279     int match = uriMatcher.match(uri);
280     switch (match) {
281       case ANNOTATED_CALL_LOG_TABLE_CODE:
282         int rows = database.update(AnnotatedCallLog.TABLE, values, selection, selectionArgs);
283         if (rows == 0) {
284           LogUtil.w("AnnotatedCallLogContentProvider.update", "no rows updated");
285           return rows;
286         }
287         if (!isApplyingBatch()) {
288           notifyChange(uri);
289         }
290         return rows;
291       case ANNOTATED_CALL_LOG_TABLE_ID_CODE:
292         Assert.checkArgument(
293             !values.containsKey(AnnotatedCallLog._ID), "Do not specify _ID when updating by ID");
294         Assert.checkArgument(selection == null, "Do not specify selection when updating by ID");
295         Assert.checkArgument(
296             selectionArgs == null, "Do not specify selection args when updating by ID");
297         selection = getSelectionWithId(ContentUris.parseId(uri));
298         rows = database.update(AnnotatedCallLog.TABLE, values, selection, selectionArgs);
299         if (rows == 0) {
300           LogUtil.w("AnnotatedCallLogContentProvider.update", "no rows updated");
301           return rows;
302         }
303         if (!isApplyingBatch()) {
304           notifyChange(uri);
305         }
306         return rows;
307       case ANNOTATED_CALL_LOG_TABLE_DISTINCT_NUMBER_CODE:
308       case COALESCED_ANNOTATED_CALL_LOG_TABLE_CODE:
309         throw new UnsupportedOperationException();
310       default:
311         throw new IllegalArgumentException("Unknown uri: " + uri);
312     }
313   }
314 
315   /**
316    * {@inheritDoc}
317    *
318    * <p>Note: When applyBatch is used with the AnnotatedCallLog, only a single notification for the
319    * content URI is generated, not individual notifications for each affected URI.
320    */
321   @NonNull
322   @Override
applyBatch(@onNull ArrayList<ContentProviderOperation> operations)323   public ContentProviderResult[] applyBatch(@NonNull ArrayList<ContentProviderOperation> operations)
324       throws OperationApplicationException {
325     ContentProviderResult[] results = new ContentProviderResult[operations.size()];
326     if (operations.isEmpty()) {
327       return results;
328     }
329 
330     SQLiteDatabase database = databaseHelper.getWritableDatabase();
331     try {
332       applyingBatch.set(true);
333       database.beginTransaction();
334       for (int i = 0; i < operations.size(); i++) {
335         ContentProviderOperation operation = operations.get(i);
336         int match = uriMatcher.match(operation.getUri());
337         switch (match) {
338           case ANNOTATED_CALL_LOG_TABLE_CODE:
339           case ANNOTATED_CALL_LOG_TABLE_ID_CODE:
340             // These are allowed values, continue.
341             break;
342           case ANNOTATED_CALL_LOG_TABLE_DISTINCT_NUMBER_CODE:
343             throw new UnsupportedOperationException();
344           case COALESCED_ANNOTATED_CALL_LOG_TABLE_CODE:
345             throw new UnsupportedOperationException(
346                 "coalesced call log does not support applyBatch");
347           default:
348             throw new IllegalArgumentException("Unknown uri: " + operation.getUri());
349         }
350         ContentProviderResult result = operation.apply(this, results, i);
351         if (operations.get(i).isInsert()) {
352           if (result.uri == null) {
353             throw new OperationApplicationException("error inserting row");
354           }
355         } else if (result.count == 0) {
356           /*
357            * The batches built by MutationApplier happen to contain operations in order of:
358            *
359            * 1. Inserts
360            * 2. Updates
361            * 3. Deletes
362            *
363            * Let's say the last row in the table is row Z, and MutationApplier wishes to update it,
364            * as well as insert row A. When row A gets inserted, row Z will be deleted via the
365            * trigger if the table is full. Then later, when we try to process the update for row Z,
366            * it won't exist.
367            */
368           LogUtil.w(
369               "AnnotatedCallLogContentProvider.applyBatch",
370               "update or delete failed, possibly because row got cleaned up");
371         }
372         results[i] = result;
373       }
374       database.setTransactionSuccessful();
375     } finally {
376       applyingBatch.set(false);
377       database.endTransaction();
378     }
379     notifyChange(AnnotatedCallLog.CONTENT_URI);
380     return results;
381   }
382 
getSelectionWithId(long id)383   private String getSelectionWithId(long id) {
384     return AnnotatedCallLog._ID + "=" + id;
385   }
386 
notifyChange(Uri uri)387   private void notifyChange(Uri uri) {
388     getContext().getContentResolver().notifyChange(uri, null);
389     // Any time the annotated call log changes, we need to also notify observers of the
390     // CoalescedAnnotatedCallLog, since that is just a massaged in-memory view of the real annotated
391     // call log table.
392     getContext().getContentResolver().notifyChange(CoalescedAnnotatedCallLog.CONTENT_URI, null);
393   }
394 }
395