Home | History | Annotate | Download | only in contacts
      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 
     78     public DbModifierWithNotification(String tableName, SQLiteDatabase db, Context context) {
     79         this(tableName, db, null, context);
     80     }
     81 
     82     public DbModifierWithNotification(String tableName, InsertHelper insertHelper,
     83             Context context) {
     84         this(tableName, null, insertHelper, context);
     85     }
     86 
     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
    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(
    147             Uri notificationUri, Set<String> packagesModified) {
    148         if (mIsCallsTable) {
    149             mVoicemailNotifier.addIntentActions(VoicemailContract.ACTION_NEW_VOICEMAIL);
    150         }
    151         notifyVoicemailChange(notificationUri, packagesModified);
    152     }
    153 
    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
    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 
    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 
    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 
    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
    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
    292     public void startBulkOperation() {
    293         mIsBulkOperation = true;
    294         mDb.beginTransaction();
    295     }
    296 
    297     @Override
    298     public void yieldBulkOperation() {
    299         mDb.yieldIfContendedSafely();
    300     }
    301 
    302     @Override
    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      */
    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      */
    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      */
    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      */
    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     @VisibleForTesting
    398     static void setVoicemailNotifierForTest(VoicemailNotifier notifier) {
    399         sVoicemailNotifierForTest = notifier;
    400     }
    401 }
    402