1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3 
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License
16  */
17 
18 package com.android.providers.contacts;
19 
20 import static android.Manifest.permission.READ_VOICEMAIL;
21 
22 import android.content.ComponentName;
23 import android.content.ContentUris;
24 import android.content.ContentValues;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.database.Cursor;
28 import android.database.DatabaseUtils.InsertHelper;
29 import android.database.sqlite.SQLiteDatabase;
30 import android.database.sqlite.SQLiteQueryBuilder;
31 import android.net.Uri;
32 import android.os.Binder;
33 import android.provider.CallLog.Calls;
34 import android.provider.VoicemailContract;
35 import android.provider.VoicemailContract.Status;
36 import android.provider.VoicemailContract.Voicemails;
37 import android.util.ArraySet;
38 
39 import com.android.common.io.MoreCloseables;
40 import com.android.internal.annotations.VisibleForTesting;
41 import com.android.providers.contacts.CallLogDatabaseHelper.Tables;
42 import com.android.providers.contacts.util.DbQueryUtils;
43 
44 import com.google.android.collect.Lists;
45 import com.google.common.collect.Iterables;
46 import java.util.Collection;
47 import java.util.Set;
48 
49 /**
50  * An implementation of {@link DatabaseModifier} for voicemail related tables which additionally
51  * generates necessary notifications after the modification operation is performed.
52  * The class generates notifications for both voicemail as well as call log URI depending on which
53  * of then got affected by the change.
54  */
55 public class DbModifierWithNotification implements DatabaseModifier {
56 
57     private static final String TAG = "DbModifierWithNotify";
58 
59     private static final String[] PROJECTION = new String[] {
60             VoicemailContract.SOURCE_PACKAGE_FIELD
61     };
62     private static final int SOURCE_PACKAGE_COLUMN_INDEX = 0;
63     private static final String NON_NULL_SOURCE_PACKAGE_SELECTION =
64             VoicemailContract.SOURCE_PACKAGE_FIELD + " IS NOT NULL";
65     private static final String NOT_DELETED_SELECTION =
66             Voicemails.DELETED + " == 0";
67     private final String mTableName;
68     private final SQLiteDatabase mDb;
69     private final boolean mHasReadVoicemailPermission;
70     private final InsertHelper mInsertHelper;
71     private final Context mContext;
72     private final Uri mBaseUri;
73     private final boolean mIsCallsTable;
74     private final VoicemailNotifier mVoicemailNotifier;
75 
76     private boolean mIsBulkOperation = false;
77 
78     private static VoicemailNotifier sVoicemailNotifierForTest;
79 
DbModifierWithNotification(String tableName, SQLiteDatabase db, Context context)80     public DbModifierWithNotification(String tableName, SQLiteDatabase db, Context context) {
81         this(tableName, db, null, context);
82     }
83 
DbModifierWithNotification(String tableName, InsertHelper insertHelper, Context context)84     public DbModifierWithNotification(String tableName, InsertHelper insertHelper,
85             Context context) {
86         this(tableName, null, insertHelper, context);
87     }
88 
DbModifierWithNotification(String tableName, SQLiteDatabase db, InsertHelper insertHelper, Context context)89     private DbModifierWithNotification(String tableName, SQLiteDatabase db,
90             InsertHelper insertHelper, Context context) {
91         this(tableName, db, insertHelper, true /* hasReadVoicemail */, context);
92     }
93 
DbModifierWithNotification(String tableName, SQLiteDatabase db, InsertHelper insertHelper, boolean hasReadVoicemailPermission, Context context)94     public DbModifierWithNotification(String tableName, SQLiteDatabase db,
95             InsertHelper insertHelper, boolean hasReadVoicemailPermission, Context context) {
96         mTableName = tableName;
97         mDb = db;
98         mHasReadVoicemailPermission = hasReadVoicemailPermission;
99         mInsertHelper = insertHelper;
100         mContext = context;
101         mBaseUri = mTableName.equals(Tables.VOICEMAIL_STATUS) ?
102                 Status.CONTENT_URI : Voicemails.CONTENT_URI;
103         mIsCallsTable = mTableName.equals(Tables.CALLS);
104         mVoicemailNotifier = sVoicemailNotifierForTest != null ? sVoicemailNotifierForTest
105                 : new VoicemailNotifier(mContext, mBaseUri);
106     }
107 
108     @Override
insert(String table, String nullColumnHack, ContentValues values)109     public long insert(String table, String nullColumnHack, ContentValues values) {
110         Set<String> packagesModified = getModifiedPackages(values);
111         if (mIsCallsTable) {
112             values.put(Calls.LAST_MODIFIED, getTimeMillis());
113         }
114         long rowId = mDb.insert(table, nullColumnHack, values);
115         if (rowId > 0 && packagesModified.size() != 0) {
116             notifyVoicemailChangeOnInsert(ContentUris.withAppendedId(mBaseUri, rowId),
117                     packagesModified);
118         }
119         if (rowId > 0 && mIsCallsTable) {
120             notifyCallLogChange(mContext);
121         }
122         return rowId;
123     }
124 
125     @Override
insert(ContentValues values)126     public long insert(ContentValues values) {
127         Set<String> packagesModified = getModifiedPackages(values);
128         if (mIsCallsTable) {
129             values.put(Calls.LAST_MODIFIED, getTimeMillis());
130         }
131         long rowId = mInsertHelper.insert(values);
132         if (rowId > 0 && packagesModified.size() != 0) {
133             notifyVoicemailChangeOnInsert(
134                     ContentUris.withAppendedId(mBaseUri, rowId), packagesModified);
135         }
136         if (rowId > 0 && mIsCallsTable) {
137             notifyCallLogChange(mContext);
138         }
139         return rowId;
140     }
141 
notifyCallLogChange(Context context)142     public static void notifyCallLogChange(Context context) {
143         context.getContentResolver().notifyChange(Calls.CONTENT_URI, null, false);
144 
145         Intent intent = new Intent("com.android.internal.action.CALL_LOG_CHANGE");
146         intent.setComponent(new ComponentName("com.android.calllogbackup",
147                 "com.android.calllogbackup.CallLogChangeReceiver"));
148 
149         if (!context.getPackageManager().queryBroadcastReceivers(intent, 0).isEmpty()) {
150             context.sendBroadcast(intent);
151         }
152     }
153 
notifyVoicemailChangeOnInsert( Uri notificationUri, Set<String> packagesModified)154     private void notifyVoicemailChangeOnInsert(
155             Uri notificationUri, Set<String> packagesModified) {
156         if (mIsCallsTable) {
157             mVoicemailNotifier.addIntentActions(VoicemailContract.ACTION_NEW_VOICEMAIL);
158         }
159         notifyVoicemailChange(notificationUri, packagesModified);
160     }
161 
notifyVoicemailChange(Uri notificationUri, Set<String> modifiedPackages)162     private void notifyVoicemailChange(Uri notificationUri,
163             Set<String> modifiedPackages) {
164         mVoicemailNotifier.addUri(notificationUri);
165         mVoicemailNotifier.addModifiedPackages(modifiedPackages);
166         mVoicemailNotifier.addIntentActions(Intent.ACTION_PROVIDER_CHANGED);
167         if (!mIsBulkOperation) {
168             mVoicemailNotifier.sendNotification();
169         }
170     }
171 
172     @Override
update(Uri uri, String table, ContentValues values, String whereClause, String[] whereArgs)173     public int update(Uri uri, String table, ContentValues values, String whereClause,
174             String[] whereArgs) {
175         Set<String> packagesModified = getModifiedPackages(whereClause, whereArgs);
176         packagesModified.addAll(getModifiedPackages(values));
177 
178         boolean isVoicemailContent =
179                 packagesModified.size() != 0 && isUpdatingVoicemailColumns(values);
180 
181         boolean hasMarkedRead = false;
182         if (mIsCallsTable) {
183             if (values.containsKey(Voicemails.DELETED)
184                     && !values.getAsBoolean(Voicemails.DELETED)) {
185                 values.put(Calls.LAST_MODIFIED, getTimeMillis());
186             } else {
187                 updateLastModified(table, whereClause, whereArgs);
188             }
189             if (isVoicemailContent) {
190                 if (updateDirtyFlag(values, packagesModified)) {
191                     if (values.containsKey(Calls.IS_READ)
192                             && getAsBoolean(values,
193                             Calls.IS_READ)) {
194                         // If the server has set the IS_READ, it should also unset the new flag
195                         if (!values.containsKey(Calls.NEW)) {
196                             values.put(Calls.NEW, 0);
197                             hasMarkedRead = true;
198                         }
199                     }
200                 }
201             }
202         }
203         // updateDirtyFlag might remove the value and leave values empty.
204         if (values.isEmpty()) {
205             return 0;
206         }
207 
208         final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
209         qb.setTables(mTableName);
210         qb.setProjectionMap(CallLogProvider.sCallsProjectionMap);
211         qb.setStrict(true);
212         if (!mHasReadVoicemailPermission) {
213             qb.setStrictGrammar(true);
214         }
215         int count = qb.update(mDb, values, whereClause, whereArgs);
216 
217         if (count > 0 && isVoicemailContent || Tables.VOICEMAIL_STATUS.equals(table)) {
218             notifyVoicemailChange(mBaseUri, packagesModified);
219         }
220         if (count > 0 && mIsCallsTable) {
221             notifyCallLogChange(mContext);
222         }
223         if (hasMarkedRead) {
224             // A "New" voicemail has been marked as read by the server. This voicemail is no longer
225             // new but the content consumer might still think it is. ACTION_NEW_VOICEMAIL should
226             // trigger a rescan of new voicemails.
227             mContext.sendBroadcast(
228                     new Intent(VoicemailContract.ACTION_NEW_VOICEMAIL, uri),
229                     READ_VOICEMAIL);
230         }
231         return count;
232     }
233 
updateDirtyFlag(ContentValues values, Set<String> packagesModified)234     private boolean updateDirtyFlag(ContentValues values, Set<String> packagesModified) {
235         // If a calling package is modifying its own entries, it means that the change came
236         // from the server and thus is synced or "clean". Otherwise, it means that a local
237         // change is being made to the database, so the entries should be marked as "dirty"
238         // so that the corresponding sync adapter knows they need to be synced.
239         int isDirty;
240         Integer callerSetDirty = values.getAsInteger(Voicemails.DIRTY);
241         if (callerSetDirty != null) {
242             // Respect the calling package if it sets the dirty flag
243             if (callerSetDirty == Voicemails.DIRTY_RETAIN) {
244                 values.remove(Voicemails.DIRTY);
245                 return false;
246             } else {
247                 isDirty = callerSetDirty == 0 ? 0 : 1;
248             }
249         } else {
250             isDirty = isSelfModifyingOrInternal(packagesModified) ? 0 : 1;
251         }
252 
253         values.put(Voicemails.DIRTY, isDirty);
254         return isDirty == 0;
255     }
256 
isUpdatingVoicemailColumns(ContentValues values)257     private boolean isUpdatingVoicemailColumns(ContentValues values) {
258         for (String key : values.keySet()) {
259             if (VoicemailContentTable.ALLOWED_COLUMNS.contains(key)) {
260                 return true;
261             }
262         }
263         return false;
264     }
265 
updateLastModified(String table, String whereClause, String[] whereArgs)266     private void updateLastModified(String table, String whereClause, String[] whereArgs) {
267         ContentValues values = new ContentValues();
268         values.put(Calls.LAST_MODIFIED, getTimeMillis());
269 
270         mDb.update(table, values,
271                 DbQueryUtils.concatenateClauses(NOT_DELETED_SELECTION, whereClause),
272                 whereArgs);
273     }
274 
275     @Override
delete(String table, String whereClause, String[] whereArgs)276     public int delete(String table, String whereClause, String[] whereArgs) {
277         Set<String> packagesModified = getModifiedPackages(whereClause, whereArgs);
278         boolean isVoicemail = packagesModified.size() != 0;
279 
280         // If a deletion is made by a package that is not the package that inserted the voicemail,
281         // this means that the user deleted the voicemail. However, we do not want to delete it from
282         // the database until after the server has been notified of the deletion. To ensure this,
283         // mark the entry as "deleted"--deleted entries should be hidden from the user.
284         // Once the changes are synced to the server, delete will be called again, this time
285         // removing the rows from the table.
286         // If the deletion is being made by the package that inserted the voicemail or by
287         // CP2 (cleanup after uninstall), then we don't need to wait for sync, so just delete it.
288         final int count;
289 
290         final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
291         qb.setTables(mTableName);
292         qb.setProjectionMap(CallLogProvider.sCallsProjectionMap);
293         qb.setStrict(true);
294         if (!mHasReadVoicemailPermission) {
295             qb.setStrictGrammar(true);
296         }
297 
298         if (mIsCallsTable && isVoicemail && !isSelfModifyingOrInternal(packagesModified)) {
299             ContentValues values = new ContentValues();
300             values.put(VoicemailContract.Voicemails.DIRTY, 1);
301             values.put(VoicemailContract.Voicemails.DELETED, 1);
302             values.put(VoicemailContract.Voicemails.LAST_MODIFIED, getTimeMillis());
303             count = qb.update(mDb, values, whereClause, whereArgs);
304         } else {
305             count = qb.delete(mDb, whereClause, whereArgs);
306         }
307 
308         if (count > 0 && isVoicemail) {
309             notifyVoicemailChange(mBaseUri, packagesModified);
310         }
311         if (count > 0 && mIsCallsTable) {
312             notifyCallLogChange(mContext);
313         }
314         return count;
315     }
316 
317     @Override
startBulkOperation()318     public void startBulkOperation() {
319         mIsBulkOperation = true;
320         mDb.beginTransaction();
321     }
322 
323     @Override
yieldBulkOperation()324     public void yieldBulkOperation() {
325         mDb.yieldIfContendedSafely();
326     }
327 
328     @Override
finishBulkOperation()329     public void finishBulkOperation() {
330         mDb.setTransactionSuccessful();
331         mDb.endTransaction();
332         mIsBulkOperation = false;
333         mVoicemailNotifier.sendNotification();
334     }
335 
336     /**
337      * Returns the set of packages affected when a modify operation is run for the specified
338      * where clause. When called from an insert operation an empty set returned by this method
339      * implies (indirectly) that this does not affect any voicemail entry, as a voicemail entry is
340      * always expected to have the source package field set.
341      */
getModifiedPackages(String whereClause, String[] whereArgs)342     private Set<String> getModifiedPackages(String whereClause, String[] whereArgs) {
343         Set<String> modifiedPackages = new ArraySet<>();
344         Cursor cursor = mDb.query(mTableName, PROJECTION,
345                 DbQueryUtils.concatenateClauses(NON_NULL_SOURCE_PACKAGE_SELECTION, whereClause),
346                 whereArgs, null, null, null);
347         while (cursor.moveToNext()) {
348             modifiedPackages.add(cursor.getString(SOURCE_PACKAGE_COLUMN_INDEX));
349         }
350         MoreCloseables.closeQuietly(cursor);
351         return modifiedPackages;
352     }
353 
354     /**
355      * Returns the source package that gets affected (in an insert/update operation) by the supplied
356      * content values. An empty set returned by this method also implies (indirectly) that this does
357      * not affect any voicemail entry, as a voicemail entry is always expected to have the source
358      * package field set.
359      */
getModifiedPackages(ContentValues values)360     private Set<String> getModifiedPackages(ContentValues values) {
361         Set<String> impactedPackages = new ArraySet<>();
362         if (values.containsKey(VoicemailContract.SOURCE_PACKAGE_FIELD)) {
363             impactedPackages.add(values.getAsString(VoicemailContract.SOURCE_PACKAGE_FIELD));
364         }
365         return impactedPackages;
366     }
367 
368     /**
369      * @param packagesModified source packages that inserted the voicemail that is being modified
370      * @return {@code true} if the caller is modifying its own voicemail, or this is an internal
371      * transaction, {@code false} otherwise.
372      */
isSelfModifyingOrInternal(Set<String> packagesModified)373     private boolean isSelfModifyingOrInternal(Set<String> packagesModified) {
374         final Collection<String> callingPackages = getCallingPackages();
375         if (callingPackages == null) {
376             return false;
377         }
378         // The last clause has the same effect as doing Process.myUid() == Binder.getCallingUid(),
379         // but allows us to mock the results for testing.
380         return packagesModified.size() == 1 && (callingPackages.contains(
381                 Iterables.getOnlyElement(packagesModified))
382                 || callingPackages.contains(mContext.getPackageName()));
383     }
384 
385     /**
386      * Returns the package names of the calling process. If the calling process has more than
387      * one packages, this returns them all
388      */
getCallingPackages()389     private Collection<String> getCallingPackages() {
390         int caller = Binder.getCallingUid();
391         if (caller == 0) {
392             return null;
393         }
394         return Lists.newArrayList(mContext.getPackageManager().getPackagesForUid(caller));
395     }
396 
397     /**
398      * A variant of {@link ContentValues#getAsBoolean(String)} that also treat the string "0" as
399      * false and other integer string as true. 0, 1, false, true, "0", "1", "false", "true" might
400      * all be inserted into the ContentValues as a boolean, but "0" and "1" are not handled by
401      * {@link ContentValues#getAsBoolean(String)}
402      */
getAsBoolean(ContentValues values, String key)403     private static Boolean getAsBoolean(ContentValues values, String key) {
404         Object value = values.get(key);
405         if (value instanceof CharSequence) {
406             try {
407                 int intValue = Integer.parseInt(value.toString());
408                 return intValue != 0;
409             } catch (NumberFormatException nfe) {
410                 // Do nothing.
411             }
412         }
413         return values.getAsBoolean(key);
414     }
415 
getTimeMillis()416     private long getTimeMillis() {
417         if (CallLogProvider.getTimeForTestMillis() == null) {
418             return System.currentTimeMillis();
419         }
420         return CallLogProvider.getTimeForTestMillis();
421     }
422 
423     @VisibleForTesting
setVoicemailNotifierForTest(VoicemailNotifier notifier)424     static void setVoicemailNotifierForTest(VoicemailNotifier notifier) {
425         sVoicemailNotifierForTest = notifier;
426     }
427 }
428