summaryrefslogtreecommitdiffstats
path: root/src/com/android/providers/contacts
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/providers/contacts')
-rw-r--r--src/com/android/providers/contacts/VoicemailContentProvider.java501
-rw-r--r--src/com/android/providers/contacts/VoicemailContentTable.java289
-rw-r--r--src/com/android/providers/contacts/VoicemailTable.java67
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