Merge "VoicemailStatus content provider implementation."
diff --git a/src/com/android/providers/contacts/VoicemailContentProvider.java b/src/com/android/providers/contacts/VoicemailContentProvider.java
index 3a093c0..6cdde4c 100644
--- a/src/com/android/providers/contacts/VoicemailContentProvider.java
+++ b/src/com/android/providers/contacts/VoicemailContentProvider.java
@@ -15,6 +15,8 @@
  */
 package com.android.providers.contacts;
 
+import static android.provider.VoicemailContract.SOURCE_PACKAGE_FIELD;
+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;
@@ -33,6 +35,7 @@
 import android.net.Uri;
 import android.os.Binder;
 import android.os.ParcelFileDescriptor;
+import android.provider.BaseColumns;
 import android.provider.VoicemailContract;
 import android.provider.VoicemailContract.Voicemails;
 
@@ -41,23 +44,28 @@
 import java.util.List;
 
 /**
- * An implementation of the Voicemail content provider.
+ * An implementation of the Voicemail content provider. This class in the entry point for both
+ * voicemail content ('calls') table and 'voicemail_status' table. This class performs all common
+ * permission checks and then delegates database level operations to respective table delegate
+ * objects.
  */
 public class VoicemailContentProvider extends ContentProvider
         implements VoicemailTable.DelegateHelper {
     private static final String TAG = "VoicemailContentProvider";
-    private static final String VOICEMAILS_TABLE_NAME = Tables.CALLS;
 
     private ContentResolver mContentResolver;
     private VoicemailPermissions mVoicemailPermissions;
     private VoicemailTable.Delegate mVoicemailContentTable;
+    private VoicemailTable.Delegate mVoicemailStatusTable;
 
     @Override
     public boolean onCreate() {
         Context context = context();
         mContentResolver = context.getContentResolver();
         mVoicemailPermissions = new VoicemailPermissions(context);
-        mVoicemailContentTable = new VoicemailContentTable(VOICEMAILS_TABLE_NAME, context,
+        mVoicemailContentTable = new VoicemailContentTable(Tables.CALLS, context,
+                getDatabaseHelper(context), this);
+        mVoicemailStatusTable = new VoicemailStatusTable(Tables.VOICEMAIL_STATUS, context,
                 getDatabaseHelper(context), this);
         return true;
     }
@@ -79,19 +87,19 @@
             // Special case: for illegal URIs, we return null rather than thrown an exception.
             return null;
         }
-        return mVoicemailContentTable.getType(uriData);
+        return getTableDelegate(uriData).getType(uriData);
     }
 
     @Override
     public int bulkInsert(Uri uri, ContentValues[] valuesArray) {
-        UriData uriData = checkPermissionsAndCreateUriData(uri);
-        return mVoicemailContentTable.bulkInsert(uriData, valuesArray);
+        UriData uriData = checkPermissionsAndCreateUriData(uri, valuesArray);
+        return getTableDelegate(uriData).bulkInsert(uriData, valuesArray);
     }
 
     @Override
     public Uri insert(Uri uri, ContentValues values) {
-        UriData uriData = checkPermissionsAndCreateUriData(uri);
-        return mVoicemailContentTable.insert(uriData, values);
+        UriData uriData = checkPermissionsAndCreateUriData(uri, values);
+        return getTableDelegate(uriData).insert(uriData, values);
     }
 
     @Override
@@ -100,16 +108,16 @@
         UriData uriData = checkPermissionsAndCreateUriData(uri);
         SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
         selectionBuilder.addClause(getPackageRestrictionClause());
-        return mVoicemailContentTable.query(uriData, projection, selectionBuilder.build(),
+        return getTableDelegate(uriData).query(uriData, projection, selectionBuilder.build(),
                 selectionArgs, sortOrder);
     }
 
     @Override
     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
-        UriData uriData = checkPermissionsAndCreateUriData(uri);
+        UriData uriData = checkPermissionsAndCreateUriData(uri, values);
         SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
         selectionBuilder.addClause(getPackageRestrictionClause());
-        return mVoicemailContentTable.update(uriData, values, selectionBuilder.build(),
+        return getTableDelegate(uriData).update(uriData, values, selectionBuilder.build(),
                 selectionArgs);
     }
 
