Merge "Set the new last-access time global search column."
diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
index 2e2af94..69cf2b4 100644
--- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java
+++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
@@ -65,6 +65,7 @@
 import android.provider.ContactsContract.Settings;
 import android.provider.ContactsContract.StatusUpdates;
 import android.provider.SocialContract.Activities;
+import android.provider.VoicemailContract;
 import android.provider.VoicemailContract.Voicemails;
 import android.telephony.PhoneNumberUtils;
 import android.text.TextUtils;
@@ -97,7 +98,7 @@
      *   600-699 Ice Cream Sandwich
      * </pre>
      */
-    static final int DATABASE_VERSION = 603;
+    static final int DATABASE_VERSION = 604;
 
     private static final String DATABASE_NAME = "contacts2.db";
     private static final String DATABASE_PRESENCE = "presence_db";
@@ -124,6 +125,7 @@
         public static final String DIRECTORIES = "directories";
         public static final String DEFAULT_DIRECTORY = "default_directory";
         public static final String SEARCH_INDEX = "search_index";
+        public static final String VOICEMAIL_STATUS = "voicemail_status";
 
         /**
          * For {@link ContactsContract.DataUsageFeedback}. The table structure itself
@@ -792,7 +794,7 @@
 
     @Override
     public void onCreate(SQLiteDatabase db) {
-        Log.i(TAG, "Bootstrapping database");
+        Log.i(TAG, "Bootstrapping database version: " + DATABASE_VERSION);
 
         mSyncState.createDatabase(db);
 
@@ -1080,6 +1082,17 @@
                 Voicemails.STATE + " INTEGER" +
         ");");
 
+        // Voicemail source status table.
+        db.execSQL("CREATE TABLE " + Tables.VOICEMAIL_STATUS + " (" +
+                VoicemailContract.Status._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+                VoicemailContract.Status.SOURCE_PACKAGE + " TEXT UNIQUE NOT NULL," +
+                VoicemailContract.Status.SETTINGS_URI + " TEXT," +
+                VoicemailContract.Status.VOICEMAIL_ACCESS_URI + " TEXT," +
+                VoicemailContract.Status.CONFIGURATION_STATE + " INTEGER," +
+                VoicemailContract.Status.DATA_CHANNEL_STATE + " INTEGER," +
+                VoicemailContract.Status.NOTIFICATION_CHANNEL_STATE + " INTEGER" +
+        ");");
+
         // Activities table
         db.execSQL("CREATE TABLE " + Tables.ACTIVITIES + " (" +
                 Activities._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
@@ -1969,6 +1982,11 @@
             oldVersion = 603;
         }
 
+        if (oldVersion < 604) {
+            upgradeToVersion604(db);
+            oldVersion = 604;
+        }
+
         if (upgradeViewsAndTriggers) {
             createContactsViews(db);
             createGroupsView(db);
@@ -3043,6 +3061,18 @@
         db.execSQL("ALTER TABLE calls ADD state INTEGER;");
     }
 
+    private void upgradeToVersion604(SQLiteDatabase db) {
+        db.execSQL("CREATE TABLE voicemail_status (" +
+                "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
+                "source_package TEXT UNIQUE NOT NULL," +
+                "settings_uri TEXT," +
+                "voicemail_access_uri TEXT," +
+                "configuration_state INTEGER," +
+                "data_channel_state INTEGER," +
+                "notification_channel_state INTEGER" +
+        ");");
+    }
+
     public String extractHandleFromEmailAddress(String email) {
         Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email);
         if (tokens.length == 0) {
diff --git a/src/com/android/providers/contacts/VoicemailContentProvider.java b/src/com/android/providers/contacts/VoicemailContentProvider.java
index 5e17a33..3a093c0 100644
--- a/src/com/android/providers/contacts/VoicemailContentProvider.java
+++ b/src/com/android/providers/contacts/VoicemailContentProvider.java
@@ -15,82 +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.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.PackageManager;
+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 com.android.providers.contacts.ContactsDatabaseHelper.Tables;
-import com.android.providers.contacts.ContactsDatabaseHelper.Views;
-import com.android.providers.contacts.util.CloseUtils;
-import com.android.providers.contacts.util.DbQueryUtils;
-import com.android.providers.contacts.util.TypedUriMatcherImpl;
-
-import java.io.File;
 import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.util.HashMap;
+import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 
-// 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;
     }
 
@@ -106,327 +74,64 @@
     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 VoicemailContract.DIR_TYPE;
+        return mVoicemailContentTable.getType(uriData);
     }
 
-    /** 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;
+    @Override
+    public int bulkInsert(Uri uri, ContentValues[] valuesArray) {
+        UriData uriData = checkPermissionsAndCreateUriData(uri);
+        return mVoicemailContentTable.bulkInsert(uriData, valuesArray);
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        UriData uriData = checkPermissionsAndCreateUriData(uri);
+        return mVoicemailContentTable.insert(uriData, values);
     }
 
     @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, VoicemailContract.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));
-    }
-
-    @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;
-    }
-
-    @Override
-    public Uri insert(Uri uri, ContentValues values) {
-        mVoicemailPermissions.checkCallerHasOwnVoicemailAccess();
-        return insertInternal(createUriData(uri), values, true);
-    }
-
-    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(
-                    Uri.withAppendedPath(VoicemailContract.CONTENT_URI_SOURCE,
-                            copiedValues.getAsString(Voicemails.SOURCE_PACKAGE)), 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;
-    }
-
-    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);
-    }
-
-    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());
-        }
-    }
-
-    /**
-     * 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);
-        }
-    }
-
-    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()));
-        }
+        UriData uriData = checkPermissionsAndCreateUriData(uri);
+        SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
+        selectionBuilder.addClause(getPackageRestrictionClause());
+        return mVoicemailContentTable.query(uriData, projection, selectionBuilder.build(),
+                selectionArgs, sortOrder);
     }
 
     @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);
-        }
-        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()));
-        }
+        UriData uriData = checkPermissionsAndCreateUriData(uri);
+        SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
+        selectionBuilder.addClause(getPackageRestrictionClause());
+        return mVoicemailContentTable.update(uriData, values, selectionBuilder.build(),
+                selectionArgs);
     }
 
     @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);
-        }
-
-        // Now delete the rows themselves.
-        int count = db.delete(VOICEMAILS_TABLE_NAME, combinedClause, selectionArgs);
-        if (count > 0) {
-            notifyChange(uri, Intent.ACTION_PROVIDER_CHANGED);
-        }
-        return count;
+        UriData uriData = checkPermissionsAndCreateUriData(uri);
+        SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
+        selectionBuilder.addClause(getPackageRestrictionClause());
+        return mVoicemailContentTable.delete(uriData, selectionBuilder.build(), selectionArgs);
     }
 
     @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);
-        }
-
-        return openFileHelper;
-    }
-
-    /**
-     * 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) {
-        // Notify the observers.
-        mContentResolver.notifyChange(notificationUri, null, true);
-        String callingPackage = getCallingPackage();
-        // Fire notification intents.
-        for (String intentAction : intentActions) {
-            // TODO: We can possibly be more intelligent here and send targeted intents based on
-            // what voicemail permission the package has. If possible, here is what we would like to
-            // do for a given broadcast intent -
-            // 1) Send it to all packages that have READ_WRITE_ALL_VOICEMAIL permission.
-            // 2) Send it to only the owner package that has just READ_WRITE_OWN_VOICEMAIL, if not
-            // already sent in (1).
-            List<ResolveInfo> resolveInfos = context().getPackageManager()
-                    .queryBroadcastReceivers(new Intent(intentAction, notificationUri), 0);
-            for (ResolveInfo resolveInfo : resolveInfos) {
-                String packageName = resolveInfo.activityInfo.packageName;
-                Intent intent = new Intent(intentAction, notificationUri);
-                intent.setPackage(packageName);
-                intent.putExtra(VoicemailContract.EXTRA_SELF_CHANGE,
-                        callingPackage.equals(packageName));
-                context().sendBroadcast(intent, Manifest.permission.READ_WRITE_OWN_VOICEMAIL);
-            }
-        }
-    }
-
-    /** 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);
-        }
+        UriData uriData = checkPermissionsAndCreateUriData(uri);
+        // openFileHelper() relies on "_data" column to be populated with the file path.
+        return mVoicemailContentTable.openFile(uriData, mode, openFileHelper(uri, mode));
     }
 
     /**
      * Decorates a URI by providing methods to get various properties from the URI.
      */
