diff options
Diffstat (limited to 'src/com/android/providers/contacts')
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 |