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 com.android.providers.contacts.Manifest.permission.READ_WRITE_ALL_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.ContactsDatabaseHelper.Tables;
     43 import com.android.providers.contacts.util.DbQueryUtils;
     44 import com.google.android.collect.Lists;
     45 
     46 import java.util.ArrayList;
     47 import java.util.Collection;
     48 import java.util.HashSet;
     49 import java.util.List;
     50 import java.util.Set;
     51 
     52 /**
     53  * An implementation of {@link DatabaseModifier} for voicemail related tables which additionally
     54  * generates necessary notifications after the modification operation is performed.
     55  * The class generates notifications for both voicemail as well as call log URI depending on which
     56  * of then got affected by the change.
     57  */
     58 public class DbModifierWithNotification implements DatabaseModifier {
     59     private static final String TAG = "DbModifierWithVmNotification";
     60 
     61     private static final String[] PROJECTION = new String[] {
     62             VoicemailContract.SOURCE_PACKAGE_FIELD
     63     };
     64     private static final int SOURCE_PACKAGE_COLUMN_INDEX = 0;
     65     private static final String NON_NULL_SOURCE_PACKAGE_SELECTION =
     66             VoicemailContract.SOURCE_PACKAGE_FIELD + " IS NOT NULL";
     67 
     68     private final String mTableName;
     69     private final SQLiteDatabase mDb;
     70     private final InsertHelper mInsertHelper;
     71     private final Context mContext;
     72     private final Uri mBaseUri;
     73     private final boolean mIsCallsTable;
     74     private final VoicemailPermissions mVoicemailPermissions;
     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         long rowId = mDb.insert(table, nullColumnHack, values);
    101         if (rowId > 0 && packagesModified.size() != 0) {
    102             notifyVoicemailChangeOnInsert(ContentUris.withAppendedId(mBaseUri, rowId),
    103                     packagesModified);
    104         }
    105         if (rowId > 0 && mIsCallsTable) {
    106             notifyCallLogChange();
    107         }
    108         return rowId;
    109     }
    110 
    111     @Override
    112     public long insert(ContentValues values) {
    113         Set<String> packagesModified = getModifiedPackages(values);
    114         long rowId = mInsertHelper.insert(values);
    115         if (rowId > 0 && packagesModified.size() != 0) {
    116             notifyVoicemailChangeOnInsert(
    117                     ContentUris.withAppendedId(mBaseUri, rowId), packagesModified);
    118         }
    119         if (rowId > 0 && mIsCallsTable) {
    120             notifyCallLogChange();
    121         }
    122         return rowId;
    123     }
    124 
    125     private void notifyCallLogChange() {
    126         mContext.getContentResolver().notifyChange(Calls.CONTENT_URI, null, false);
    127     }
    128 
    129     private void notifyVoicemailChangeOnInsert(Uri notificationUri, Set<String> packagesModified) {
    130         if (mIsCallsTable) {
    131             notifyVoicemailChange(notificationUri, packagesModified,
    132                     VoicemailContract.ACTION_NEW_VOICEMAIL, Intent.ACTION_PROVIDER_CHANGED);
    133         } else {
    134             notifyVoicemailChange(notificationUri, packagesModified,
    135                     Intent.ACTION_PROVIDER_CHANGED);
    136         }
    137     }
    138 
    139     @Override
    140     public int update(String table, ContentValues values, String whereClause, String[] whereArgs) {
    141         Set<String> packagesModified = getModifiedPackages(whereClause, whereArgs);
    142         packagesModified.addAll(getModifiedPackages(values));
    143         int count = mDb.update(table, values, whereClause, whereArgs);
    144         if (count > 0 && packagesModified.size() != 0) {
    145             notifyVoicemailChange(mBaseUri, packagesModified, Intent.ACTION_PROVIDER_CHANGED);
    146         }
    147         if (count > 0 && mIsCallsTable) {
    148             notifyCallLogChange();
    149         }
    150         return count;
    151     }
    152 
    153     @Override
    154     public int delete(String table, String whereClause, String[] whereArgs) {
    155         Set<String> packagesModified = getModifiedPackages(whereClause, whereArgs);
    156         int count = mDb.delete(table, whereClause, whereArgs);
    157         if (count > 0 && packagesModified.size() != 0) {
    158             notifyVoicemailChange(mBaseUri, packagesModified, Intent.ACTION_PROVIDER_CHANGED);
    159         }
    160         if (count > 0 && mIsCallsTable) {
    161             notifyCallLogChange();
    162         }
    163         return count;
    164     }
    165 
    166     /**
    167      * Returns the set of packages affected when a modify operation is run for the specified
    168      * where clause. When called from an insert operation an empty set returned by this method
    169      * implies (indirectly) that this does not affect any voicemail entry, as a voicemail entry is
    170      * always expected to have the source package field set.
    171      */
    172     private Set<String> getModifiedPackages(String whereClause, String[] whereArgs) {
    173         Set<String> modifiedPackages = new HashSet<String>();
    174         Cursor cursor = mDb.query(mTableName, PROJECTION,
    175                 DbQueryUtils.concatenateClauses(NON_NULL_SOURCE_PACKAGE_SELECTION, whereClause),
    176                 whereArgs, null, null, null);
    177         while(cursor.moveToNext()) {
    178             modifiedPackages.add(cursor.getString(SOURCE_PACKAGE_COLUMN_INDEX));
    179         }
    180         MoreCloseables.closeQuietly(cursor);
    181         return modifiedPackages;
    182     }
    183 
    184     /**
    185      * Returns the source package that gets affected (in an insert/update operation) by the supplied
    186      * content values. An empty set returned by this method also implies (indirectly) that this does
    187      * not affect any voicemail entry, as a voicemail entry is always expected to have the source
    188      * package field set.
    189      */
    190     private Set<String> getModifiedPackages(ContentValues values) {
    191         Set<String> impactedPackages = new HashSet<String>();
    192         if(values.containsKey(VoicemailContract.SOURCE_PACKAGE_FIELD)) {
    193             impactedPackages.add(values.getAsString(VoicemailContract.SOURCE_PACKAGE_FIELD));
    194         }
    195         return impactedPackages;
    196     }
    197 
    198     private void notifyVoicemailChange(Uri notificationUri, Set<String> modifiedPackages,
    199             String... intentActions) {
    200         // Notify the observers.
    201         // Must be done only once, even if there are multiple broadcast intents.
    202         mContext.getContentResolver().notifyChange(notificationUri, null, true);
    203         Collection<String> callingPackages = getCallingPackages();
    204         // Now fire individual intents.
    205         for (String intentAction : intentActions) {
    206             // self_change extra should be included only for provider_changed events.
    207             boolean includeSelfChangeExtra = intentAction.equals(Intent.ACTION_PROVIDER_CHANGED);
    208             for (ComponentName component :
    209                     getBroadcastReceiverComponents(intentAction, notificationUri)) {
    210                 // Ignore any package that is not affected by the change and don't have full access
    211                 // either.
    212                 if (!modifiedPackages.contains(component.getPackageName()) &&
    213                         !mVoicemailPermissions.packageHasFullAccess(component.getPackageName())) {
    214                     continue;
    215                 }
    216 
    217                 Intent intent = new Intent(intentAction, notificationUri);
    218                 intent.setComponent(component);
    219                 if (includeSelfChangeExtra && callingPackages != null) {
    220                     intent.putExtra(VoicemailContract.EXTRA_SELF_CHANGE,
    221                             callingPackages.contains(component.getPackageName()));
    222                 }
    223                 String permissionNeeded = modifiedPackages.contains(component.getPackageName()) ?
    224                         ADD_VOICEMAIL : READ_WRITE_ALL_VOICEMAIL;
    225                 mContext.sendBroadcast(intent, permissionNeeded);
    226                 Log.v(TAG, String.format("Sent intent. act:%s, url:%s, comp:%s, perm:%s," +
    227                         " self_change:%s", intent.getAction(), intent.getData(),
    228                         component.getClassName(), permissionNeeded,
    229                         intent.hasExtra(VoicemailContract.EXTRA_SELF_CHANGE) ?
    230                                 intent.getBooleanExtra(VoicemailContract.EXTRA_SELF_CHANGE, false) :
    231                                         null));
    232             }
    233         }
    234     }
    235 
    236     /** Determines the components that can possibly receive the specified intent. */
    237     private List<ComponentName> getBroadcastReceiverComponents(String intentAction, Uri uri) {
    238         Intent intent = new Intent(intentAction, uri);
    239         List<ComponentName> receiverComponents = new ArrayList<ComponentName>();
    240         // For broadcast receivers ResolveInfo.activityInfo is the one that is populated.
    241         for (ResolveInfo resolveInfo :
    242                 mContext.getPackageManager().queryBroadcastReceivers(intent, 0)) {
    243             ActivityInfo activityInfo = resolveInfo.activityInfo;
    244             receiverComponents.add(new ComponentName(activityInfo.packageName, activityInfo.name));
    245         }
    246         return receiverComponents;
    247     }
    248 
    249     /**
    250      * Returns the package names of the calling process. If the calling process has more than
    251      * one packages, this returns them all
    252      */
    253     private Collection<String> getCallingPackages() {
    254         int caller = Binder.getCallingUid();
    255         if (caller == 0) {
    256             return null;
    257         }
    258         return Lists.newArrayList(mContext.getPackageManager().getPackagesForUid(caller));
    259     }
    260 }
    261