diff options
author | Debashish Chatterjee <debashishc@google.com> | 2011-07-05 08:12:29 +0100 |
---|---|---|
committer | Debashish Chatterjee <debashishc@google.com> | 2011-07-06 14:48:18 +0100 |
commit | 4b571ba0de4fac4ff9d2a4277032b8c6548fdbfa (patch) | |
tree | 0b4d0d6470e0b8e584a41c6c9a0cdb02382af3c6 /src/com | |
parent | 6fbb4b739c288395bd80343400d3455b0292a0fd (diff) | |
download | packages_providers_ContactsProvider-4b571ba0de4fac4ff9d2a4277032b8c6548fdbfa.zip packages_providers_ContactsProvider-4b571ba0de4fac4ff9d2a4277032b8c6548fdbfa.tar.gz packages_providers_ContactsProvider-4b571ba0de4fac4ff9d2a4277032b8c6548fdbfa.tar.bz2 |
Refactored VoicemailContentProvider to simplify using multiple tables.
This is needed to simplify the voicemail provider to be able to serve
operations on the voicemail_status table as well.
The idea is to retain all common functionality related to voicemail
permission check etc in the main VoicemailContentProvider class. And
delegate database level operations to another class that only deals with
functionality speficic to the underlying table.
In the favor of code reuse the interaction between voicemail_content_provider
and voicemail_table_implementations is both ways. VoicemailTable
interface defines the operations exposed by both sides.
I have also added a couple of new test cases to cover getType() and
source_package check in the uri. Old test cases continue to pass to
prove that the refactoring did not break any functionality.
Change-Id: I4cb031234c2f5746084c51557e2ba0edbaf3d6de
Diffstat (limited to 'src/com')
3 files changed, 499 insertions, 358 deletions
diff --git a/src/com/android/providers/contacts/VoicemailContentProvider.java b/src/com/android/providers/contacts/VoicemailContentProvider.java index 2f74868..3a093c0 100644 --- a/src/com/android/providers/contacts/VoicemailContentProvider.java +++ b/src/com/android/providers/contacts/VoicemailContentProvider.java @@ -15,80 +15,50 @@ */ package com.android.providers.contacts; -import static com.android.providers.contacts.util.DbQueryUtils.checkForSupportedColumns; -import static com.android.providers.contacts.util.DbQueryUtils.concatenateClauses; import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause; import com.android.providers.contacts.ContactsDatabaseHelper.Tables; -import com.android.providers.contacts.util.CloseUtils; +import com.android.providers.contacts.util.SelectionBuilder; import com.android.providers.contacts.util.TypedUriMatcherImpl; import android.content.ComponentName; import android.content.ContentProvider; import android.content.ContentResolver; -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.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.os.Binder; import android.os.ParcelFileDescriptor; -import android.provider.CallLog.Calls; import android.provider.VoicemailContract; import android.provider.VoicemailContract.Voicemails; -import android.util.Log; -import java.io.File; import java.io.FileNotFoundException; -import java.io.IOException; import java.util.ArrayList; import java.util.List; -// TODO: Restrict access to only voicemail columns (i.e no access to call_log -// specific fields) -// TODO: Port unit tests from perforce. /** * An implementation of the Voicemail content provider. */ -public class VoicemailContentProvider extends ContentProvider { +public class VoicemailContentProvider extends ContentProvider + implements VoicemailTable.DelegateHelper { private static final String TAG = "VoicemailContentProvider"; - - /** The private directory in which to store the data associated with the voicemail. */ - private static final String DATA_DIRECTORY = "voicemail-data"; - - private static final String[] MIME_TYPE_ONLY_PROJECTION = new String[] { Voicemails.MIME_TYPE }; - private static final String[] FILENAME_ONLY_PROJECTION = new String[] { Voicemails._DATA }; private static final String VOICEMAILS_TABLE_NAME = Tables.CALLS; - // Voicemail projection map - private static final ProjectionMap sVoicemailProjectionMap = new ProjectionMap.Builder() - .add(Voicemails._ID) - .add(Voicemails.NUMBER) - .add(Voicemails.DATE) - .add(Voicemails.DURATION) - .add(Voicemails.NEW) - .add(Voicemails.STATE) - .add(Voicemails.SOURCE_DATA) - .add(Voicemails.SOURCE_PACKAGE) - .add(Voicemails.HAS_CONTENT) - .add(Voicemails.MIME_TYPE) - .add(Voicemails._DATA) - .build(); private ContentResolver mContentResolver; - private ContactsDatabaseHelper mDbHelper; private VoicemailPermissions mVoicemailPermissions; + private VoicemailTable.Delegate mVoicemailContentTable; @Override public boolean onCreate() { Context context = context(); mContentResolver = context.getContentResolver(); - mDbHelper = getDatabaseHelper(context); mVoicemailPermissions = new VoicemailPermissions(context); + mVoicemailContentTable = new VoicemailContentTable(VOICEMAILS_TABLE_NAME, context, + getDatabaseHelper(context), this); return true; } @@ -104,284 +74,120 @@ public class VoicemailContentProvider extends ContentProvider { public String getType(Uri uri) { UriData uriData = null; try { - uriData = createUriData(uri); + uriData = UriData.createUriData(uri); } catch (IllegalArgumentException ignored) { // Special case: for illegal URIs, we return null rather than thrown an exception. return null; } - // TODO: DB lookup for the mime type may cause strict mode exception for the callers of - // getType(). See if this could be avoided. - if (uriData.hasId()) { - // An individual voicemail - so lookup the MIME type in the db. - return lookupMimeType(uriData); - } - // Not an individual voicemail - must be a directory listing type. - return Voicemails.DIR_TYPE; - } - - /** Query the db for the MIME type of the given URI, called only from {@link #getType(Uri)}. */ - private String lookupMimeType(UriData uriData) { - Cursor cursor = null; - try { - // Use queryInternal, bypassing provider permission check. This is needed because - // getType() can be called from any application context (even without voicemail - // permissions) to know the MIME type of the URI. There is no security issue here as we - // do not expose any sensitive data through this interface. - cursor = queryInternal(uriData, MIME_TYPE_ONLY_PROJECTION, null, null, null); - if (cursor.moveToFirst()) { - return cursor.getString(cursor.getColumnIndex(Voicemails.MIME_TYPE)); - } - } finally { - CloseUtils.closeQuietly(cursor); - } - return null; + return mVoicemailContentTable.getType(uriData); } @Override - public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, - String sortOrder) { - mVoicemailPermissions.checkCallerHasOwnVoicemailAccess(); - UriData uriData = createUriData(uri); - checkPackagePermission(uriData); - return queryInternal(uriData, projection, - concatenateClauses(selection, getPackageRestrictionClause()), selectionArgs, - sortOrder); - } - - /** - * Internal version of query(), that does not apply any provider restriction and lets the query - * flow through without such checks. - * <p> - * This is useful for internal queries when we do not worry about access permissions. - */ - private Cursor queryInternal(UriData uriData, String[] projection, String selection, - String[] selectionArgs, String sortOrder) { - SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); - qb.setTables(Tables.CALLS); - qb.setProjectionMap(sVoicemailProjectionMap); - qb.setStrict(true); - - String combinedClause = concatenateClauses(selection, getWhereClause(uriData), - getCallTypeClause()); - SQLiteDatabase db = mDbHelper.getReadableDatabase(); - Cursor c = qb.query(db, projection, combinedClause, selectionArgs, null, null, sortOrder); - if (c != null) { - c.setNotificationUri(mContentResolver, Voicemails.CONTENT_URI); - } - return c; - } - - private String getWhereClause(UriData uriData) { - return concatenateClauses( - (uriData.hasId() ? - getEqualityClause(Voicemails._ID, uriData.getId()) - : null), - (uriData.hasSourcePackage() ? - getEqualityClause(Voicemails.SOURCE_PACKAGE, uriData.getSourcePackage()) - : null)); + public int bulkInsert(Uri uri, ContentValues[] valuesArray) { + UriData uriData = checkPermissionsAndCreateUriData(uri); + return mVoicemailContentTable.bulkInsert(uriData, valuesArray); } @Override - public int bulkInsert(Uri uri, ContentValues[] valuesArray) { - mVoicemailPermissions.checkCallerHasOwnVoicemailAccess(); - // TODO: There is scope to optimize this method further. At the least we can avoid doing the - // extra work related to the calling provider and checking permissions. - UriData uriData = createUriData(uri); - int numInserted = 0; - for (ContentValues values : valuesArray) { - if (insertInternal(uriData, values, false) != null) { - numInserted++; - } - } - if (numInserted > 0) { - notifyChange(uri, Intent.ACTION_PROVIDER_CHANGED); - } - return numInserted; + public Uri insert(Uri uri, ContentValues values) { + UriData uriData = checkPermissionsAndCreateUriData(uri); + return mVoicemailContentTable.insert(uriData, values); } @Override - public Uri insert(Uri uri, ContentValues values) { - mVoicemailPermissions.checkCallerHasOwnVoicemailAccess(); - return insertInternal(createUriData(uri), values, true); + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + UriData uriData = checkPermissionsAndCreateUriData(uri); + SelectionBuilder selectionBuilder = new SelectionBuilder(selection); + selectionBuilder.addClause(getPackageRestrictionClause()); + return mVoicemailContentTable.query(uriData, projection, selectionBuilder.build(), + selectionArgs, sortOrder); } - private Uri insertInternal(UriData uriData, ContentValues values, - boolean sendProviderChangedNotification) { - checkForSupportedColumns(sVoicemailProjectionMap, values); - ContentValues copiedValues = new ContentValues(values); - checkInsertSupported(uriData); - checkAndAddSourcePackageIntoValues(uriData, copiedValues); - - // "_data" column is used by base ContentProvider's openFileHelper() to determine filename - // when Input/Output stream is requested to be opened. - copiedValues.put(Voicemails._DATA, generateDataFile()); - - // call type is always voicemail. - copiedValues.put(Calls.TYPE, Calls.VOICEMAIL_TYPE); - - SQLiteDatabase db = mDbHelper.getWritableDatabase(); - long rowId = db.insert(VOICEMAILS_TABLE_NAME, null, copiedValues); - if (rowId > 0) { - Uri newUri = ContentUris.withAppendedId(uriData.getUri(), rowId); - if (sendProviderChangedNotification) { - notifyChange(newUri, VoicemailContract.ACTION_NEW_VOICEMAIL, - Intent.ACTION_PROVIDER_CHANGED); - } else { - notifyChange(newUri, VoicemailContract.ACTION_NEW_VOICEMAIL); - } - // Populate the 'voicemail_uri' field to be used by the call_log provider. - updateVoicemailUri(db, newUri); - return newUri; - } - return null; + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + UriData uriData = checkPermissionsAndCreateUriData(uri); + SelectionBuilder selectionBuilder = new SelectionBuilder(selection); + selectionBuilder.addClause(getPackageRestrictionClause()); + return mVoicemailContentTable.update(uriData, values, selectionBuilder.build(), + selectionArgs); } - private void updateVoicemailUri(SQLiteDatabase db, Uri newUri) { - ContentValues values = new ContentValues(); - values.put(Calls.VOICEMAIL_URI, newUri.toString()); - // Directly update the db because we cannot update voicemail_uri through external - // update() due to projectionMap check. This also avoids unnecessary permission - // checks that are already done as part of insert request. - db.update(VOICEMAILS_TABLE_NAME, values, getWhereClause(createUriData(newUri)), null); + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + UriData uriData = checkPermissionsAndCreateUriData(uri); + SelectionBuilder selectionBuilder = new SelectionBuilder(selection); + selectionBuilder.addClause(getPackageRestrictionClause()); + return mVoicemailContentTable.delete(uriData, selectionBuilder.build(), selectionArgs); } - private void checkAndAddSourcePackageIntoValues(UriData uriData, ContentValues values) { - // If content values don't contain the provider, calculate the right provider to use. - if (!values.containsKey(Voicemails.SOURCE_PACKAGE)) { - String provider = uriData.hasSourcePackage() ? - uriData.getSourcePackage() : getCallingPackage(); - values.put(Voicemails.SOURCE_PACKAGE, provider); - } - // If you put a provider in the URI and in the values, they must match. - if (uriData.hasSourcePackage() && values.containsKey(Voicemails.SOURCE_PACKAGE)) { - if (!uriData.getSourcePackage().equals(values.get(Voicemails.SOURCE_PACKAGE))) { - throw new SecurityException( - "Provider in URI was " + uriData.getSourcePackage() + - " but doesn't match provider in ContentValues which was " - + values.get(Voicemails.SOURCE_PACKAGE)); - } - } - // You must have access to the provider given in values. - if (!mVoicemailPermissions.callerHasFullAccess()) { - checkPackagesMatch(getCallingPackage(), values.getAsString(Voicemails.SOURCE_PACKAGE), - uriData.getUri()); - } + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + UriData uriData = checkPermissionsAndCreateUriData(uri); + // openFileHelper() relies on "_data" column to be populated with the file path. + return mVoicemailContentTable.openFile(uriData, mode, openFileHelper(uri, mode)); } /** - * Checks that the callingProvider is same as voicemailProvider. Throws {@link - * SecurityException} if they don't match. + * Decorates a URI by providing methods to get various properties from the URI. */ - private final void checkPackagesMatch(String callingProvider, String voicemailProvider, - Uri uri) { - if (!voicemailProvider.equals(callingProvider)) { - String errorMsg = String.format("Permission denied for URI: %s\n. " + - "Provider %s cannot perform this operation for %s. Requires %s permission.", - uri, callingProvider, voicemailProvider, - Manifest.permission.READ_WRITE_ALL_VOICEMAIL); - throw new SecurityException(errorMsg); - } - } + public static class UriData { + private final Uri mUri; + private final String mId; + private final String mSourcePackage; - private void checkInsertSupported(UriData uriData) { - if (uriData.hasId()) { - throw new UnsupportedOperationException(String.format( - "Cannot insert URI: %s. Inserted URIs should not contain an id.", - uriData.getUri())); + public UriData(Uri uri, String id, String sourcePackage) { + mUri = uri; + mId = id; + mSourcePackage = sourcePackage; } - } - @Override - public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - mVoicemailPermissions.checkCallerHasOwnVoicemailAccess(); - UriData uriData = createUriData(uri); - checkPackagePermission(uriData); - checkForSupportedColumns(sVoicemailProjectionMap, values); - checkUpdateSupported(uriData); - final SQLiteDatabase db = mDbHelper.getWritableDatabase(); - // TODO: This implementation does not allow bulk update because it only accepts - // URI that include message Id. I think we do want to support bulk update. - String combinedClause = concatenateClauses(selection, getPackageRestrictionClause(), - getWhereClause(uriData), getCallTypeClause()); - int count = db.update(VOICEMAILS_TABLE_NAME, values, combinedClause, selectionArgs); - if (count > 0) { - notifyChange(uri, Intent.ACTION_PROVIDER_CHANGED); + /** Gets the original URI to which this {@link UriData} corresponds. */ + public final Uri getUri() { + return mUri; } - return count; - } - private void checkUpdateSupported(UriData uriData) { - if (!uriData.hasId()) { - throw new UnsupportedOperationException(String.format( - "Cannot update URI: %s. Bulk update not supported", uriData.getUri())); + /** Tells us if our URI has an individual voicemail id. */ + public final boolean hasId() { + return mId != null; } - } - @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { - mVoicemailPermissions.checkCallerHasOwnVoicemailAccess(); - UriData uriData = createUriData(uri); - checkPackagePermission(uriData); - final SQLiteDatabase db = mDbHelper.getWritableDatabase(); - String combinedClause = concatenateClauses(selection, getPackageRestrictionClause(), - getWhereClause(uriData), getCallTypeClause()); - - // Delete all the files associated with this query. Once we've deleted the rows, there will - // be no way left to get hold of the files. - Cursor cursor = null; - try { - cursor = queryInternal(uriData, FILENAME_ONLY_PROJECTION, selection, selectionArgs, - null); - while (cursor.moveToNext()) { - File file = new File(cursor.getString(0)); - if (file.exists()) { - boolean success = file.delete(); - if (!success) { - Log.e(TAG, "Failed to delete file: " + file.getAbsolutePath()); - } - } - } - } finally { - CloseUtils.closeQuietly(cursor); + /** Gets the ID for the voicemail. */ + public final String getId() { + return mId; } - // Now delete the rows themselves. - int count = db.delete(VOICEMAILS_TABLE_NAME, combinedClause, selectionArgs); - if (count > 0) { - notifyChange(uri, Intent.ACTION_PROVIDER_CHANGED); + /** Tells us if our URI has a source package string. */ + public final boolean hasSourcePackage() { + return mSourcePackage != null; } - return count; - } - - @Override - public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { - mVoicemailPermissions.checkCallerHasOwnVoicemailAccess(); - UriData uriData = createUriData(uri); - checkPackagePermission(uriData); - - // This relies on "_data" column to be populated with the file path. - ParcelFileDescriptor openFileHelper = openFileHelper(uri, mode); - // If the open succeeded, then update the file exists bit in the table. - if (mode.contains("w")) { - ContentValues contentValues = new ContentValues(); - contentValues.put(Voicemails.HAS_CONTENT, 1); - update(uri, contentValues, null, null); + /** Gets the source package. */ + public final String getSourcePackage() { + return mSourcePackage; } - return openFileHelper; + /** Create a {@link UriData} corresponding to a given uri. */ + public static UriData createUriData(Uri uri) { + String sourcePackage = uri.getQueryParameter( + VoicemailContract.PARAM_KEY_SOURCE_PACKAGE); + List<String> segments = uri.getPathSegments(); + switch (createUriMatcher().match(uri)) { + case VOICEMAILS: + return new UriData(uri, null, sourcePackage); + case VOICEMAILS_ID: + return new UriData(uri, segments.get(1), sourcePackage); + case NO_MATCH: + throw new IllegalArgumentException("Invalid URI: " + uri); + default: + throw new IllegalStateException("Impossible, all cases are covered"); + } + } } - /** - * Notifies the content resolver and fires required broadcast intent(s) to notify about the - * change. - * - * @param notificationUri The URI that got impacted due to the change. This is the URI that is - * included in content resolver and broadcast intent notification. - * @param intentActions List of intent actions that needs to be fired. A separate intent is - * fired for each intent action. - */ - private void notifyChange(Uri notificationUri, String... intentActions) { + @Override + // VoicemailTable.DelegateHelper interface. + public void notifyChange(Uri notificationUri, String... intentActions) { // Notify the observers. mContentResolver.notifyChange(notificationUri, null, true); String callingPackage = getCallingPackage(); @@ -404,68 +210,62 @@ public class VoicemailContentProvider extends ContentProvider { } } - /** Determines the packages that can possibly receive the specified intent. */ - protected 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 : - context().getPackageManager().queryBroadcastReceivers(intent, 0)) { - ActivityInfo activityInfo = resolveInfo.activityInfo; - receiverComponents.add(new ComponentName(activityInfo.packageName, activityInfo.name)); + @Override + // VoicemailTable.DelegateHelper interface. + public void checkAndAddSourcePackageIntoValues(UriData uriData, ContentValues values) { + // If content values don't contain the provider, calculate the right provider to use. + if (!values.containsKey(VoicemailContract.SOURCE_PACKAGE_FIELD)) { + String provider = uriData.hasSourcePackage() ? + uriData.getSourcePackage() : getCallingPackage(); + values.put(VoicemailContract.SOURCE_PACKAGE_FIELD, provider); + } + // If you put a provider in the URI and in the values, they must match. + if (uriData.hasSourcePackage() && + values.containsKey(VoicemailContract.SOURCE_PACKAGE_FIELD)) { + if (!uriData.getSourcePackage().equals( + values.get(VoicemailContract.SOURCE_PACKAGE_FIELD))) { + throw new SecurityException( + "Provider in URI was " + uriData.getSourcePackage() + + " but doesn't match provider in ContentValues which was " + + values.get(VoicemailContract.SOURCE_PACKAGE_FIELD)); + } + } + // You must have access to the provider given in values. + if (!mVoicemailPermissions.callerHasFullAccess()) { + checkPackagesMatch(getCallingPackage(), + values.getAsString(VoicemailContract.SOURCE_PACKAGE_FIELD), + uriData.getUri()); } - return receiverComponents; } - /** Generates a random file for storing audio data. */ - private String generateDataFile() { - try { - File dataDirectory = context().getDir(DATA_DIRECTORY, Context.MODE_PRIVATE); - File voicemailFile = File.createTempFile("voicemail", "", dataDirectory); - return voicemailFile.getAbsolutePath(); - } catch (IOException e) { - // If we are unable to create a temporary file, something went horribly wrong. - throw new RuntimeException("unable to create temp file", e); - } + private static TypedUriMatcherImpl<VoicemailUriType> createUriMatcher() { + return new TypedUriMatcherImpl<VoicemailUriType>( + VoicemailContract.AUTHORITY, VoicemailUriType.values()); } /** - * Decorates a URI by providing methods to get various properties from the URI. + * Performs necessary voicemail permission checks common to all operations and returns + * the structured representation, {@link UriData}, of the supplied uri. */ - private static class UriData { - private final Uri mUri; - private final String mId; - private final String mSourcePackage; - - public UriData(Uri uri, String id, String sourcePackage) { - mUri = uri; - mId = id; - mSourcePackage = sourcePackage; - } - - /** Gets the original URI to which this {@link UriData} corresponds. */ - public final Uri getUri() { - return mUri; - } - - /** Tells us if our URI has an individual voicemail id. */ - public final boolean hasId() { - return mId != null; - } - - /** Gets the ID for the voicemail. */ - public final String getId() { - return mId; - } - - /** Tells us if our URI has a source package string. */ - public final boolean hasSourcePackage() { - return mSourcePackage != null; - } + private UriData checkPermissionsAndCreateUriData(Uri uri) { + mVoicemailPermissions.checkCallerHasOwnVoicemailAccess(); + UriData uriData = UriData.createUriData(uri); + checkPackagePermission(uriData); + return uriData; + } - /** Gets the source package. */ - public final String getSourcePackage() { - return mSourcePackage; + /** + * Checks that the callingProvider is same as voicemailProvider. Throws {@link + * SecurityException} if they don't match. + */ + private final void checkPackagesMatch(String callingProvider, String voicemailProvider, + Uri uri) { + if (!voicemailProvider.equals(callingProvider)) { + String errorMsg = String.format("Permission denied for URI: %s\n. " + + "Provider %s cannot perform this operation for %s. Requires %s permission.", + uri, callingProvider, voicemailProvider, + Manifest.permission.READ_WRITE_ALL_VOICEMAIL); + throw new SecurityException(errorMsg); } } @@ -483,35 +283,14 @@ public class VoicemailContentProvider extends ContentProvider { // You cannot have a match if this is not a provider uri. throw new SecurityException(String.format( "Provider %s does not have %s permission." + - "\nPlease use /voicemail/provider/ query path instead.\nURI: %s", + "\nPlease set query parameter '%s' in the URI.\nURI: %s", getCallingPackage(), Manifest.permission.READ_WRITE_ALL_VOICEMAIL, - uriData.getUri())); + VoicemailContract.PARAM_KEY_SOURCE_PACKAGE, uriData.getUri())); } checkPackagesMatch(getCallingPackage(), uriData.getSourcePackage(), uriData.getUri()); } } - private static TypedUriMatcherImpl<VoicemailUriType> createUriMatcher() { - return new TypedUriMatcherImpl<VoicemailUriType>( - VoicemailContract.AUTHORITY, VoicemailUriType.values()); - } - - /** Get a {@link UriData} corresponding to a given uri. */ - private UriData createUriData(Uri uri) { - String sourcePackage = uri.getQueryParameter(VoicemailContract.PARAM_KEY_SOURCE_PACKAGE); - List<String> segments = uri.getPathSegments(); - switch (createUriMatcher().match(uri)) { - case VOICEMAILS: - return new UriData(uri, null, sourcePackage); - case VOICEMAILS_ID: - return new UriData(uri, segments.get(1), sourcePackage); - case NO_MATCH: - throw new IllegalArgumentException("Invalid URI: " + uri); - default: - throw new IllegalStateException("Impossible, all cases are covered"); - } - } - /** * Gets the name of the calling package. * <p> @@ -559,10 +338,16 @@ public class VoicemailContentProvider extends ContentProvider { return getEqualityClause(Voicemails.SOURCE_PACKAGE, getCallingPackage()); } - - /** Creates a clause to restrict the selection to only voicemail call type.*/ - private String getCallTypeClause() { - return getEqualityClause(Calls.TYPE, String.valueOf(Calls.VOICEMAIL_TYPE)); + /** Determines the components that can possibly receive the specified intent. */ + protected 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 : + context().getPackageManager().queryBroadcastReceivers(intent, 0)) { + ActivityInfo activityInfo = resolveInfo.activityInfo; + receiverComponents.add(new ComponentName(activityInfo.packageName, activityInfo.name)); + } + return receiverComponents; } - } diff --git a/src/com/android/providers/contacts/VoicemailContentTable.java b/src/com/android/providers/contacts/VoicemailContentTable.java new file mode 100644 index 0000000..8f6f3bf --- /dev/null +++ b/src/com/android/providers/contacts/VoicemailContentTable.java @@ -0,0 +1,289 @@ +/* + * 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 com.android.providers.contacts.util.DbQueryUtils.checkForSupportedColumns; +import static com.android.providers.contacts.util.DbQueryUtils.concatenateClauses; +import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause; + +import com.android.providers.contacts.VoicemailContentProvider.UriData; +import com.android.providers.contacts.util.CloseUtils; + +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteQueryBuilder; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.provider.CallLog.Calls; +import android.provider.VoicemailContract; +import android.provider.VoicemailContract.Voicemails; +import android.util.Log; + +import java.io.File; +import java.io.IOException; + +/** + * Implementation of {@link VoicemailTable.Delegate} for the voicemail content table. + */ +public class VoicemailContentTable implements VoicemailTable.Delegate { + private static final String TAG = "VoicemailContentProvider"; + // Voicemail projection map + private static final ProjectionMap sVoicemailProjectionMap = new ProjectionMap.Builder() + .add(Voicemails._ID) + .add(Voicemails.NUMBER) + .add(Voicemails.DATE) + .add(Voicemails.DURATION) + .add(Voicemails.NEW) + .add(Voicemails.STATE) + .add(Voicemails.SOURCE_DATA) + .add(Voicemails.SOURCE_PACKAGE) + .add(Voicemails.HAS_CONTENT) + .add(Voicemails.MIME_TYPE) + .add(Voicemails._DATA) + .build(); + + /** The private directory in which to store the data associated with the voicemail. */ + private static final String DATA_DIRECTORY = "voicemail-data"; + + private static final String[] MIME_TYPE_ONLY_PROJECTION = new String[] { Voicemails.MIME_TYPE }; + private static final String[] FILENAME_ONLY_PROJECTION = new String[] { Voicemails._DATA }; + + private final String mTableName; + private final SQLiteOpenHelper mDbHelper; + private final Context mContext; + private final VoicemailTable.DelegateHelper mDelegateHelper; + + public VoicemailContentTable(String tableName, Context context, SQLiteOpenHelper dbHelper, + VoicemailTable.DelegateHelper contentProviderHelper) { + mTableName = tableName; + mContext = context; + mDbHelper = dbHelper; + mDelegateHelper = contentProviderHelper; + } + + @Override + public int bulkInsert(UriData uriData, ContentValues[] valuesArray) { + int numInserted = 0; + for (ContentValues values : valuesArray) { + if (insertInternal(uriData, values, false) != null) { + numInserted++; + } + } + if (numInserted > 0) { + mDelegateHelper.notifyChange(uriData.getUri(), Intent.ACTION_PROVIDER_CHANGED); + } + return numInserted; + } + + @Override + public Uri insert(UriData uriData, ContentValues values) { + return insertInternal(uriData, values, true); + } + + private Uri insertInternal(UriData uriData, ContentValues values, + boolean sendProviderChangedNotification) { + checkForSupportedColumns(sVoicemailProjectionMap, values); + ContentValues copiedValues = new ContentValues(values); + checkInsertSupported(uriData); + mDelegateHelper.checkAndAddSourcePackageIntoValues(uriData, copiedValues); + + // "_data" column is used by base ContentProvider's openFileHelper() to determine filename + // when Input/Output stream is requested to be opened. + copiedValues.put(Voicemails._DATA, generateDataFile()); + + // call type is always voicemail. + copiedValues.put(Calls.TYPE, Calls.VOICEMAIL_TYPE); + + SQLiteDatabase db = mDbHelper.getWritableDatabase(); + long rowId = db.insert(mTableName, null, copiedValues); + if (rowId > 0) { + Uri newUri = ContentUris.withAppendedId(uriData.getUri(), rowId); + mDelegateHelper.notifyChange(newUri, VoicemailContract.ACTION_NEW_VOICEMAIL); + if (sendProviderChangedNotification) { + mDelegateHelper.notifyChange(newUri, Intent.ACTION_PROVIDER_CHANGED); + } + // Populate the 'voicemail_uri' field to be used by the call_log provider. + updateVoicemailUri(db, newUri); + return newUri; + } + return null; + } + + private void checkInsertSupported(UriData uriData) { + if (uriData.hasId()) { + throw new UnsupportedOperationException(String.format( + "Cannot insert URI: %s. Inserted URIs should not contain an id.", + uriData.getUri())); + } + } + + /** Generates a random file for storing audio data. */ + private String generateDataFile() { + try { + File dataDirectory = mContext.getDir(DATA_DIRECTORY, Context.MODE_PRIVATE); + File voicemailFile = File.createTempFile("voicemail", "", dataDirectory); + return voicemailFile.getAbsolutePath(); + } catch (IOException e) { + // If we are unable to create a temporary file, something went horribly wrong. + throw new RuntimeException("unable to create temp file", e); + } + } + private void updateVoicemailUri(SQLiteDatabase db, Uri newUri) { + ContentValues values = new ContentValues(); + values.put(Calls.VOICEMAIL_URI, newUri.toString()); + // Directly update the db because we cannot update voicemail_uri through external + // update() due to projectionMap check. This also avoids unnecessary permission + // checks that are already done as part of insert request. + db.update(mTableName, values, getWhereClause( + UriData.createUriData(newUri)), null); + } + + @Override + public int delete(UriData uriData, String selection, String[] selectionArgs) { + final SQLiteDatabase db = mDbHelper.getWritableDatabase(); + String combinedClause = concatenateClauses(selection, getWhereClause(uriData), + getCallTypeClause()); + + // Delete all the files associated with this query. Once we've deleted the rows, there will + // be no way left to get hold of the files. + Cursor cursor = null; + try { + cursor = query(uriData, FILENAME_ONLY_PROJECTION, selection, selectionArgs, null); + while (cursor.moveToNext()) { + File file = new File(cursor.getString(0)); + if (file.exists()) { + boolean success = file.delete(); + if (!success) { + Log.e(TAG, "Failed to delete file: " + file.getAbsolutePath()); + } + } + } + } finally { + CloseUtils.closeQuietly(cursor); + } + + // Now delete the rows themselves. + int count = db.delete(mTableName, combinedClause, selectionArgs); + if (count > 0) { + mDelegateHelper.notifyChange(uriData.getUri(), Intent.ACTION_PROVIDER_CHANGED); + } + return count; + } + + @Override + public Cursor query(UriData uriData, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); + qb.setTables(mTableName); + qb.setProjectionMap(sVoicemailProjectionMap); + qb.setStrict(true); + + String combinedClause = concatenateClauses(selection, getWhereClause(uriData), + getCallTypeClause()); + SQLiteDatabase db = mDbHelper.getReadableDatabase(); + Cursor c = qb.query(db, projection, combinedClause, selectionArgs, null, null, sortOrder); + if (c != null) { + c.setNotificationUri(mContext.getContentResolver(), Voicemails.CONTENT_URI); + } + return c; + } + + @Override + public int update(UriData uriData, ContentValues values, String selection, + String[] selectionArgs) { + checkForSupportedColumns(sVoicemailProjectionMap, values); + checkUpdateSupported(uriData); + final SQLiteDatabase db = mDbHelper.getWritableDatabase(); + // TODO: This implementation does not allow bulk update because it only accepts + // URI that include message Id. I think we do want to support bulk update. + String combinedClause = concatenateClauses(selection, getWhereClause(uriData), + getCallTypeClause()); + int count = db.update(mTableName, values, combinedClause, selectionArgs); + if (count > 0) { + mDelegateHelper.notifyChange(uriData.getUri(), Intent.ACTION_PROVIDER_CHANGED); + } + return count; + } + + private void checkUpdateSupported(UriData uriData) { + if (!uriData.hasId()) { + throw new UnsupportedOperationException(String.format( + "Cannot update URI: %s. Bulk update not supported", uriData.getUri())); + } + } + + @Override + public String getType(UriData uriData) { + // TODO: DB lookup for the mime type may cause strict mode exception for the callers of + // getType(). See if this could be avoided. + if (uriData.hasId()) { + // An individual voicemail - so lookup the MIME type in the db. + return lookupMimeType(uriData); + } + // Not an individual voicemail - must be a directory listing type. + return Voicemails.DIR_TYPE; + } + + /** Query the db for the MIME type of the given URI, called only from getType(). */ + private String lookupMimeType(UriData uriData) { + Cursor cursor = null; + try { + // Use queryInternal, bypassing provider permission check. This is needed because + // getType() can be called from any application context (even without voicemail + // permissions) to know the MIME type of the URI. There is no security issue here as we + // do not expose any sensitive data through this interface. + cursor = query(uriData, MIME_TYPE_ONLY_PROJECTION, null, null, null); + if (cursor.moveToFirst()) { + return cursor.getString(cursor.getColumnIndex(Voicemails.MIME_TYPE)); + } + } finally { + CloseUtils.closeQuietly(cursor); + } + return null; + } + + @Override + public ParcelFileDescriptor openFile(UriData uriData, String mode, + ParcelFileDescriptor openFileHelper) { + // If the open succeeded, then update the has_content bit in the table. + if (mode.contains("w")) { + ContentValues contentValues = new ContentValues(); + contentValues.put(Voicemails.HAS_CONTENT, 1); + update(uriData, contentValues, null, null); + } + return openFileHelper; + } + + private String getWhereClause(UriData uriData) { + return concatenateClauses( + (uriData.hasId() ? + getEqualityClause(Voicemails._ID, uriData.getId()) + : null), + (uriData.hasSourcePackage() ? + getEqualityClause(Voicemails.SOURCE_PACKAGE, uriData.getSourcePackage()) + : null)); + } + + /** Creates a clause to restrict the selection to only voicemail call type.*/ + private String getCallTypeClause() { + return getEqualityClause(Calls.TYPE, String.valueOf(Calls.VOICEMAIL_TYPE)); + } +} diff --git a/src/com/android/providers/contacts/VoicemailTable.java b/src/com/android/providers/contacts/VoicemailTable.java new file mode 100644 index 0000000..d068775 --- /dev/null +++ b/src/com/android/providers/contacts/VoicemailTable.java @@ -0,0 +1,67 @@ +/* + * 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 com.android.providers.contacts.VoicemailContentProvider.UriData; + +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import android.os.ParcelFileDescriptor; + +/** + * Defines interfaces for communication between voicemail content provider and voicemail table + * implementations. + */ +public interface VoicemailTable { + /** + * Interface that the voicemail content provider uses to delegate database level operations + * to the appropriate voicemail table implementation. + */ + public interface Delegate { + public Uri insert(UriData uriData, ContentValues values); + public int bulkInsert(UriData uriData, ContentValues[] valuesArray); + public int delete(UriData uriData, String selection, String[] selectionArgs); + public Cursor query(UriData uriData, String[] projection, String selection, + String[] selectionArgs, String sortOrder); + public int update(UriData uriData, ContentValues values, String selection, + String[] selectionArgs); + public String getType(UriData uriData); + public ParcelFileDescriptor openFile(UriData uriData, String mode, + ParcelFileDescriptor openFileHelper); + } + + /** + * A helper interface that an implementation of {@link Delegate} uses to access common + * functionality across different voicemail tables. + */ + public interface DelegateHelper { + /** + * Notifies the content resolver and fires required broadcast intent(s) to notify about the + * change. + * + * @param notificationUri The URI that got impacted due to the change. This is the URI that + * is included in content resolver and broadcast intent notification. + * @param intentActions List of intent actions that needs to be fired. A separate intent is + * fired for each intent action. + */ + public void notifyChange(Uri notificationUri, String... intentActions); + /** + * Inserts source_package field into ContentValues. Used in insert operations. + */ + public void checkAndAddSourcePackageIntoValues(UriData uriData, ContentValues values); + } +}
\ No newline at end of file |