-    private static class UriData {
+    public static class UriData {
         private final Uri mUri;
         private final String mId;
         private final String mSourcePackage;
@@ -461,6 +166,107 @@
         public final String getSourcePackage() {
             return mSourcePackage;
         }
+
+        /** 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");
+            }
+        }
+    }
+
+    @Override
+    // VoicemailTable.DelegateHelper interface.
+    public void notifyChange(Uri notificationUri, String... intentActions) {
+        // Notify the observers.
+        mContentResolver.notifyChange(notificationUri, null, true);
+        String callingPackage = getCallingPackage();
+        // Fire notification intents.
+        for (String intentAction : intentActions) {
+            // TODO: We can possibly be more intelligent here and send targeted intents based on
+            // what voicemail permission the package has. If possible, here is what we would like to
+            // do for a given broadcast intent -
+            // 1) Send it to all packages that have READ_WRITE_ALL_VOICEMAIL permission.
+            // 2) Send it to only the owner package that has just READ_WRITE_OWN_VOICEMAIL, if not
+            // already sent in (1).
+            for (ComponentName component :
+                    getBroadcastReceiverComponents(intentAction, notificationUri)) {
+                Intent intent = new Intent(intentAction, notificationUri);
+                intent.setComponent(component);
+                intent.putExtra(VoicemailContract.EXTRA_SELF_CHANGE,
+                        callingPackage.equals(component.getPackageName()));
+                context().sendBroadcast(intent, Manifest.permission.READ_WRITE_OWN_VOICEMAIL);
+            }
+        }
+    }
+
+    @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());
+        }
+    }
+
+    private static TypedUriMatcherImpl<VoicemailUriType> createUriMatcher() {
+        return new TypedUriMatcherImpl<VoicemailUriType>(
+                VoicemailContract.AUTHORITY, VoicemailUriType.values());
+    }
+
+    /**
+     * Performs necessary voicemail permission checks common to all operations and returns
+     * the structured representation, {@link UriData}, of the supplied uri.
+     */
+    private UriData checkPermissionsAndCreateUriData(Uri uri) {
+        mVoicemailPermissions.checkCallerHasOwnVoicemailAccess();
+        UriData uriData = UriData.createUriData(uri);
+        checkPackagePermission(uriData);
+        return uriData;
+    }
+
+    /**
+     * 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);
+        }
     }
 
     /**
@@ -477,38 +283,14 @@
                 // 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) {
-        List<String> segments = uri.getPathSegments();
-        switch (createUriMatcher().match(uri)) {
-            case VOICEMAILS:
-                return new UriData(uri, null, null);
-            case VOICEMAILS_ID:
-                return new UriData(uri, segments.get(1), null);
-            case VOICEMAILS_SOURCE:
-                return new UriData(uri, null, segments.get(2));
-            case VOICEMAILS_SOURCE_ID:
-                return new UriData(uri, segments.get(3), segments.get(2));
-            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>
@@ -556,10 +338,16 @@
         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
diff --git a/src/com/android/providers/contacts/VoicemailUriType.java b/src/com/android/providers/contacts/VoicemailUriType.java
index 9e728a2..c3a33ab 100644
--- a/src/com/android/providers/contacts/VoicemailUriType.java
+++ b/src/com/android/providers/contacts/VoicemailUriType.java
@@ -23,9 +23,7 @@
 enum VoicemailUriType implements UriType {
     NO_MATCH(null),
     VOICEMAILS("voicemail"),
-    VOICEMAILS_ID("voicemail/#"),
-    VOICEMAILS_SOURCE("voicemail/source/*"),
-    VOICEMAILS_SOURCE_ID("voicemail/source/*/#");
+    VOICEMAILS_ID("voicemail/#");
 
     private final String path;
 
diff --git a/tests/src/com/android/providers/contacts/VoicemailProviderTest.java b/tests/src/com/android/providers/contacts/VoicemailProviderTest.java
index c9d01c7..98498bd 100644
--- a/tests/src/com/android/providers/contacts/VoicemailProviderTest.java
+++ b/tests/src/com/android/providers/contacts/VoicemailProviderTest.java
@@ -16,6 +16,7 @@
 
 package com.android.providers.contacts;
 
+import android.content.ComponentName;
 import android.content.ContentProvider;
 import android.content.ContentUris;
 import android.content.ContentValues;
@@ -24,8 +25,8 @@
 import android.content.Intent;
 import android.database.Cursor;
 import android.net.Uri;
-import android.provider.VoicemailContract;
 import android.provider.CallLog.Calls;
+import android.provider.VoicemailContract;
 import android.provider.VoicemailContract.Voicemails;
 import android.test.MoreAsserts;
 
@@ -94,13 +95,13 @@
         // Give away full permission, in case it was granted previously.
         mActor.removePermissions(ALL_PERMISSION);
         mActor.addPermissions(OWN_PERMISSION);
-        mUseSourceUri = false;
+        mUseSourceUri = true;
     }
 
     private void setUpForFullPermission() {
         mActor.addPermissions(OWN_PERMISSION);
         mActor.addPermissions(ALL_PERMISSION);
-        mUseSourceUri = true;
+        mUseSourceUri = false;
     }
 
     private void setUpForNoPermission() {
@@ -110,12 +111,8 @@
     }
 
     private Uri contentUri() {
-         if (mUseSourceUri) {
-             return VoicemailContract.CONTENT_URI;
-         } else {
-             return Uri.withAppendedPath(VoicemailContract.CONTENT_URI_SOURCE,
-                     mActor.packageName);
-         }
+        return mUseSourceUri ?
+                Voicemails.buildSourceUri(mActor.packageName) : Voicemails.CONTENT_URI;
     }
 
     public void testInsert() throws Exception {
@@ -166,6 +163,54 @@
         assertEquals(0, getCount(uri, null, null));
     }
 
+    public void testGetType() throws Exception {
+        // voicemail with no MIME type.
+        ContentValues values = getDefaultVoicemailValues();
+        Uri uri = mResolver.insert(contentUri(), values);
+        assertEquals(null, mResolver.getType(uri));
+
+        values.put(Voicemails.MIME_TYPE, "foo/bar");
+        uri = mResolver.insert(contentUri(), values);
+        assertEquals("foo/bar", mResolver.getType(uri));
+
+        // base URIs.
+        assertEquals(Voicemails.DIR_TYPE, mResolver.getType(Voicemails.CONTENT_URI));
+        assertEquals(Voicemails.DIR_TYPE, mResolver.getType(Voicemails.buildSourceUri("foo")));
+    }
+
+    // Test to ensure that without full permission it is not possible to use the base uri (i.e. with
+    // no package URI specified).
+    public void testMustUsePackageUriWithoutFullPermission() {
+        setUpForOwnPermission();
+        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
+            @Override
+            public void run() {
+                mResolver.insert(Voicemails.CONTENT_URI, getDefaultVoicemailValues());
+            }
+        });
+
+        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
+            @Override
+            public void run() {
+                mResolver.update(Voicemails.CONTENT_URI, getDefaultVoicemailValues(), null, null);
+            }
+        });
+
+        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
+            @Override
+            public void run() {
+                mResolver.query(Voicemails.CONTENT_URI, null, null, null, null);
+            }
+        });
+
+        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
+            @Override
+            public void run() {
+                mResolver.delete(Voicemails.CONTENT_URI, null, null);
+            }
+        });
+    }
+
     public void testPermissions_InsertAndQuery() {
         setUpForFullPermission();
         // Insert two records - one each with own and another package.
@@ -197,8 +242,9 @@
         // Now give away full permission and check that we can update and delete only
         // the own voicemail.
         setUpForOwnPermission();
-        mResolver.update(ownVoicemail, getDefaultVoicemailValues(), null, null);
-        mResolver.delete(ownVoicemail, null, null);
+        mResolver.update(withSourcePackageParam(ownVoicemail),
+                getDefaultVoicemailValues(), null, null);
+        mResolver.delete(withSourcePackageParam(ownVoicemail), null, null);
 
         // However, attempting to update or delete another-package's voicemail should fail.
         EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
@@ -215,6 +261,12 @@
         });
     }
 
