• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.ADD_VOICEMAIL;
21 import static android.Manifest.permission.READ_VOICEMAIL;
22 
23 import android.content.ComponentName;
24 import android.content.ContentUris;
25 import android.content.ContentValues;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.pm.ActivityInfo;
29 import android.content.pm.ResolveInfo;
30 import android.database.Cursor;
31 import android.database.DatabaseUtils.InsertHelper;
32 import android.database.sqlite.SQLiteDatabase;
33 import android.net.Uri;
34 import android.os.Binder;
35 import android.provider.CallLog.Calls;
36 import android.provider.VoicemailContract;
37 import android.provider.VoicemailContract.Status;
38 import android.provider.VoicemailContract.Voicemails;
39 import android.util.Log;
40 
41 import com.android.common.io.MoreCloseables;
42 import com.android.providers.contacts.CallLogDatabaseHelper.Tables;
43 import com.android.providers.contacts.util.DbQueryUtils;
44 
45 import com.google.android.collect.Lists;
46 import com.google.common.collect.Iterables;
47 import java.util.ArrayList;
48 import java.util.Collection;
49 import java.util.HashSet;
50 import java.util.List;
51 import java.util.Set;
52 
53 /**
54  * An implementation of {@link DatabaseModifier} for voicemail related tables which additionally
55  * generates necessary notifications after the modification operation is performed.
56  * The class generates notifications for both voicemail as well as call log URI depending on which
57  * of then got affected by the change.
58  */
59 public class DbModifierWithNotification implements DatabaseModifier {
60     private static final String TAG = "DbModifierWithNotify";
61 
62     private static final String[] PROJECTION = new String[] {
63             VoicemailContract.SOURCE_PACKAGE_FIELD
64     };
65     private static final int SOURCE_PACKAGE_COLUMN_INDEX = 0;
66     private static final String NON_NULL_SOURCE_PACKAGE_SELECTION =
67             VoicemailContract.SOURCE_PACKAGE_FIELD + " IS NOT NULL";
68     private static final String NOT_DELETED_SELECTION =
69             Voicemails.DELETED + " == 0";
70     private final String mTableName;
71     private final SQLiteDatabase mDb;
72     private final InsertHelper mInsertHelper;
73     private final Context mContext;
74     private final Uri mBaseUri;
75     private final boolean mIsCallsTable;
76     private final VoicemailPermissions mVoicemailPermissions;
77 
78 
DbModifierWithNotification(String tableName, SQLiteDatabase db, Context context)79     public DbModifierWithNotification(String tableName, SQLiteDatabase db, Context context) {
80         this(tableName, db, null, context);
81     }
82 
DbModifierWithNotification(String tableName, InsertHelper insertHelper, Context context)83     public DbModifierWithNotification(String tableName, InsertHelper insertHelper,
84             Context context) {
85         this(tableName, null, insertHelper, context);
86     }
87 
DbModifierWithNotification(String tableName, SQLiteDatabase db, InsertHelper insertHelper, Context context)88     private DbModifierWithNotification(String tableName, SQLiteDatabase db,
89             InsertHelper insertHelper, Context context) {
90         mTableName = tableName;
91         mDb = db;
92         mInsertHelper = insertHelper;
93         mContext = context;
94         mBaseUri = mTableName.equals(Tables.VOICEMAIL_STATUS) ?
95                 Status.CONTENT_URI : Voicemails.CONTENT_URI;
96         mIsCallsTable = mTableName.equals(Tables.CALLS);
97         mVoicemailPermissions = new VoicemailPermissions(mContext);
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(Uri notificationUri, Set<String> packagesModified) {
147         if (mIsCallsTable) {
148             notifyVoicemailChange(notificationUri, packagesModified,
149                     VoicemailContract.ACTION_NEW_VOICEMAIL, Intent.ACTION_PROVIDER_CHANGED);
150         } else {
151             notifyVoicemailChange(notificationUri, packagesModified,
152                     Intent.ACTION_PROVIDER_CHANGED);
153         }
154     }
155 
156     @Override
update(Uri uri, String table, ContentValues values, String whereClause, String[] whereArgs)157     public int update(Uri uri, String table, ContentValues values, String whereClause,
158             String[] whereArgs) {
159         Set<String> packagesModified = getModifiedPackages(whereClause, whereArgs);
160         packagesModified.addAll(getModifiedPackages(values));
161 
162         boolean isVoicemail = packagesModified.size() != 0;
163 
164         boolean hasMarkedRead = false;
165         if (mIsCallsTable) {
166             if (values.containsKey(Voicemails.DELETED)
167                     && !values.getAsBoolean(Voicemails.DELETED)) {
168                 values.put(Calls.LAST_MODIFIED, getTimeMillis());
169             } else {
170                 updateLastModified(table, whereClause, whereArgs);
171             }
172             if (isVoicemail) {
173                 // If a calling package is modifying its own entries, it means that the change came
174                 // from the server and thus is synced or "clean". Otherwise, it means that a local
175                 // change is being made to the database, so the entries should be marked as "dirty"
176                 // so that the corresponding sync adapter knows they need to be synced.
177                 int isDirty;
178                 Integer callerSetDirty = values.getAsInteger(Voicemails.DIRTY);
179                 if (callerSetDirty != null) {
180                     // Respect the calling package if it sets the dirty flag
181                     isDirty = callerSetDirty == 0 ? 0 : 1;
182                 } else {
183                     isDirty = isSelfModifyingOrInternal(packagesModified) ? 0 : 1;
184                 }
185                 values.put(VoicemailContract.Voicemails.DIRTY, isDirty);
186 
187                 if (isDirty == 0 && values.containsKey(Calls.IS_READ) && getAsBoolean(values,
188                         Calls.IS_READ)) {
189                     // If the server has set the IS_READ, it should also unset the new flag
190                     if (!values.containsKey(Calls.NEW)) {
191                         values.put(Calls.NEW, 0);
192                         hasMarkedRead = true;
193                     }
194                 }
195             }
196         }
197 
198         int count = mDb.update(table, values, whereClause, whereArgs);
199         if (count > 0 && isVoicemail) {
200             notifyVoicemailChange(mBaseUri, packagesModified, Intent.ACTION_PROVIDER_CHANGED);
201         }
202         if (count > 0 && mIsCallsTable) {
203             notifyCallLogChange();
204         }
205         if (hasMarkedRead) {
206             // A "New" voicemail has been marked as read by the server. This voicemail is no longer
207             // new but the content consumer might still think it is. ACTION_NEW_VOICEMAIL should
208             // trigger a rescan of new voicemails.
209             mContext.sendBroadcast(
210                     new Intent(VoicemailContract.ACTION_NEW_VOICEMAIL, uri),
211                     READ_VOICEMAIL);
212         }
213         return count;
214     }
215 
updateLastModified(String table, String whereClause, String[] whereArgs)216     private void updateLastModified(String table, String whereClause, String[] whereArgs) {
217         ContentValues values = new ContentValues();
218         values.put(Calls.LAST_MODIFIED, getTimeMillis());
219 
220         mDb.update(table, values,
221                 DbQueryUtils.concatenateClauses(NOT_DELETED_SELECTION, whereClause),
222                 whereArgs);
223     }
224 
225     @Override
delete(String table, String whereClause, String[] whereArgs)226     public int delete(String table, String whereClause, String[] whereArgs) {
227         Set<String> packagesModified = getModifiedPackages(whereClause, whereArgs);
228         boolean isVoicemail = packagesModified.size() != 0;
229 
230         // If a deletion is made by a package that is not the package that inserted the voicemail,
231         // this means that the user deleted the voicemail. However, we do not want to delete it from
232         // the database until after the server has been notified of the deletion. To ensure this,
233         // mark the entry as "deleted"--deleted entries should be hidden from the user.
234         // Once the changes are synced to the server, delete will be called again, this time
235         // removing the rows from the table.
236         // If the deletion is being made by the package that inserted the voicemail or by
237         // CP2 (cleanup after uninstall), then we don't need to wait for sync, so just delete it.
238         final int count;
239         if (mIsCallsTable && isVoicemail && !isSelfModifyingOrInternal(packagesModified)) {
240             ContentValues values = new ContentValues();
241             values.put(VoicemailContract.Voicemails.DIRTY, 1);
242             values.put(VoicemailContract.Voicemails.DELETED, 1);
243             values.put(VoicemailContract.Voicemails.LAST_MODIFIED, getTimeMillis());
244             count = mDb.update(table, values, whereClause, whereArgs);
245         } else {
246             count = mDb.delete(table, whereClause, whereArgs);
247         }
248 
249         if (count > 0 && isVoicemail) {
250             notifyVoicemailChange(mBaseUri, packagesModified, Intent.ACTION_PROVIDER_CHANGED);
251         }
252         if (count > 0 && mIsCallsTable) {
253             notifyCallLogChange();
254         }
255         return count;
256     }
257 
258     /**
259      * Returns the set of packages affected when a modify operation is run for the specified
260      * where clause. When called from an insert operation an empty set returned by this method
261      * implies (indirectly) that this does not affect any voicemail entry, as a voicemail entry is
262      * always expected to have the source package field set.
263      */
getModifiedPackages(String whereClause, String[] whereArgs)264     private Set<String> getModifiedPackages(String whereClause, String[] whereArgs) {
265         Set<String> modifiedPackages = new HashSet<String>();
266         Cursor cursor = mDb.query(mTableName, PROJECTION,
267                 DbQueryUtils.concatenateClauses(NON_NULL_SOURCE_PACKAGE_SELECTION, whereClause),
268                 whereArgs, null, null, null);
269         while(cursor.moveToNext()) {
270             modifiedPackages.add(cursor.getString(SOURCE_PACKAGE_COLUMN_INDEX));
271         }
272         MoreCloseables.closeQuietly(cursor);
273         return modifiedPackages;
274     }
275 
276     /**
277      * Returns the source package that gets affected (in an insert/update operation) by the supplied
278      * content values. An empty set returned by this method also implies (indirectly) that this does
279      * not affect any voicemail entry, as a voicemail entry is always expected to have the source
280      * package field set.
281      */
getModifiedPackages(ContentValues values)282     private Set<String> getModifiedPackages(ContentValues values) {
283         Set<String> impactedPackages = new HashSet<String>();
284         if(values.containsKey(VoicemailContract.SOURCE_PACKAGE_FIELD)) {
285             impactedPackages.add(values.getAsString(VoicemailContract.SOURCE_PACKAGE_FIELD));
286         }
287         return impactedPackages;
288     }
289 
290     /**
291      * @param packagesModified source packages that inserted the voicemail that is being modified
292      * @return {@code true} if the caller is modifying its own voicemail, or this is an internal
293      *         transaction, {@code false} otherwise.
294      */
isSelfModifyingOrInternal(Set<String> packagesModified)295     private boolean isSelfModifyingOrInternal(Set<String> packagesModified) {
296         final Collection<String> callingPackages = getCallingPackages();
297         if (callingPackages == null) {
298             return false;
299         }
300         // The last clause has the same effect as doing Process.myUid() == Binder.getCallingUid(),
301         // but allows us to mock the results for testing.
302         return packagesModified.size() == 1 && (callingPackages.contains(
303                 Iterables.getOnlyElement(packagesModified))
304                         || callingPackages.contains(mContext.getPackageName()));
305     }
306 
notifyVoicemailChange(Uri notificationUri, Set<String> modifiedPackages, String... intentActions)307     private void notifyVoicemailChange(Uri notificationUri, Set<String> modifiedPackages,
308             String... intentActions) {
309         // Notify the observers.
310         // Must be done only once, even if there are multiple broadcast intents.
311         mContext.getContentResolver().notifyChange(notificationUri, null, true);
312         Collection<String> callingPackages = getCallingPackages();
313         // Now fire individual intents.
314         for (String intentAction : intentActions) {
315             // self_change extra should be included only for provider_changed events.
316             boolean includeSelfChangeExtra = intentAction.equals(Intent.ACTION_PROVIDER_CHANGED);
317             for (ComponentName component :
318                     getBroadcastReceiverComponents(intentAction, notificationUri)) {
319                 // Ignore any package that is not affected by the change and don't have full access
320                 // either.
321                 if (!modifiedPackages.contains(component.getPackageName()) &&
322                         !mVoicemailPermissions.packageHasReadAccess(
323                                 component.getPackageName())) {
324                     continue;
325                 }
326 
327                 Intent intent = new Intent(intentAction, notificationUri);
328                 intent.setComponent(component);
329                 if (includeSelfChangeExtra && callingPackages != null) {
330                     intent.putExtra(VoicemailContract.EXTRA_SELF_CHANGE,
331                             callingPackages.contains(component.getPackageName()));
332                 }
333                 String permissionNeeded = modifiedPackages.contains(component.getPackageName()) ?
334                         ADD_VOICEMAIL : READ_VOICEMAIL;
335                 mContext.sendBroadcast(intent, permissionNeeded);
336                 Log.v(TAG, String.format("Sent intent. act:%s, url:%s, comp:%s, perm:%s," +
337                         " self_change:%s", intent.getAction(), intent.getData(),
338                         component.getClassName(), permissionNeeded,
339                         intent.hasExtra(VoicemailContract.EXTRA_SELF_CHANGE) ?
340                                 intent.getBooleanExtra(VoicemailContract.EXTRA_SELF_CHANGE, false) :
341                                         null));
342             }
343         }
344     }
345 
346     /** Determines the components that can possibly receive the specified intent. */
getBroadcastReceiverComponents(String intentAction, Uri uri)347     private List<ComponentName> getBroadcastReceiverComponents(String intentAction, Uri uri) {
348         Intent intent = new Intent(intentAction, uri);
349         List<ComponentName> receiverComponents = new ArrayList<ComponentName>();
350         // For broadcast receivers ResolveInfo.activityInfo is the one that is populated.
351         for (ResolveInfo resolveInfo :
352                 mContext.getPackageManager().queryBroadcastReceivers(intent, 0)) {
353             ActivityInfo activityInfo = resolveInfo.activityInfo;
354             receiverComponents.add(new ComponentName(activityInfo.packageName, activityInfo.name));
355         }
356         return receiverComponents;
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