@@ -118,14 +126,30 @@
         UriData uriData = checkPermissionsAndCreateUriData(uri);
         SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
         selectionBuilder.addClause(getPackageRestrictionClause());
-        return mVoicemailContentTable.delete(uriData, selectionBuilder.build(), selectionArgs);
+        return getTableDelegate(uriData).delete(uriData, selectionBuilder.build(), selectionArgs);
     }
 
     @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));
+        return getTableDelegate(uriData).openFile(uriData, mode);
+    }
+
+    /** Returns the correct table delegate object that can handle this URI. */
+    private VoicemailTable.Delegate getTableDelegate(UriData uriData) {
+        switch (uriData.getUriType()) {
+            case STATUS:
+            case STATUS_ID:
+                return mVoicemailStatusTable;
+            case VOICEMAILS:
+            case VOICEMAILS_ID:
+                return mVoicemailContentTable;
+            case NO_MATCH:
+                throw new IllegalStateException("Invalid uri type for uri: " + uriData.getUri());
+            default:
+                throw new IllegalStateException("Impossible, all cases are covered.");
+        }
     }
 
     /**
@@ -135,8 +159,10 @@
         private final Uri mUri;
         private final String mId;
         private final String mSourcePackage;
+        private final VoicemailUriType mUriType;
 
-        public UriData(Uri uri, String id, String sourcePackage) {
+        public UriData(Uri uri, VoicemailUriType uriType, String id, String sourcePackage) {
+            mUriType = uriType;
             mUri = uri;
             mId = id;
             mSourcePackage = sourcePackage;
@@ -167,22 +193,43 @@
             return mSourcePackage;
         }
 
+        /** Gets the Voicemail URI type. */
+        public final VoicemailUriType getUriType() {
+            return mUriType;
+        }
+
+        /** Builds a where clause from the URI data. */
+        public final String getWhereClause() {
+            return concatenateClauses(
+                    (hasId() ? getEqualityClause(BaseColumns._ID, getId()) : null),
+                    (hasSourcePackage() ? getEqualityClause(SOURCE_PACKAGE_FIELD,
+                            getSourcePackage()) : null));
+        }
+
         /** 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)) {
+            VoicemailUriType uriType = createUriMatcher().match(uri);
+            switch (uriType) {
                 case VOICEMAILS:
-                    return new UriData(uri, null, sourcePackage);
+                case STATUS:
+                    return new UriData(uri, uriType, null, sourcePackage);
                 case VOICEMAILS_ID:
-                    return new UriData(uri, segments.get(1), sourcePackage);
+                case STATUS_ID:
+                    return new UriData(uri, uriType, segments.get(1), sourcePackage);
                 case NO_MATCH:
                     throw new IllegalArgumentException("Invalid URI: " + uri);
                 default:
                     throw new IllegalStateException("Impossible, all cases are covered");
             }
         }
+
+        private static TypedUriMatcherImpl<VoicemailUriType> createUriMatcher() {
+            return new TypedUriMatcherImpl<VoicemailUriType>(
+                    VoicemailContract.AUTHORITY, VoicemailUriType.values());
+        }
     }
 
     @Override
@@ -214,21 +261,10 @@
     // 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)) {
+        if (!values.containsKey(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));
-            }
+            values.put(SOURCE_PACKAGE_FIELD, provider);
         }
         // You must have access to the provider given in values.
         if (!mVoicemailPermissions.callerHasFullAccess()) {
@@ -238,9 +274,26 @@
         }
     }
 
-    private static TypedUriMatcherImpl<VoicemailUriType> createUriMatcher() {
-        return new TypedUriMatcherImpl<VoicemailUriType>(
-                VoicemailContract.AUTHORITY, VoicemailUriType.values());
+    /**
+     * Checks that the source_package field is same in uriData and ContentValues, if it happens
+     * to be set in both.
+     */
+    private void checkSourcePackageSameIfSet(UriData uriData, ContentValues values) {
+        if (uriData.hasSourcePackage() && values.containsKey(SOURCE_PACKAGE_FIELD)) {
+            if (!uriData.getSourcePackage().equals(values.get(SOURCE_PACKAGE_FIELD))) {
+                throw new SecurityException(
+                        "source_package in URI was " + uriData.getSourcePackage() +
+                        " but doesn't match source_package in ContentValues which was "
+                        + values.get(SOURCE_PACKAGE_FIELD));
+            }
+        }
+    }
+
+    @Override
+    /** Implementation of  {@link VoicemailTable.DelegateHelper#openDataFile(UriData, String)} */
+    public ParcelFileDescriptor openDataFile(UriData uriData, String mode)
+            throws FileNotFoundException {
+        return openFileHelper(uriData.getUri(), mode);
     }
 
     /**
@@ -255,15 +308,27 @@
     }
 
     /**
-     * Checks that the callingProvider is same as voicemailProvider. Throws {@link
+     * Same as {@link #checkPackagePermission(UriData)}. In addition does permission check
+     * on the ContentValues.
+     */
+    private UriData checkPermissionsAndCreateUriData(Uri uri, ContentValues... valuesArray) {
+        UriData uriData = checkPermissionsAndCreateUriData(uri);
+        for (ContentValues values : valuesArray) {
+            checkSourcePackageSameIfSet(uriData, values);
+        }
+        return uriData;
+    }
+
+    /**
+     * Checks that the callingPackage is same as voicemailSourcePackage. Throws {@link
      * SecurityException} if they don't match.
      */