+    private Uri withSourcePackageParam(Uri uri) {
+        return uri.buildUpon()
+            .appendQueryParameter(VoicemailContract.PARAM_KEY_SOURCE_PACKAGE, mActor.packageName)
+            .build();
+    }
+
     // Test to ensure that all operations fail when no voicemail permission is granted.
     public void testNoPermissions() {
         setUpForNoPermission();
@@ -343,6 +395,10 @@
                 public File getDir(String name, int mode) {
                     return mDelgate.getDir(name, mode);
                 }
+                @Override
+                public void sendBroadcast(Intent intent, String receiverPermission) {
+                    mDelgate.sendOrderedBroadcast(intent, receiverPermission);
+                }
             };
         }
 
@@ -350,6 +406,14 @@
         protected String getCallingPackage() {
             return getContext().getPackageName();
         }
+
+        @Override
+        protected List<ComponentName> getBroadcastReceiverComponents(String intentAction, Uri uri) {
+            List<ComponentName> broadcastReceiverComponents = new ArrayList<ComponentName>();
+            broadcastReceiverComponents.add(new ComponentName(
+                    getContext().getPackageName(), "TestReceiverClass"));
+            return broadcastReceiverComponents;
+        }
     }
 
     /** Lazily construct the test directory when required. */
@@ -401,7 +465,7 @@
         return new VvmProviderCalls() {
             @Override
             public void sendOrderedBroadcast(Intent intent, String receiverPermission) {
-                // TODO: Auto-generated method stub
+                // Do nothing for now.
             }
 
             @Override