Introduced new voicemail fields in 'calls' table.

The 'calls' table is going to be shared between the existing call_log
provider, and to be added voicemail provider. This change adds all the
columns needed to support voicemail in the 'calls' table.

The call_log provider, however, uses only one additional field
'voicemail_uri', whereas all other new fields will exclusively be used the
new voicemail provider. The change also ensures that the voicemail
provider specific fields are not exposed through the call_log provider.

Change-Id: Ieea4b14052b7e7e9db0e674138772b4e06b3f074
diff --git a/src/com/android/providers/contacts/CallLogProvider.java b/src/com/android/providers/contacts/CallLogProvider.java
index ae43c88..689e6be 100644
--- a/src/com/android/providers/contacts/CallLogProvider.java
+++ b/src/com/android/providers/contacts/CallLogProvider.java
@@ -32,6 +32,7 @@
 import android.provider.CallLog.Calls;
 
 import java.util.HashMap;
+import java.util.Set;
 
 /**
  * Call log content provider.
@@ -62,6 +63,7 @@
         sCallsProjectionMap.put(Calls.DURATION, Calls.DURATION);
         sCallsProjectionMap.put(Calls.TYPE, Calls.TYPE);
         sCallsProjectionMap.put(Calls.NEW, Calls.NEW);
+        sCallsProjectionMap.put(Calls.VOICEMAIL_URI, Calls.VOICEMAIL_URI);
         sCallsProjectionMap.put(Calls.CACHED_NAME, Calls.CACHED_NAME);
         sCallsProjectionMap.put(Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_TYPE);
         sCallsProjectionMap.put(Calls.CACHED_NUMBER_LABEL, Calls.CACHED_NUMBER_LABEL);
@@ -93,25 +95,26 @@
     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
             String sortOrder) {
         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+        qb.setTables(Tables.CALLS);
+        qb.setProjectionMap(sCallsProjectionMap);
+        qb.setStrict(true);
+
         int match = sURIMatcher.match(uri);
         switch (match) {
-            case CALLS: {
-                qb.setTables("calls");
-                qb.setProjectionMap(sCallsProjectionMap);
+            case CALLS:
                 break;
-            }
 
             case CALLS_ID: {
-                qb.setTables("calls");
-                qb.setProjectionMap(sCallsProjectionMap);
-                qb.appendWhere("calls._id=");
-                qb.appendWhere(uri.getPathSegments().get(1));
+                try {
+                    Long id = Long.valueOf(uri.getPathSegments().get(1));
+                    qb.appendWhere(Calls._ID + "=" + id.toString());
+                } catch (NumberFormatException e) {
+                    throw new IllegalArgumentException("Invalid call id in uri: " + uri, e);
+                }
                 break;
             }
 
             case CALLS_FILTER: {
-                qb.setTables("calls");
-                qb.setProjectionMap(sCallsProjectionMap);
                 String phoneNumber = uri.getPathSegments().get(2);
                 qb.appendWhere("PHONE_NUMBERS_EQUAL(number, ");
                 qb.appendWhereEscapeString(phoneNumber);
@@ -148,6 +151,7 @@
 
     @Override
     public Uri insert(Uri uri, ContentValues values) {
+        checkForSupportedColumns(values);
         // Inserted the current country code, so we know the country
         // the number belongs to.
         values.put(Calls.COUNTRY_ISO, getCurrentCountryIso());
@@ -166,6 +170,7 @@
 
     @Override
     public int update(Uri url, ContentValues values, String selection, String[] selectionArgs) {
+        checkForSupportedColumns(values);
         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
         String where;
         final int matchedUriId = sURIMatcher.match(url);
@@ -216,4 +221,13 @@
     protected String getCurrentCountryIso() {
         return mCountryMonitor.getCountryIso();
     }
+
+    /** Checks if ContentValues contains none other than supported columns. */
+    private void checkForSupportedColumns(ContentValues values) {
+        for (String requestedColumn : values.keySet()) {
+            if (!sCallsProjectionMap.keySet().contains(requestedColumn)) {
+                throw new IllegalArgumentException("Column '" + requestedColumn + "' is invalid.");
+            }
+        }
+    }
 }
diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
index 473b30a..b14f422 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.Voicemails;
 import android.telephony.PhoneNumberUtils;
 import android.text.TextUtils;
 import android.text.util.Rfc822Token;
@@ -96,7 +97,7 @@
      *   600-699 Ice Cream Sandwich
      * </pre>
      */
-    static final int DATABASE_VERSION = 601;
+    static final int DATABASE_VERSION = 602;
 
     private static final String DATABASE_NAME = "contacts2.db";
     private static final String DATABASE_PRESENCE = "presence_db";
@@ -1065,7 +1066,15 @@
                 Calls.CACHED_NAME + " TEXT," +
                 Calls.CACHED_NUMBER_TYPE + " INTEGER," +
                 Calls.CACHED_NUMBER_LABEL + " TEXT," +
-                Calls.COUNTRY_ISO + " TEXT" + ");");
+                Calls.COUNTRY_ISO + " TEXT," +
+                Calls.VOICEMAIL_URI + " TEXT," +
+                Voicemails._DATA + " TEXT," +
+                Voicemails.HAS_CONTENT + " INTEGER," +
+                Voicemails.MIME_TYPE + " TEXT," +
+                Voicemails.SOURCE_DATA + " TEXT," +
+                Voicemails.SOURCE_PACKAGE + " TEXT," +
+                Voicemails.STATE + " INTEGER" +
+        ");");
 
         // Activities table
         db.execSQL("CREATE TABLE " + Tables.ACTIVITIES + " (" +
@@ -1922,6 +1931,11 @@
             oldVersion = 601;
         }
 
+        if (oldVersion < 602) {
+            upgradeToVersion602(db);
+            oldVersion = 602;
+        }
+
         if (upgradeViewsAndTriggers) {
             createContactsViews(db);
             createGroupsView(db);
@@ -2986,6 +3000,16 @@
                 "data_usage_stat (data_id, usage_type)");
     }
 
