/* * 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 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 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 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 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 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 getModifiedPackages(String whereClause, String[] whereArgs) { Set modifiedPackages = new HashSet(); 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 getModifiedPackages(ContentValues values) { Set impactedPackages = new HashSet(); if(values.containsKey(VoicemailContract.SOURCE_PACKAGE_FIELD)) { impactedPackages.add(values.getAsString(VoicemailContract.SOURCE_PACKAGE_FIELD)); } return impactedPackages; } private void notifyVoicemailChange(Uri notificationUri, Set 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 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 getBroadcastReceiverComponents(String intentAction, Uri uri) { Intent intent = new Intent(intentAction, uri); List receiverComponents = new ArrayList(); // 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 getCallingPackages() { int caller = Binder.getCallingUid(); if (caller == 0) { return null; } return Lists.newArrayList(mContext.getPackageManager().getPackagesForUid(caller)); } }