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