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