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