blob: fda8321ac363cad31ca98a7347a4f15db62b5de0 [file] [log] [blame]
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.providers.contacts;
import static android.Manifest.permission.ADD_VOICEMAIL;
import static com.android.providers.contacts.Manifest.permission.READ_WRITE_ALL_VOICEMAIL;
import android.content.ComponentName;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ResolveInfo;
import android.database.Cursor;
import android.database.DatabaseUtils.InsertHelper;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Binder;
import android.provider.CallLog.Calls;
import android.provider.VoicemailContract;
import android.provider.VoicemailContract.Status;
import android.provider.VoicemailContract.Voicemails;
import android.util.Log;
import com.android.common.io.MoreCloseables;
import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
import com.android.providers.contacts.util.DbQueryUtils;
import com.google.android.collect.Lists;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* An implementation of {@link DatabaseModifier} for voicemail related tables which additionally
* generates necessary notifications after the modification operation is performed.
* The class generates notifications for both voicemail as well as call log URI depending on which
* of then got affected by the change.
*/
public class DbModifierWithNotification implements DatabaseModifier {
private static final String TAG = "DbModifierWithVmNotification";
private static final String[] PROJECTION = new String[] {
VoicemailContract.SOURCE_PACKAGE_FIELD
};
private static final int SOURCE_PACKAGE_COLUMN_INDEX = 0;
private static final String NON_NULL_SOURCE_PACKAGE_SELECTION =
VoicemailContract.SOURCE_PACKAGE_FIELD + " IS NOT NULL";
private final String mTableName;
private final SQLiteDatabase mDb;
private final InsertHelper mInsertHelper;
private final Context mContext;
private final Uri mBaseUri;
private final boolean mIsCallsTable;
private final VoicemailPermissions mVoicemailPermissions;
public DbModifierWithNotification(String tableName, SQLiteDatabase db, Context context) {
this(tableName, db, null, context);
}
public DbModifierWithNotification(String tableName, InsertHelper insertHelper,
Context context) {
this(tableName, null, insertHelper, context);
}
private DbModifierWithNotification(String tableName, SQLiteDatabase db,
InsertHelper insertHelper, Context context) {
mTableName = tableName;
mDb = db;
mInsertHelper = insertHelper;
mContext = context;
mBaseUri = mTableName.equals(Tables.VOICEMAIL_STATUS) ?
Status.CONTENT_URI : Voicemails.CONTENT_URI;
mIsCallsTable = mTableName.equals(Tables.CALLS);
mVoicemailPermissions = new VoicemailPermissions(mContext);
}
@Override
public long insert(String table, String nullColumnHack, ContentValues values) {
Set<String> packagesModified = getModifiedPackages(values);
long rowId = mDb.insert(table, nullColumnHack, values);
if (rowId > 0 && packagesModified.size() != 0) {
notifyVoicemailChangeOnInsert(ContentUris.withAppendedId(mBaseUri, rowId),
packagesModified);
}
if (rowId > 0 && mIsCallsTable) {
notifyCallLogChange();
}
return rowId;
}
@Override
public long insert(ContentValues values) {
Set<String> packagesModified = getModifiedPackages(values);
long rowId = mInsertHelper.insert(values);
if (rowId > 0 && packagesModified.size() != 0) {
notifyVoicemailChangeOnInsert(
ContentUris.withAppendedId(mBaseUri, rowId), packagesModified);
}
if (rowId > 0 && mIsCallsTable) {
notifyCallLogChange();
}
return rowId;
}
private void notifyCallLogChange() {
mContext.getContentResolver().notifyChange(Calls.CONTENT_URI, null, false);
}
private void notifyVoicemailChangeOnInsert(Uri notificationUri, Set<String> packagesModified) {
if (mIsCallsTable) {
notifyVoicemailChange(notificationUri, packagesModified,
VoicemailContract.ACTION_NEW_VOICEMAIL, Intent.ACTION_PROVIDER_CHANGED);
} else {
notifyVoicemailChange(notificationUri, packagesModified,
Intent.ACTION_PROVIDER_CHANGED);
}
}
@Override
public int update(String table, ContentValues values, String whereClause, String[] whereArgs) {
Set<String> packagesModified = getModifiedPackages(whereClause, whereArgs);
packagesModified.addAll(getModifiedPackages(values));
int count = mDb.update(table, values, whereClause, whereArgs);
if (count > 0 && packagesModified.size() != 0) {
notifyVoicemailChange(mBaseUri, packagesModified, Intent.ACTION_PROVIDER_CHANGED);
}
if (count > 0 && mIsCallsTable) {
notifyCallLogChange();
}
return count;
}
@Override
public int delete(String table, String whereClause, String[] whereArgs) {
Set<String> packagesModified = getModifiedPackages(whereClause, whereArgs);
int count = mDb.delete(table, whereClause, whereArgs);
if (count > 0 && packagesModified.size() != 0) {
notifyVoicemailChange(mBaseUri, packagesModified, Intent.ACTION_PROVIDER_CHANGED);
}
if (count > 0 && mIsCallsTable) {
notifyCallLogChange();
}
return count;
}
/**
* Returns the set of packages affected when a modify operation is run for the specified
* where clause. When called from an insert operation an empty set returned by this method
* implies (indirectly) that this does not affect any voicemail entry, as a voicemail entry is
* always expected to have the source package field set.
*/
private Set<String> getModifiedPackages(String whereClause, String[] whereArgs) {
Set<String> modifiedPackages = new HashSet<String>();
Cursor cursor = mDb.query(mTableName, PROJECTION,
DbQueryUtils.concatenateClauses(NON_NULL_SOURCE_PACKAGE_SELECTION, whereClause),
whereArgs, null, null, null);
while(cursor.moveToNext()) {
modifiedPackages.add(cursor.getString(SOURCE_PACKAGE_COLUMN_INDEX));
}
MoreCloseables.closeQuietly(cursor);
return modifiedPackages;
}
/**
* Returns the source package that gets affected (in an insert/update operation) by the supplied
* content values. An empty set returned by this method also implies (indirectly) that this does
* not affect any voicemail entry, as a voicemail entry is always expected to have the source
* package field set.
*/
private Set<String> getModifiedPackages(ContentValues values) {
Set<String> impactedPackages = new HashSet<String>();
if(values.containsKey(VoicemailContract.SOURCE_PACKAGE_FIELD)) {
impactedPackages.add(values.getAsString(VoicemailContract.SOURCE_PACKAGE_FIELD));
}
return impactedPackages;
}
private void notifyVoicemailChange(Uri notificationUri, Set<String> modifiedPackages,
String... intentActions) {
// Notify the observers.
// Must be done only once, even if there are multiple broadcast intents.
mContext.getContentResolver().notifyChange(notificationUri, null, true);
Collection<String> callingPackages = getCallingPackages();
// Now fire individual intents.
for (String intentAction : intentActions) {
// self_change extra should be included only for provider_changed events.
boolean includeSelfChangeExtra = intentAction.equals(Intent.ACTION_PROVIDER_CHANGED);
for (ComponentName component :
getBroadcastReceiverComponents(intentAction, notificationUri)) {
// Ignore any package that is not affected by the change and don't have full access
// either.
if (!modifiedPackages.contains(component.getPackageName()) &&
!mVoicemailPermissions.packageHasFullAccess(component.getPackageName())) {
continue;
}
Intent intent = new Intent(intentAction, notificationUri);
intent.setComponent(component);
if (includeSelfChangeExtra && callingPackages != null) {
intent.putExtra(VoicemailContract.EXTRA_SELF_CHANGE,
callingPackages.contains(component.getPackageName()));
}
String permissionNeeded = modifiedPackages.contains(component.getPackageName()) ?
ADD_VOICEMAIL : READ_WRITE_ALL_VOICEMAIL;
mContext.sendBroadcast(intent, permissionNeeded);
Log.v(TAG, String.format("Sent intent. act:%s, url:%s, comp:%s, perm:%s," +
" self_change:%s", intent.getAction(), intent.getData(),
component.getClassName(), permissionNeeded,
intent.hasExtra(VoicemailContract.EXTRA_SELF_CHANGE) ?
intent.getBooleanExtra(VoicemailContract.EXTRA_SELF_CHANGE, false) :
null));
}
}
}
/** Determines the components that can possibly receive the specified intent. */
private List<ComponentName> getBroadcastReceiverComponents(String intentAction, Uri uri) {
Intent intent = new Intent(intentAction, uri);
List<ComponentName> receiverComponents = new ArrayList<ComponentName>();
// For broadcast receivers ResolveInfo.activityInfo is the one that is populated.
for (ResolveInfo resolveInfo :
mContext.getPackageManager().queryBroadcastReceivers(intent, 0)) {
ActivityInfo activityInfo = resolveInfo.activityInfo;
receiverComponents.add(new ComponentName(activityInfo.packageName, activityInfo.name));
}
return receiverComponents;
}
/**
* Returns the package names of the calling process. If the calling process has more than
* one packages, this returns them all
*/
private Collection<String> getCallingPackages() {
int caller = Binder.getCallingUid();
if (caller == 0) {
return null;
}
return Lists.newArrayList(mContext.getPackageManager().getPackagesForUid(caller));
}
}