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 com.android.providers.contacts.Manifest.permission.READ_WRITE_ALL_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.ContactsDatabaseHelper.Tables;
43import com.android.providers.contacts.util.DbQueryUtils;
44import com.google.android.collect.Lists;
45
46import java.util.ArrayList;
47import java.util.Collection;
48import java.util.HashSet;
49import java.util.List;
50import java.util.Set;
51
52/**
53 * An implementation of {@link DatabaseModifier} for voicemail related tables which additionally
54 * generates necessary notifications after the modification operation is performed.
55 * The class generates notifications for both voicemail as well as call log URI depending on which
56 * of then got affected by the change.
57 */
58public class DbModifierWithNotification implements DatabaseModifier {
59    private static final String TAG = "DbModifierWithVmNotification";
60
61    private static final String[] PROJECTION = new String[] {
62            VoicemailContract.SOURCE_PACKAGE_FIELD
63    };
64    private static final int SOURCE_PACKAGE_COLUMN_INDEX = 0;
65    private static final String NON_NULL_SOURCE_PACKAGE_SELECTION =
66            VoicemailContract.SOURCE_PACKAGE_FIELD + " IS NOT NULL";
67
68    private final String mTableName;
69    private final SQLiteDatabase mDb;
70    private final InsertHelper mInsertHelper;
71    private final Context mContext;
72    private final Uri mBaseUri;
73    private final boolean mIsCallsTable;
74    private final VoicemailPermissions mVoicemailPermissions;
75
76    public DbModifierWithNotification(String tableName, SQLiteDatabase db, Context context) {
77        this(tableName, db, null, context);
78    }
79
80    public DbModifierWithNotification(String tableName, InsertHelper insertHelper,
81            Context context) {
82        this(tableName, null, insertHelper, context);
83    }
84
85    private DbModifierWithNotification(String tableName, SQLiteDatabase db,
86            InsertHelper insertHelper, Context context) {
87        mTableName = tableName;
88        mDb = db;
89        mInsertHelper = insertHelper;
90        mContext = context;
91        mBaseUri = mTableName.equals(Tables.VOICEMAIL_STATUS) ?
92                Status.CONTENT_URI : Voicemails.CONTENT_URI;
93        mIsCallsTable = mTableName.equals(Tables.CALLS);
94        mVoicemailPermissions = new VoicemailPermissions(mContext);
95    }
96
97    @Override
98    public long insert(String table, String nullColumnHack, ContentValues values) {
99        Set<String> packagesModified = getModifiedPackages(values);
100        long rowId = mDb.insert(table, nullColumnHack, values);
101        if (rowId > 0 && packagesModified.size() != 0) {
102            notifyVoicemailChangeOnInsert(ContentUris.withAppendedId(mBaseUri, rowId),
103                    packagesModified);
104        }
105        if (rowId > 0 && mIsCallsTable) {
106            notifyCallLogChange();
107        }
108        return rowId;
109    }
110
111    @Override
112    public long insert(ContentValues values) {
113        Set<String> packagesModified = getModifiedPackages(values);
114        long rowId = mInsertHelper.insert(values);
115        if (rowId > 0 && packagesModified.size() != 0) {
116            notifyVoicemailChangeOnInsert(
117                    ContentUris.withAppendedId(mBaseUri, rowId), packagesModified);
118        }
119        if (rowId > 0 && mIsCallsTable) {
120            notifyCallLogChange();
121        }
122        return rowId;
123    }
124
125    private void notifyCallLogChange() {
126        mContext.getContentResolver().notifyChange(Calls.CONTENT_URI, null, false);
127    }
128
129    private void notifyVoicemailChangeOnInsert(Uri notificationUri, Set<String> packagesModified) {
130        if (mIsCallsTable) {
131            notifyVoicemailChange(notificationUri, packagesModified,
132                    VoicemailContract.ACTION_NEW_VOICEMAIL, Intent.ACTION_PROVIDER_CHANGED);
133        } else {
134            notifyVoicemailChange(notificationUri, packagesModified,
135                    Intent.ACTION_PROVIDER_CHANGED);
136        }
137    }
138
139    @Override
140    public int update(String table, ContentValues values, String whereClause, String[] whereArgs) {
141        Set<String> packagesModified = getModifiedPackages(whereClause, whereArgs);
142        packagesModified.addAll(getModifiedPackages(values));
143        int count = mDb.update(table, values, whereClause, whereArgs);
144        if (count > 0 && packagesModified.size() != 0) {
145            notifyVoicemailChange(mBaseUri, packagesModified, Intent.ACTION_PROVIDER_CHANGED);
146        }
147        if (count > 0 && mIsCallsTable) {
148            notifyCallLogChange();
149        }
150        return count;
151    }
152
153    @Override
154    public int delete(String table, String whereClause, String[] whereArgs) {
155        Set<String> packagesModified = getModifiedPackages(whereClause, whereArgs);
156        int count = mDb.delete(table, whereClause, whereArgs);
157        if (count > 0 && packagesModified.size() != 0) {
158            notifyVoicemailChange(mBaseUri, packagesModified, Intent.ACTION_PROVIDER_CHANGED);
159        }
160        if (count > 0 && mIsCallsTable) {
161            notifyCallLogChange();
162        }
163        return count;
164    }
165
166    /**
167     * Returns the set of packages affected when a modify operation is run for the specified
168     * where clause. When called from an insert operation an empty set returned by this method
169     * implies (indirectly) that this does not affect any voicemail entry, as a voicemail entry is
170     * always expected to have the source package field set.
171     */
172    private Set<String> getModifiedPackages(String whereClause, String[] whereArgs) {
173        Set<String> modifiedPackages = new HashSet<String>();
174        Cursor cursor = mDb.query(mTableName, PROJECTION,
175                DbQueryUtils.concatenateClauses(NON_NULL_SOURCE_PACKAGE_SELECTION, whereClause),
176                whereArgs, null, null, null);
177        while(cursor.moveToNext()) {
178            modifiedPackages.add(cursor.getString(SOURCE_PACKAGE_COLUMN_INDEX));
179        }
180        MoreCloseables.closeQuietly(cursor);
181        return modifiedPackages;
182    }
183
184    /**
185     * Returns the source package that gets affected (in an insert/update operation) by the supplied
186     * content values. An empty set returned by this method also implies (indirectly) that this does
187     * not affect any voicemail entry, as a voicemail entry is always expected to have the source
188     * package field set.
189     */
190    private Set<String> getModifiedPackages(ContentValues values) {
191        Set<String> impactedPackages = new HashSet<String>();
192        if(values.containsKey(VoicemailContract.SOURCE_PACKAGE_FIELD)) {
193            impactedPackages.add(values.getAsString(VoicemailContract.SOURCE_PACKAGE_FIELD));
194        }
195        return impactedPackages;
196    }
197
198    private void notifyVoicemailChange(Uri notificationUri, Set<String> modifiedPackages,
199            String... intentActions) {
200        // Notify the observers.
201        // Must be done only once, even if there are multiple broadcast intents.
202        mContext.getContentResolver().notifyChange(notificationUri, null, true);
203        Collection<String> callingPackages = getCallingPackages();
204        // Now fire individual intents.
205        for (String intentAction : intentActions) {
206            // self_change extra should be included only for provider_changed events.
207            boolean includeSelfChangeExtra = intentAction.equals(Intent.ACTION_PROVIDER_CHANGED);
208            for (ComponentName component :
209                    getBroadcastReceiverComponents(intentAction, notificationUri)) {
210                // Ignore any package that is not affected by the change and don't have full access
211                // either.
212                if (!modifiedPackages.contains(component.getPackageName()) &&
213                        !mVoicemailPermissions.packageHasFullAccess(component.getPackageName())) {
214                    continue;
215                }
216
217                Intent intent = new Intent(intentAction, notificationUri);
218                intent.setComponent(component);
219                if (includeSelfChangeExtra && callingPackages != null) {
220                    intent.putExtra(VoicemailContract.EXTRA_SELF_CHANGE,
221                            callingPackages.contains(component.getPackageName()));
222                }
223                String permissionNeeded = modifiedPackages.contains(component.getPackageName()) ?
224                        ADD_VOICEMAIL : READ_WRITE_ALL_VOICEMAIL;
225                mContext.sendBroadcast(intent, permissionNeeded);
226                Log.v(TAG, String.format("Sent intent. act:%s, url:%s, comp:%s, perm:%s," +
227                        " self_change:%s", intent.getAction(), intent.getData(),
228                        component.getClassName(), permissionNeeded,
229                        intent.hasExtra(VoicemailContract.EXTRA_SELF_CHANGE) ?
230                                intent.getBooleanExtra(VoicemailContract.EXTRA_SELF_CHANGE, false) :
231                                        null));
232            }
233        }
234    }
235
236    /** Determines the components that can possibly receive the specified intent. */
237    private List<ComponentName> getBroadcastReceiverComponents(String intentAction, Uri uri) {
238        Intent intent = new Intent(intentAction, uri);
239        List<ComponentName> receiverComponents = new ArrayList<ComponentName>();
240        // For broadcast receivers ResolveInfo.activityInfo is the one that is populated.
241        for (ResolveInfo resolveInfo :
242                mContext.getPackageManager().queryBroadcastReceivers(intent, 0)) {
243            ActivityInfo activityInfo = resolveInfo.activityInfo;
244            receiverComponents.add(new ComponentName(activityInfo.packageName, activityInfo.name));
245        }
246        return receiverComponents;
247    }
248
249    /**
250     * Returns the package names of the calling process. If the calling process has more than
251     * one packages, this returns them all
252     */
253    private Collection<String> getCallingPackages() {
254        int caller = Binder.getCallingUid();
255        if (caller == 0) {
256            return null;
257        }
258        return Lists.newArrayList(mContext.getPackageManager().getPackagesForUid(caller));
259    }
260}
261