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