-    private final void checkPackagesMatch(String callingProvider, String voicemailProvider,
+    private final void checkPackagesMatch(String callingPackage, String voicemailSourcePackage,
             Uri uri) {
-        if (!voicemailProvider.equals(callingProvider)) {
+        if (!voicemailSourcePackage.equals(callingPackage)) {
             String errorMsg = String.format("Permission denied for URI: %s\n. " +
-                    "Provider %s cannot perform this operation for %s. Requires %s permission.",
-                    uri, callingProvider, voicemailProvider,
+                    "Package %s cannot perform this operation for %s. Requires %s permission.",
+                    uri, callingPackage, voicemailSourcePackage,
                     Manifest.permission.READ_WRITE_ALL_VOICEMAIL);
             throw new SecurityException(errorMsg);
         }
@@ -272,7 +337,7 @@
     /**
      * Checks that either the caller has READ_WRITE_ALL_VOICEMAIL permission, or has the
      * READ_WRITE_OWN_VOICEMAIL permission and is using a URI that matches
-     * /voicemail/source/[source-package] where [source-package] is the same as the calling
+     * /voicemail/?source_package=[source-package] where [source-package] is the same as the calling
      * package.
      *
      * @throws SecurityException if the check fails.
@@ -280,7 +345,7 @@
     private void checkPackagePermission(UriData uriData) {
         if (!mVoicemailPermissions.callerHasFullAccess()) {
             if (!uriData.hasSourcePackage()) {
-                // You cannot have a match if this is not a provider uri.
+                // 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 set query parameter '%s' in the URI.\nURI: %s",
diff --git a/src/com/android/providers/contacts/VoicemailContentTable.java b/src/com/android/providers/contacts/VoicemailContentTable.java
index 8f6f3bf..0712a65 100644
--- a/src/com/android/providers/contacts/VoicemailContentTable.java
+++ b/src/com/android/providers/contacts/VoicemailContentTable.java
@@ -38,6 +38,7 @@
 import android.util.Log;
 
 import java.io.File;
+import java.io.FileNotFoundException;
 import java.io.IOException;
 
 /**
@@ -152,14 +153,13 @@
         // 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);
+        db.update(mTableName, values, UriData.createUriData(newUri).getWhereClause(), null);
     }
 
     @Override
     public int delete(UriData uriData, String selection, String[] selectionArgs) {
         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
-        String combinedClause = concatenateClauses(selection, getWhereClause(uriData),
+        String combinedClause = concatenateClauses(selection, uriData.getWhereClause(),
                 getCallTypeClause());
 
         // Delete all the files associated with this query.  Once we've deleted the rows, there will
@@ -196,7 +196,7 @@
         qb.setProjectionMap(sVoicemailProjectionMap);
         qb.setStrict(true);
 
-        String combinedClause = concatenateClauses(selection, getWhereClause(uriData),
+        String combinedClause = concatenateClauses(selection, uriData.getWhereClause(),
                 getCallTypeClause());
         SQLiteDatabase db = mDbHelper.getReadableDatabase();
         Cursor c = qb.query(db, projection, combinedClause, selectionArgs, null, null, sortOrder);
@@ -214,7 +214,7 @@
         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),
+        String combinedClause = concatenateClauses(selection, uriData.getWhereClause(),
                 getCallTypeClause());
         int count = db.update(mTableName, values, combinedClause, selectionArgs);
         if (count > 0) {
@@ -261,25 +261,16 @@
     }
 
     @Override
-    public ParcelFileDescriptor openFile(UriData uriData, String mode,
-            ParcelFileDescriptor openFileHelper) {
+    public ParcelFileDescriptor openFile(UriData uriData, String mode)
+            throws FileNotFoundException {
+        ParcelFileDescriptor fileDescriptor = mDelegateHelper.openDataFile(uriData, mode);
         // 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));
+        return fileDescriptor;
     }
 
     /** Creates a clause to restrict the selection to only voicemail call type.*/
diff --git a/src/com/android/providers/contacts/VoicemailStatusTable.java b/src/com/android/providers/contacts/VoicemailStatusTable.java
new file mode 100644
index 0000000..1f1fa8b
--- /dev/null
+++ b/src/com/android/providers/contacts/VoicemailStatusTable.java
@@ -0,0 +1,135 @@
+/*
+ * 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.concatenateClauses;
+
+import com.android.providers.contacts.VoicemailContentProvider.UriData;
+
+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.VoicemailContract.Status;
+
+/**
+ * Implementation of {@link VoicemailTable.Delegate} for the voicemail status table.
+ */
+public class VoicemailStatusTable implements VoicemailTable.Delegate {
+    private static final ProjectionMap sStatusProjectionMap = new ProjectionMap.Builder()
+            .add(Status._ID)
+            .add(Status.CONFIGURATION_STATE)
+            .add(Status.DATA_CHANNEL_STATE)
+            .add(Status.NOTIFICATION_CHANNEL_STATE)
+            .add(Status.SETTINGS_URI)
+            .add(Status.SOURCE_PACKAGE)
+            .add(Status.VOICEMAIL_ACCESS_URI)
+            .build();
+
+    private final String mTableName;
+    private final Context mContext;
+    private final SQLiteOpenHelper mDbHelper;
+    private final VoicemailTable.DelegateHelper mDelegateHelper;
+
+    public VoicemailStatusTable(String tableName, Context context, SQLiteOpenHelper dbHelper,
+            VoicemailTable.DelegateHelper delegateHelper) {
+        mTableName = tableName;
+        mContext = context;
+        mDbHelper = dbHelper;
+        mDelegateHelper = delegateHelper;
+    }
+
+    @Override
+    public Uri insert(UriData uriData, ContentValues values) {
+        SQLiteDatabase db = mDbHelper.getWritableDatabase();
+        ContentValues copiedValues = new ContentValues(values);
+        mDelegateHelper.checkAndAddSourcePackageIntoValues(uriData, copiedValues);
+        long rowId = db.insert(mTableName, null, copiedValues);
+        if (rowId > 0) {
+            Uri newUri = ContentUris.withAppendedId(uriData.getUri(), rowId);
+            mDelegateHelper.notifyChange(newUri, Intent.ACTION_PROVIDER_CHANGED);
+            return newUri;
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public int bulkInsert(UriData uriData, ContentValues[] valuesArray) {
+        throw new UnsupportedOperationException("bulkInsert is not supported for status table");
+    }
+
+    @Override
+    public int delete(UriData uriData, String selection, String[] selectionArgs) {
+        SQLiteDatabase db = mDbHelper.getWritableDatabase();
+        String combinedClause = concatenateClauses(selection, uriData.getWhereClause());
+        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(sStatusProjectionMap);
+        qb.setStrict(true);
+
+        String combinedClause = concatenateClauses(selection, uriData.getWhereClause());
+        SQLiteDatabase db = mDbHelper.getReadableDatabase();
+        Cursor c = qb.query(db, projection, combinedClause, selectionArgs, null, null, sortOrder);
+        if (c != null) {
+            c.setNotificationUri(mContext.getContentResolver(), Status.CONTENT_URI);
+        }
+        return c;
+    }
+
+    @Override
+    public int update(UriData uriData, ContentValues values, String selection,
+            String[] selectionArgs) {
+        SQLiteDatabase db = mDbHelper.getWritableDatabase();
+        String combinedClause = concatenateClauses(selection, uriData.getWhereClause());
+        int count = db.update(mTableName, values, combinedClause, selectionArgs);
+        if (count > 0) {
+            mDelegateHelper.notifyChange(uriData.getUri(), Intent.ACTION_PROVIDER_CHANGED);
+        }
+        return count;
+    }
+
+    @Override
+    public String getType(UriData uriData) {
+        if (uriData.hasId()) {
+            return Status.ITEM_TYPE;
+        } else {
+            return Status.DIR_TYPE;
+        }
+    }
+
+    @Override
+    public ParcelFileDescriptor openFile(UriData uriData, String mode) {
+        throw new UnsupportedOperationException("File operation is not supported for status table");
+    }
+}
diff --git a/src/com/android/providers/contacts/VoicemailTable.java b/src/com/android/providers/contacts/VoicemailTable.java
index d068775..cab48fd 100644
--- a/src/com/android/providers/contacts/VoicemailTable.java
+++ b/src/com/android/providers/contacts/VoicemailTable.java
@@ -22,6 +22,8 @@
 import android.net.Uri;
 import android.os.ParcelFileDescriptor;
 
+import java.io.FileNotFoundException;
+
 /**
  * Defines interfaces for communication between voicemail content provider and voicemail table
  * implementations.
@@ -40,8 +42,8 @@
         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);
+        public ParcelFileDescriptor openFile(UriData uriData, String mode)
+                throws FileNotFoundException;
     }
 
     /**
@@ -63,5 +65,12 @@
          * Inserts source_package field into ContentValues. Used in insert operations.
          */
         public void checkAndAddSourcePackageIntoValues(UriData uriData, ContentValues values);
+
+        /**
+         * Opens the file pointed to by the column "_data".
+         * @throws FileNotFoundException
+         */
+        public ParcelFileDescriptor openDataFile(UriData uriData, String mode)
+                throws FileNotFoundException;
     }
-}
\ 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 c3a33ab..eb1c3bd 100644
--- a/src/com/android/providers/contacts/VoicemailUriType.java
+++ b/src/com/android/providers/contacts/VoicemailUriType.java
@@ -23,7 +23,9 @@
 enum VoicemailUriType implements UriType {
     NO_MATCH(null),
     VOICEMAILS("voicemail"),
-    VOICEMAILS_ID("voicemail/#");
+    VOICEMAILS_ID("voicemail/#"),
+    STATUS("status"),
+    STATUS_ID("status/#");
 
     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 98498bd..d0c8d5c 100644
--- a/tests/src/com/android/providers/contacts/VoicemailProviderTest.java
+++ b/tests/src/com/android/providers/contacts/VoicemailProviderTest.java
@@ -27,10 +27,12 @@
 import android.net.Uri;
 import android.provider.CallLog.Calls;
 import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Status;
 import android.provider.VoicemailContract.Voicemails;
 import android.test.MoreAsserts;
 
 import java.io.File;
+import java.io.FileNotFoundException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.ArrayList;
@@ -110,14 +112,22 @@
         mUseSourceUri = true;
     }
 
-    private Uri contentUri() {
+    /** Returns the appropriate /voicemail URI. */
+    private Uri voicemailUri() {
         return mUseSourceUri ?
                 Voicemails.buildSourceUri(mActor.packageName) : Voicemails.CONTENT_URI;
     }
 
+    /** Returns the appropriate /status URI. */
+    private Uri statusUri() {
+        return mUseSourceUri ?
+                Status.buildSourceUri(mActor.packageName) : Status.CONTENT_URI;
+    }
+
+
     public void testInsert() throws Exception {
-        ContentValues values = getDefaultVoicemailValues();
-        Uri uri = mResolver.insert(contentUri(), values);
+        ContentValues values = getTestVoicemailValues();
+        Uri uri = mResolver.insert(voicemailUri(), values);
         assertStoredValues(uri, values);
         assertSelection(uri, values, Voicemails._ID, ContentUris.parseId(uri));
         assertEquals(1, countFilesInTestDirectory());
@@ -157,7 +167,7 @@
 
     public void testDelete() {
         Uri uri = insertVoicemail();
-        int count = mResolver.delete(contentUri(), Voicemails._ID + "="
+        int count = mResolver.delete(voicemailUri(), Voicemails._ID + "="
                 + ContentUris.parseId(uri), null);
         assertEquals(1, count);
         assertEquals(0, getCount(uri, null, null));
@@ -165,12 +175,12 @@
 
     public void testGetType() throws Exception {
         // voicemail with no MIME type.
-        ContentValues values = getDefaultVoicemailValues();
-        Uri uri = mResolver.insert(contentUri(), values);
+        ContentValues values = getTestVoicemailValues();
+        Uri uri = mResolver.insert(voicemailUri(), values);
         assertEquals(null, mResolver.getType(uri));
 
         values.put(Voicemails.MIME_TYPE, "foo/bar");
-        uri = mResolver.insert(contentUri(), values);
+        uri = mResolver.insert(voicemailUri(), values);
         assertEquals("foo/bar", mResolver.getType(uri));
 
         // base URIs.
@@ -185,14 +195,14 @@
         EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
             @Override
             public void run() {
-                mResolver.insert(Voicemails.CONTENT_URI, getDefaultVoicemailValues());
+                mResolver.insert(Voicemails.CONTENT_URI, getTestVoicemailValues());
             }
         });
 
         EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
             @Override
             public void run() {
-                mResolver.update(Voicemails.CONTENT_URI, getDefaultVoicemailValues(), null, null);
+                mResolver.update(Voicemails.CONTENT_URI, getTestVoicemailValues(), null, null);
             }
         });
 
@@ -216,11 +226,11 @@
         // Insert two records - one each with own and another package.
         insertVoicemail();
         insertVoicemailForSourcePackage("another-package");
-        assertEquals(2, getCount(contentUri(), null, null));
+        assertEquals(2, getCount(voicemailUri(), null, null));
 
         // Now give away full permission and check that only 1 message is accessible.
         setUpForOwnPermission();
-        assertEquals(1, getCount(contentUri(), null, null));
+        assertEquals(1, getCount(voicemailUri(), null, null));
 
         // Once again try to insert message for another package. This time
         // it should fail.
@@ -237,13 +247,13 @@
         // Insert two records - one each with own and another package.
         final Uri ownVoicemail = insertVoicemail();
         final Uri anotherVoicemail = insertVoicemailForSourcePackage("another-package");
-        assertEquals(2, getCount(contentUri(), null, null));
+        assertEquals(2, getCount(voicemailUri(), null, null));
 
         // Now give away full permission and check that we can update and delete only
         // the own voicemail.
         setUpForOwnPermission();
         mResolver.update(withSourcePackageParam(ownVoicemail),
-                getDefaultVoicemailValues(), null, null);
+                getTestVoicemailValues(), null, null);
         mResolver.delete(withSourcePackageParam(ownVoicemail), null, null);
 
         // However, attempting to update or delete another-package's voicemail should fail.
@@ -273,25 +283,25 @@
         EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
             @Override
             public void run() {
-                mResolver.insert(contentUri(), getDefaultVoicemailValues());
+                mResolver.insert(voicemailUri(), getTestVoicemailValues());
             }
         });
         EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
             @Override
             public void run() {
-                mResolver.update(contentUri(), getDefaultVoicemailValues(), null, null);
+                mResolver.update(voicemailUri(), getTestVoicemailValues(), null, null);
             }
         });
         EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
             @Override
             public void run() {
-                mResolver.query(contentUri(), null, null, null, null);
+                mResolver.query(voicemailUri(), null, null, null, null);
             }
         });
         EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
             @Override
             public void run() {
-                mResolver.delete(contentUri(), null, null);
+                mResolver.delete(voicemailUri(), null, null);
             }
         });
     }
