Home | History | Annotate | Download | only in contacts
      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 
     76     public DbModifierWithNotification(String tableName, SQLiteDatabase db, Context context) {
     77         this(tableName, db, null, context);
     78     }
     79 
     80     public DbModifierWithNotification(String tableName, InsertHelper insertHelper,
     81             Context context) {
     82         this(tableName, null, insertHelper, context);
     83     }
     84 
     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
     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
    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 
    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 
    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
    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
    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      */
    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      */
    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      */
    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 
    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. */
    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      */
    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      */
    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