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.net.Uri; 31 import android.os.Binder; 32 import android.provider.CallLog.Calls; 33 import android.provider.VoicemailContract; 34 import android.provider.VoicemailContract.Status; 35 import android.provider.VoicemailContract.Voicemails; 36 import android.util.ArraySet; 37 38 import com.android.common.io.MoreCloseables; 39 import com.android.internal.annotations.VisibleForTesting; 40 import com.android.providers.contacts.CallLogDatabaseHelper.Tables; 41 import com.android.providers.contacts.util.DbQueryUtils; 42 43 import com.google.android.collect.Lists; 44 import com.google.common.collect.Iterables; 45 import java.util.Collection; 46 import java.util.Set; 47 48 /** 49 * An implementation of {@link DatabaseModifier} for voicemail related tables which additionally 50 * generates necessary notifications after the modification operation is performed. 51 * The class generates notifications for both voicemail as well as call log URI depending on which 52 * of then got affected by the change. 53 */ 54 public class DbModifierWithNotification implements DatabaseModifier { 55 56 private static final String TAG = "DbModifierWithNotify"; 57 58 private static final String[] PROJECTION = new String[] { 59 VoicemailContract.SOURCE_PACKAGE_FIELD 60 }; 61 private static final int SOURCE_PACKAGE_COLUMN_INDEX = 0; 62 private static final String NON_NULL_SOURCE_PACKAGE_SELECTION = 63 VoicemailContract.SOURCE_PACKAGE_FIELD + " IS NOT NULL"; 64 private static final String NOT_DELETED_SELECTION = 65 Voicemails.DELETED + " == 0"; 66 private final String mTableName; 67 private final SQLiteDatabase mDb; 68 private final InsertHelper mInsertHelper; 69 private final Context mContext; 70 private final Uri mBaseUri; 71 private final boolean mIsCallsTable; 72 private final VoicemailNotifier mVoicemailNotifier; 73 74 private boolean mIsBulkOperation = false; 75 76 private static VoicemailNotifier sVoicemailNotifierForTest; 77 DbModifierWithNotification(String tableName, SQLiteDatabase db, Context context)78 public DbModifierWithNotification(String tableName, SQLiteDatabase db, Context context) { 79 this(tableName, db, null, context); 80 } 81 DbModifierWithNotification(String tableName, InsertHelper insertHelper, Context context)82 public DbModifierWithNotification(String tableName, InsertHelper insertHelper, 83 Context context) { 84 this(tableName, null, insertHelper, context); 85 } 86 DbModifierWithNotification(String tableName, SQLiteDatabase db, InsertHelper insertHelper, Context context)87 private DbModifierWithNotification(String tableName, SQLiteDatabase db, 88 InsertHelper insertHelper, Context context) { 89 mTableName = tableName; 90 mDb = db; 91 mInsertHelper = insertHelper; 92 mContext = context; 93 mBaseUri = mTableName.equals(Tables.VOICEMAIL_STATUS) ? 94 Status.CONTENT_URI : Voicemails.CONTENT_URI; 95 mIsCallsTable = mTableName.equals(Tables.CALLS); 96 mVoicemailNotifier = sVoicemailNotifierForTest != null ? sVoicemailNotifierForTest 97 : new VoicemailNotifier(mContext, mBaseUri); 98 } 99 100 @Override insert(String table, String nullColumnHack, ContentValues values)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 insert(ContentValues values)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 notifyCallLogChange()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 notifyVoicemailChangeOnInsert( Uri notificationUri, Set<String> packagesModified)146 private void notifyVoicemailChangeOnInsert( 147 Uri notificationUri, Set<String> packagesModified) { 148 if (mIsCallsTable) { 149 mVoicemailNotifier.addIntentActions(VoicemailContract.ACTION_NEW_VOICEMAIL); 150 } 151 notifyVoicemailChange(notificationUri, packagesModified); 152 } 153 notifyVoicemailChange(Uri notificationUri, Set<String> modifiedPackages)154 private void notifyVoicemailChange(Uri notificationUri, 155 Set<String> modifiedPackages) { 156 mVoicemailNotifier.addUri(notificationUri); 157 mVoicemailNotifier.addModifiedPackages(modifiedPackages); 158 mVoicemailNotifier.addIntentActions(Intent.ACTION_PROVIDER_CHANGED); 159 if (!mIsBulkOperation) { 160 mVoicemailNotifier.sendNotification(); 161 } 162 } 163 164 @Override update(Uri uri, String table, ContentValues values, String whereClause, String[] whereArgs)165 public int update(Uri uri, String table, ContentValues values, String whereClause, 166 String[] whereArgs) { 167 Set<String> packagesModified = getModifiedPackages(whereClause, whereArgs); 168 packagesModified.addAll(getModifiedPackages(values)); 169 170 boolean isVoicemailContent = 171 packagesModified.size() != 0 && isUpdatingVoicemailColumns(values); 172 173 boolean hasMarkedRead = false; 174 if (mIsCallsTable) { 175 if (values.containsKey(Voicemails.DELETED) 176 && !values.getAsBoolean(Voicemails.DELETED)) { 177 values.put(Calls.LAST_MODIFIED, getTimeMillis()); 178 } else { 179 updateLastModified(table, whereClause, whereArgs); 180 } 181 if (isVoicemailContent) { 182 if (updateDirtyFlag(values, packagesModified)) { 183 if (values.containsKey(Calls.IS_READ) 184 && getAsBoolean(values, 185 Calls.IS_READ)) { 186 // If the server has set the IS_READ, it should also unset the new flag 187 if (!values.containsKey(Calls.NEW)) { 188 values.put(Calls.NEW, 0); 189 hasMarkedRead = true; 190 } 191 } 192 } 193 } 194 } 195 // updateDirtyFlag might remove the value and leave values empty. 196 if (values.isEmpty()) { 197 return 0; 198 } 199 int count = mDb.update(table, values, whereClause, whereArgs); 200 if (count > 0 && isVoicemailContent || Tables.VOICEMAIL_STATUS.equals(table)) { 201 notifyVoicemailChange(mBaseUri, packagesModified); 202 } 203 if (count > 0 && mIsCallsTable) { 204 notifyCallLogChange(); 205 } 206 if (hasMarkedRead) { 207 // A "New" voicemail has been marked as read by the server. This voicemail is no longer 208 // new but the content consumer might still think it is. ACTION_NEW_VOICEMAIL should 209 // trigger a rescan of new voicemails. 210 mContext.sendBroadcast( 211 new Intent(VoicemailContract.ACTION_NEW_VOICEMAIL, uri), 212 READ_VOICEMAIL); 213 } 214 return count; 215 } 216 updateDirtyFlag(ContentValues values, Set<String> packagesModified)217 private boolean updateDirtyFlag(ContentValues values, Set<String> packagesModified) { 218 // If a calling package is modifying its own entries, it means that the change came 219 // from the server and thus is synced or "clean". Otherwise, it means that a local 220 // change is being made to the database, so the entries should be marked as "dirty" 221 // so that the corresponding sync adapter knows they need to be synced. 222 int isDirty; 223 Integer callerSetDirty = values.getAsInteger(Voicemails.DIRTY); 224 if (callerSetDirty != null) { 225 // Respect the calling package if it sets the dirty flag 226 if (callerSetDirty == Voicemails.DIRTY_RETAIN) { 227 values.remove(Voicemails.DIRTY); 228 return false; 229 } else { 230 isDirty = callerSetDirty == 0 ? 0 : 1; 231 } 232 } else { 233 isDirty = isSelfModifyingOrInternal(packagesModified) ? 0 : 1; 234 } 235 236 values.put(Voicemails.DIRTY, isDirty); 237 return isDirty == 0; 238 } 239 isUpdatingVoicemailColumns(ContentValues values)240 private boolean isUpdatingVoicemailColumns(ContentValues values) { 241 for (String key : values.keySet()) { 242 if (VoicemailContentTable.ALLOWED_COLUMNS.contains(key)) { 243 return true; 244 } 245 } 246 return false; 247 } 248 updateLastModified(String table, String whereClause, String[] whereArgs)249 private void updateLastModified(String table, String whereClause, String[] whereArgs) { 250 ContentValues values = new ContentValues(); 251 values.put(Calls.LAST_MODIFIED, getTimeMillis()); 252 253 mDb.update(table, values, 254 DbQueryUtils.concatenateClauses(NOT_DELETED_SELECTION, whereClause), 255 whereArgs); 256 } 257 258 @Override delete(String table, String whereClause, String[] whereArgs)259 public int delete(String table, String whereClause, String[] whereArgs) { 260 Set<String> packagesModified = getModifiedPackages(whereClause, whereArgs); 261 boolean isVoicemail = packagesModified.size() != 0; 262 263 // If a deletion is made by a package that is not the package that inserted the voicemail, 264 // this means that the user deleted the voicemail. However, we do not want to delete it from 265 // the database until after the server has been notified of the deletion. To ensure this, 266 // mark the entry as "deleted"--deleted entries should be hidden from the user. 267 // Once the changes are synced to the server, delete will be called again, this time 268 // removing the rows from the table. 269 // If the deletion is being made by the package that inserted the voicemail or by 270 // CP2 (cleanup after uninstall), then we don't need to wait for sync, so just delete it. 271 final int count; 272 if (mIsCallsTable && isVoicemail && !isSelfModifyingOrInternal(packagesModified)) { 273 ContentValues values = new ContentValues(); 274 values.put(VoicemailContract.Voicemails.DIRTY, 1); 275 values.put(VoicemailContract.Voicemails.DELETED, 1); 276 values.put(VoicemailContract.Voicemails.LAST_MODIFIED, getTimeMillis()); 277 count = mDb.update(table, values, whereClause, whereArgs); 278 } else { 279 count = mDb.delete(table, whereClause, whereArgs); 280 } 281 282 if (count > 0 && isVoicemail) { 283 notifyVoicemailChange(mBaseUri, packagesModified); 284 } 285 if (count > 0 && mIsCallsTable) { 286 notifyCallLogChange(); 287 } 288 return count; 289 } 290 291 @Override startBulkOperation()292 public void startBulkOperation() { 293 mIsBulkOperation = true; 294 mDb.beginTransaction(); 295 } 296 297 @Override yieldBulkOperation()298 public void yieldBulkOperation() { 299 mDb.yieldIfContendedSafely(); 300 } 301 302 @Override finishBulkOperation()303 public void finishBulkOperation() { 304 mDb.setTransactionSuccessful(); 305 mDb.endTransaction(); 306 mIsBulkOperation = false; 307 mVoicemailNotifier.sendNotification(); 308 } 309 310 /** 311 * Returns the set of packages affected when a modify operation is run for the specified 312 * where clause. When called from an insert operation an empty set returned by this method 313 * implies (indirectly) that this does not affect any voicemail entry, as a voicemail entry is 314 * always expected to have the source package field set. 315 */ getModifiedPackages(String whereClause, String[] whereArgs)316 private Set<String> getModifiedPackages(String whereClause, String[] whereArgs) { 317 Set<String> modifiedPackages = new ArraySet<>(); 318 Cursor cursor = mDb.query(mTableName, PROJECTION, 319 DbQueryUtils.concatenateClauses(NON_NULL_SOURCE_PACKAGE_SELECTION, whereClause), 320 whereArgs, null, null, null); 321 while (cursor.moveToNext()) { 322 modifiedPackages.add(cursor.getString(SOURCE_PACKAGE_COLUMN_INDEX)); 323 } 324 MoreCloseables.closeQuietly(cursor); 325 return modifiedPackages; 326 } 327 328 /** 329 * Returns the source package that gets affected (in an insert/update operation) by the supplied 330 * content values. An empty set returned by this method also implies (indirectly) that this does 331 * not affect any voicemail entry, as a voicemail entry is always expected to have the source 332 * package field set. 333 */ getModifiedPackages(ContentValues values)334 private Set<String> getModifiedPackages(ContentValues values) { 335 Set<String> impactedPackages = new ArraySet<>(); 336 if (values.containsKey(VoicemailContract.SOURCE_PACKAGE_FIELD)) { 337 impactedPackages.add(values.getAsString(VoicemailContract.SOURCE_PACKAGE_FIELD)); 338 } 339 return impactedPackages; 340 } 341 342 /** 343 * @param packagesModified source packages that inserted the voicemail that is being modified 344 * @return {@code true} if the caller is modifying its own voicemail, or this is an internal 345 * transaction, {@code false} otherwise. 346 */ isSelfModifyingOrInternal(Set<String> packagesModified)347 private boolean isSelfModifyingOrInternal(Set<String> packagesModified) { 348 final Collection<String> callingPackages = getCallingPackages(); 349 if (callingPackages == null) { 350 return false; 351 } 352 // The last clause has the same effect as doing Process.myUid() == Binder.getCallingUid(), 353 // but allows us to mock the results for testing. 354 return packagesModified.size() == 1 && (callingPackages.contains( 355 Iterables.getOnlyElement(packagesModified)) 356 || callingPackages.contains(mContext.getPackageName())); 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 */ getCallingPackages()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 */ getAsBoolean(ContentValues values, String key)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 getTimeMillis()390 private long getTimeMillis() { 391 if (CallLogProvider.getTimeForTestMillis() == null) { 392 return System.currentTimeMillis(); 393 } 394 return CallLogProvider.getTimeForTestMillis(); 395 } 396 397 @VisibleForTesting setVoicemailNotifierForTest(VoicemailNotifier notifier)398 static void setVoicemailNotifierForTest(VoicemailNotifier notifier) { 399 sVoicemailNotifierForTest = notifier; 400 } 401 } 402