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