@@ -300,13 +310,13 @@
     // insertable through voicemail provider.
     public void testCannotAccessCallLogSpecificFields_Insert() {
         for (String callLogColumn : CALLLOG_PROVIDER_SPECIFIC_COLUMNS) {
-            final ContentValues values = getDefaultVoicemailValues();
+            final ContentValues values = getTestVoicemailValues();
             values.put(callLogColumn, "foo");
             EvenMoreAsserts.assertThrows("Column: " + callLogColumn,
                     IllegalArgumentException.class, new Runnable() {
                     @Override
                     public void run() {
-                        mResolver.insert(contentUri(), values);
+                        mResolver.insert(voicemailUri(), values);
                     }
                 });
         }
@@ -316,7 +326,7 @@
     // exposed through voicemail provider query.
     public void testCannotAccessCallLogSpecificFields_Query() {
         // Query.
-        Cursor cursor = mResolver.query(contentUri(), null, null, null, null);
+        Cursor cursor = mResolver.query(voicemailUri(), null, null, null, null);
         List<String> columnNames = Arrays.asList(cursor.getColumnNames());
         assertEquals(NUM_VOICEMAIL_FIELDS, columnNames.size());
         // None of the call_log provider specific columns should be present.
@@ -331,7 +341,7 @@
     public void testCannotAccessCallLogSpecificFields_Update() {
         for (String callLogColumn : CALLLOG_PROVIDER_SPECIFIC_COLUMNS) {
             final Uri insertedUri = insertVoicemail();
-            final ContentValues values = getDefaultVoicemailValues();
+            final ContentValues values = getTestVoicemailValues();
             values.put(callLogColumn, "foo");
             EvenMoreAsserts.assertThrows("Column: " + callLogColumn,
                     IllegalArgumentException.class, new Runnable() {
@@ -343,22 +353,137 @@
         }
     }
 
+    // Tests for voicemail status table.
+
+    public void testStatusInsert() throws Exception {
+        ContentValues values = getTestStatusValues();
+        Uri uri = mResolver.insert(statusUri(), values);
+        assertStoredValues(uri, values);
+        assertSelection(uri, values, Status._ID, ContentUris.parseId(uri));
+    }
+
+    // Test to ensure that duplicate entries for the same package still end up as the same record.
+    public void testStatusInsertDuplicate() throws Exception {
+        setUpForFullPermission();
+        ContentValues values = getTestStatusValues();
+        assertNotNull(mResolver.insert(statusUri(), values));
+        assertEquals(1, getCount(statusUri(), null, null));
+
+        // Insertion request for the same package should fail with no change in count.
+        values.put(Status.DATA_CHANNEL_STATE, Status.DATA_CHANNEL_STATE_NO_CONNECTION);
+        assertNull(mResolver.insert(statusUri(), values));
+        assertEquals(1, getCount(statusUri(), null, null));
+
+        // Now insert entry for another source package, and it should end up as a separate record.
+        values.put(Status.SOURCE_PACKAGE, "another.package");
+        assertNotNull(mResolver.insert(statusUri(), values));
+        assertEquals(2, getCount(statusUri(), null, null));
+    }
+
+    public void testStatusUpdate() throws Exception {
+        Uri uri = insertTestStatusEntry();
+        ContentValues values = getTestStatusValues();
+        values.put(Status.DATA_CHANNEL_STATE, Status.DATA_CHANNEL_STATE_NO_CONNECTION);
+        values.put(Status.NOTIFICATION_CHANNEL_STATE,
+                Status.NOTIFICATION_CHANNEL_STATE_MESSAGE_WAITING);
+        int count = mResolver.update(uri, values, null, null);
+        assertEquals(1, count);
+        assertStoredValues(uri, values);
+    }
+
+    public void testStatusDelete() {
+        Uri uri = insertTestStatusEntry();
+        int count = mResolver.delete(statusUri(), Status._ID + "="
+                + ContentUris.parseId(uri), null);
+        assertEquals(1, count);
+        assertEquals(0, getCount(uri, null, null));
+    }
+
+    public void testStatusGetType() throws Exception {
+        // Item URI.
+        Uri uri = insertTestStatusEntry();
+        assertEquals(Status.ITEM_TYPE, mResolver.getType(uri));
+
+        // base URIs.
+        assertEquals(Status.DIR_TYPE, mResolver.getType(Status.CONTENT_URI));
+        assertEquals(Status.DIR_TYPE, mResolver.getType(Status.buildSourceUri("foo")));
+    }
+
+    // Basic permission checks for the status table.
+    public void testStatusPermissions() throws Exception {
+        final ContentValues values = getTestStatusValues();
+        // Inserting for another package should fail with any of the URIs.
+        values.put(Status.SOURCE_PACKAGE, "another.package");
+        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
+            @Override
+            public void run() {
+                mResolver.insert(Status.CONTENT_URI, values);
+            }
+        });
+        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
+            @Override
+            public void run() {
+                mResolver.insert(Status.buildSourceUri(mActor.packageName), values);
+            }
+        });
+
+        // But insertion with own package should succeed with the right uri.
+        values.put(Status.SOURCE_PACKAGE, mActor.packageName);
+        final Uri uri = mResolver.insert(Status.buildSourceUri(mActor.packageName), values);
+        assertNotNull(uri);
+
+        // Updating source_package should not work as well.
+        values.put(Status.SOURCE_PACKAGE, "another.package");
+        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
+            @Override
+            public void run() {
+                mResolver.update(uri, values, null, null);
+            }
+        });
+    }
+
+    // File operation is not supported by /status URI.
+    public void testStatusFileOperation() throws Exception {
+        final Uri uri = insertTestStatusEntry();
+        EvenMoreAsserts.assertThrows(UnsupportedOperationException.class, new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    mResolver.openOutputStream(uri);
+                } catch (FileNotFoundException e) {
+                    fail("Unexpected exception " + e);
+                }
+            }
+        });
+
+        EvenMoreAsserts.assertThrows(UnsupportedOperationException.class, new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    mResolver.openInputStream(uri);
+                } catch (FileNotFoundException e) {
+                    fail("Unexpected exception " + e);
+                }
+            }
+        });
+    }
+
     /**
      * Inserts a voicemail record with no source package set. The content provider
      * will detect source package.
      */
     private Uri insertVoicemail() {
-        return mResolver.insert(contentUri(), getDefaultVoicemailValues());
+        return mResolver.insert(voicemailUri(), getTestVoicemailValues());
     }
 
     /** Inserts a voicemail record for the specified source package. */
     private Uri insertVoicemailForSourcePackage(String sourcePackage) {
-        ContentValues values = getDefaultVoicemailValues();
+        ContentValues values = getTestVoicemailValues();
         values.put(Voicemails.SOURCE_PACKAGE, sourcePackage);
-        return mResolver.insert(contentUri(), values);
+        return mResolver.insert(voicemailUri(), values);
     }
 
