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