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