-    private ContentValues getDefaultVoicemailValues() {
+    private ContentValues getTestVoicemailValues() {
         ContentValues values = new ContentValues();
         values.put(Voicemails.NUMBER, "1-800-4664-411");
         values.put(Voicemails.DATE, 1000);
@@ -370,6 +495,21 @@
         return values;
     }
 
+    private Uri insertTestStatusEntry() {
+        return mResolver.insert(statusUri(), getTestStatusValues());
+    }
+
+    private ContentValues getTestStatusValues() {
+        ContentValues values = new ContentValues();
+        values.put(Status.SOURCE_PACKAGE, mActor.packageName);
+        values.put(Status.VOICEMAIL_ACCESS_URI, "tel:901");
+        values.put(Status.SETTINGS_URI, "com.example.voicemail.source.SettingsActivity");
+        values.put(Status.CONFIGURATION_STATE, Status.CONFIGURATION_STATE_OK);
+        values.put(Status.DATA_CHANNEL_STATE, Status.DATA_CHANNEL_STATE_OK);
+        values.put(Status.NOTIFICATION_CHANNEL_STATE, Status.NOTIFICATION_CHANNEL_STATE_OK);
+        return values;
+    }
+
     /** The calls that we need to mock out for our VvmProvider, used by TestVoicemailProvider. */
     public interface VvmProviderCalls {
         public void sendOrderedBroadcast(Intent intent, String receiverPermission);