+    private void upgradeToVersion602(SQLiteDatabase db) {
+        db.execSQL("ALTER TABLE calls ADD voicemail_uri TEXT;");
+        db.execSQL("ALTER TABLE calls ADD _data TEXT;");
+        db.execSQL("ALTER TABLE calls ADD has_content INTEGER;");
+        db.execSQL("ALTER TABLE calls ADD mime_type TEXT;");
+        db.execSQL("ALTER TABLE calls ADD source_data TEXT;");
+        db.execSQL("ALTER TABLE calls ADD source_package TEXT;");
+        db.execSQL("ALTER TABLE calls ADD state INTEGER;");
+    }
+
     public String extractHandleFromEmailAddress(String email) {
         Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email);
         if (tokens.length == 0) {
diff --git a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
index 5da4950..8ac0b26 100644
--- a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
@@ -683,6 +683,7 @@
     }
 
     protected static class IdComparator implements Comparator<ContentValues> {
+        @Override
         public int compare(ContentValues o1, ContentValues o2) {
             long id1 = o1.getAsLong(ContactsContract.Data._ID);
             long id2 = o2.getAsLong(ContactsContract.Data._ID);
diff --git a/tests/src/com/android/providers/contacts/CallLogProviderTest.java b/tests/src/com/android/providers/contacts/CallLogProviderTest.java
index 392fa65..bf79e8b 100644
--- a/tests/src/com/android/providers/contacts/CallLogProviderTest.java
+++ b/tests/src/com/android/providers/contacts/CallLogProviderTest.java
@@ -18,17 +18,23 @@
 
 import com.android.internal.telephony.CallerInfo;
 import com.android.internal.telephony.Connection;
+import com.android.providers.contacts.EvenMoreAsserts;
+
+import java.util.Arrays;
+import java.util.List;
 
 import android.content.ContentProvider;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
 import android.net.Uri;
 import android.provider.CallLog;
 import android.provider.ContactsContract;
 import android.provider.CallLog.Calls;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.VoicemailContract.Voicemails;
 import android.test.suitebuilder.annotation.MediumTest;
 
 /**
@@ -42,6 +48,13 @@
  */
 @MediumTest
 public class CallLogProviderTest extends BaseContactsProvider2Test {
+    private static final String[] VOICEMAIL_PROVIDER_SPECIFIC_COLUMNS = new String[] {
+            Voicemails._DATA,
+            Voicemails.HAS_CONTENT,
+            Voicemails.MIME_TYPE,
+            Voicemails.SOURCE_PACKAGE,
+            Voicemails.SOURCE_DATA,
+            Voicemails.STATE};
 
     @Override
     protected Class<? extends ContentProvider> getProviderClass() {
@@ -59,7 +72,7 @@
         addProvider(TestCallLogProvider.class, CallLog.AUTHORITY);
     }
 
-    public void testInsert() {
+    public void testInsert_RegularCallRecord() {
         ContentValues values = new ContentValues();
         putCallValues(values);
         Uri uri = mResolver.insert(Calls.CONTENT_URI, values);
@@ -68,12 +81,19 @@
         assertSelection(uri, values, Calls._ID, ContentUris.parseId(uri));
     }
 
-    public void testUpdate() {
+    public void testInsert_VoicemailCallRecord() {
         ContentValues values = new ContentValues();
         putCallValues(values);
-        Uri uri = mResolver.insert(Calls.CONTENT_URI, values);
+        values.put(Calls.TYPE, Calls.VOICEMAIL_TYPE);
+        values.put(Calls.VOICEMAIL_URI, "content://foo/voicemail/2");
+        Uri uri  = mResolver.insert(Calls.CONTENT_URI, values);
+        assertStoredValues(uri, values);
+        assertSelection(uri, values, Calls._ID, ContentUris.parseId(uri));
+    }
 
-        values.clear();
+    public void testUpdate() {
+        Uri uri = insertCallRecord();
+        ContentValues values = new ContentValues();
         values.put(Calls.TYPE, Calls.OUTGOING_TYPE);
         values.put(Calls.NUMBER, "1-800-263-7643");
         values.put(Calls.DATE, 2000);
@@ -88,9 +108,7 @@
     }
 
     public void testDelete() {
-        ContentValues values = new ContentValues();
-        putCallValues(values);
-        Uri uri = mResolver.insert(Calls.CONTENT_URI, values);
+        Uri uri = insertCallRecord();
         try {
             mResolver.delete(uri, null, null);
             fail();
@@ -142,6 +160,54 @@
         assertStoredValues(uri, values);
     }
 
+    // Test to check that none of the voicemail provider specific fields are
+    // insertable through call_log provider.
+    public void testCannotAccessVoicemailSpecificFields_Insert() {
+        for (String voicemailColumn : VOICEMAIL_PROVIDER_SPECIFIC_COLUMNS) {
+            final ContentValues values = new ContentValues();
+            putCallValues(values);
+            values.put(voicemailColumn, "foo");
+            EvenMoreAsserts.assertThrows("Column: " + voicemailColumn,
+                    IllegalArgumentException.class, new Runnable() {
+                    @Override
+                    public void run() {
+                        mResolver.insert(Calls.CONTENT_URI, values);
+                    }
+                });
+        }
+    }
+
+    // Test to check that none of the voicemail provider specific fields are
+    // exposed through call_log provider query.
+    public void testCannotAccessVoicemailSpecificFields_Query() {
+        // Query.
+        Cursor cursor = mResolver.query(Calls.CONTENT_URI, null, null, null, null);
+        List<String> columnNames = Arrays.asList(cursor.getColumnNames());
+        assertEquals(11, columnNames.size());
+        // None of the voicemail provider specific columns should be present.
+        for (String voicemailColumn : VOICEMAIL_PROVIDER_SPECIFIC_COLUMNS) {
+            assertFalse("Unexpected column: '" + voicemailColumn + "' returned.",
+                    columnNames.contains(voicemailColumn));
+        }
+    }
+
+    // Test to check that none of the voicemail provider specific fields are
+    // updatable through call_log provider.
+    public void testCannotAccessVoicemailSpecificFields_Update() {
+        for (String voicemailColumn : VOICEMAIL_PROVIDER_SPECIFIC_COLUMNS) {
+            final Uri insertedUri = insertCallRecord();
+            final ContentValues values = new ContentValues();
+            values.put(voicemailColumn, "foo");
+            EvenMoreAsserts.assertThrows("Column: " + voicemailColumn,
+                    IllegalArgumentException.class, new Runnable() {
+                    @Override
+                    public void run() {
+                        mResolver.update(insertedUri, values, null, null);
+                    }
+                });
+        }
+    }
+
     private void putCallValues(ContentValues values) {
         values.put(Calls.TYPE, Calls.INCOMING_TYPE);
         values.put(Calls.NUMBER, "1-800-4664-411");
@@ -150,6 +216,12 @@
         values.put(Calls.NEW, 1);
     }
 
+    private Uri insertCallRecord() {
+        ContentValues values = new ContentValues();
+        putCallValues(values);
+        return mResolver.insert(Calls.CONTENT_URI, values);
+    }
+
     public static class TestCallLogProvider extends CallLogProvider {
         private static ContactsDatabaseHelper mDbHelper;
 
diff --git a/tests/src/com/android/providers/contacts/EvenMoreAsserts.java b/tests/src/com/android/providers/contacts/EvenMoreAsserts.java
new file mode 100644
index 0000000..aac8a67
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/EvenMoreAsserts.java
@@ -0,0 +1,54 @@
+/*
+ * 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 junit.framework.Assert;
+
+/**
+ * Contains additional assertion methods not found in Junit or MoreAsserts.
+ */
+public final class EvenMoreAsserts {
+    // Non instantiable.
+    private EvenMoreAsserts() { }
+
+    public static <T extends Exception> void assertThrows(Class<T> exception, Runnable r) {
+        assertThrows(null, exception, r);
+    }
+
+    public static <T extends Exception> void assertThrows(String message, Class<T> exception,
+            Runnable r) {
+        try {
+            r.run();
+           // Cannot invoke Assert.fail() here because it will be caught by the try/catch below
+           // and, if we are expecting an AssertionError or AssertionFailedError (depending on
+           // the platform), we might incorrectly identify that as a success.
+        } catch (Exception e) {
+            if (!exception.isInstance(e)) {
+                Assert.fail(appendUserMessage("Exception " + exception + " expected but " +
+                        e.getClass() +" thrown: " + e, message));
+            }
+            return;
+        }
+        Assert.fail(appendUserMessage(
+                "Exception " + exception + " expected but no exception was thrown.",
+                message));
+    }
+
+    private static String appendUserMessage(String errorMsg, String userMsg) {
+        return userMsg == null ? errorMsg : errorMsg + userMsg;
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/SqlInjectionDetectionTest.java b/tests/src/com/android/providers/contacts/SqlInjectionDetectionTest.java
index 612808f..f4b8bab 100644
--- a/tests/src/com/android/providers/contacts/SqlInjectionDetectionTest.java
+++ b/tests/src/com/android/providers/contacts/SqlInjectionDetectionTest.java
@@ -16,6 +16,8 @@
 
 package com.android.providers.contacts;
 
+import static com.android.providers.contacts.EvenMoreAsserts.assertThrows;
+
 import android.database.Cursor;
 import android.database.sqlite.SQLiteException;
 import android.net.Uri;
@@ -108,18 +110,4 @@
             }
         });
     }
-
-    private static <T extends Exception> void assertThrows(Class<T> exception, Runnable r) {
-        try {
-            r.run();
-            Assert.fail("Exception " + exception + " expected but no exception was thrown");
-        } catch (Exception e) {
-            if (exception.isInstance(e)) {
-                return;
-            }
-            Assert.fail("Exception " + exception + " expected but " + e.getClass() +" thrown: " +
-                    e);
-        }
-    }
-
 }