Create local database table + ContentProvider for filtered numbers

Bug: 23350722
Bug: 23350276

Change-Id: I070434cead43aa93026aa6b00ab886aa2947b1e6
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 9569c1d..9263009 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -22,7 +22,6 @@
         android:minSdkVersion="23"
         android:targetSdkVersion="23" />
 
-
     <uses-permission android:name="android.permission.CALL_PHONE" />
     <uses-permission android:name="android.permission.READ_CONTACTS" />
     <uses-permission android:name="android.permission.WRITE_CONTACTS" />
@@ -296,5 +295,12 @@
                 <action android:name="android.telecom.InCallService"/>
             </intent-filter>
         </service>
+
+        <provider
+            android:name=".database.FilteredNumberProvider"
+            android:authorities="com.android.dialer"
+            android:exported="false"
+            android:multiprocess="false"
+            />
     </application>
 </manifest>
diff --git a/src/com/android/dialer/database/DialerDatabaseHelper.java b/src/com/android/dialer/database/DialerDatabaseHelper.java
index eec24f5..8fcbb92 100644
--- a/src/com/android/dialer/database/DialerDatabaseHelper.java
+++ b/src/com/android/dialer/database/DialerDatabaseHelper.java
@@ -38,6 +38,7 @@
 
 import com.android.contacts.common.util.PermissionsUtil;
 import com.android.contacts.common.util.StopWatch;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
 import com.android.dialer.R;
 import com.android.dialer.dialpad.SmartDialNameMatcher;
 import com.android.dialer.dialpad.SmartDialPrefix;
@@ -60,6 +61,7 @@
 public class DialerDatabaseHelper extends SQLiteOpenHelper {
     private static final String TAG = "DialerDatabaseHelper";
     private static final boolean DEBUG = false;
+    private boolean mIsTestInstance = false;
 
     private static DialerDatabaseHelper sSingleton = null;
 
@@ -73,7 +75,7 @@
      *   0-98   KitKat
      * </pre>
      */
-    public static final int DATABASE_VERSION = 4;
+    public static final int DATABASE_VERSION = 5;
     public static final String DATABASE_NAME = "dialer.db";
 
     /**
@@ -86,6 +88,8 @@
     private static final int MAX_ENTRIES = 20;
 
     public interface Tables {
+        /** Saves a list of numbers to be blocked.*/
+        static final String FILTERED_NUMBER_TABLE = "filtered_numbers_table";
         /** Saves the necessary smart dial information of all contacts. */
         static final String SMARTDIAL_TABLE = "smartdial_table";
         /** Saves all possible prefixes to refer to a contacts.*/
@@ -334,7 +338,12 @@
      */
     @VisibleForTesting
     static DialerDatabaseHelper getNewInstanceForTest(Context context) {
-        return new DialerDatabaseHelper(context, null);
+        return new DialerDatabaseHelper(context, null, true);
+    }
+
+    protected DialerDatabaseHelper(Context context, String databaseName, boolean isTestInstance) {
+        this(context, databaseName, DATABASE_VERSION);
+        mIsTestInstance = isTestInstance;
     }
 
     protected DialerDatabaseHelper(Context context, String databaseName) {
@@ -358,36 +367,51 @@
 
     private void setupTables(SQLiteDatabase db) {
         dropTables(db);
-        db.execSQL("CREATE TABLE " + Tables.SMARTDIAL_TABLE + " (" +
-                SmartDialDbColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
-                SmartDialDbColumns.DATA_ID + " INTEGER, " +
-                SmartDialDbColumns.NUMBER + " TEXT," +
-                SmartDialDbColumns.CONTACT_ID + " INTEGER," +
-                SmartDialDbColumns.LOOKUP_KEY + " TEXT," +
-                SmartDialDbColumns.DISPLAY_NAME_PRIMARY + " TEXT, " +
-                SmartDialDbColumns.PHOTO_ID + " INTEGER, " +
-                SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME + " LONG, " +
-                SmartDialDbColumns.LAST_TIME_USED + " LONG, " +
-                SmartDialDbColumns.TIMES_USED + " INTEGER, " +
-                SmartDialDbColumns.STARRED + " INTEGER, " +
-                SmartDialDbColumns.IS_SUPER_PRIMARY + " INTEGER, " +
-                SmartDialDbColumns.IN_VISIBLE_GROUP + " INTEGER, " +
-                SmartDialDbColumns.IS_PRIMARY + " INTEGER" +
-        ");");
+        db.execSQL("CREATE TABLE " + Tables.SMARTDIAL_TABLE + " ("
+                + SmartDialDbColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+                + SmartDialDbColumns.DATA_ID + " INTEGER, "
+                + SmartDialDbColumns.NUMBER + " TEXT,"
+                + SmartDialDbColumns.CONTACT_ID + " INTEGER,"
+                + SmartDialDbColumns.LOOKUP_KEY + " TEXT,"
+                + SmartDialDbColumns.DISPLAY_NAME_PRIMARY + " TEXT, "
+                + SmartDialDbColumns.PHOTO_ID + " INTEGER, "
+                + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME + " LONG, "
+                + SmartDialDbColumns.LAST_TIME_USED + " LONG, "
+                + SmartDialDbColumns.TIMES_USED + " INTEGER, "
+                + SmartDialDbColumns.STARRED + " INTEGER, "
+                + SmartDialDbColumns.IS_SUPER_PRIMARY + " INTEGER, "
+                + SmartDialDbColumns.IN_VISIBLE_GROUP + " INTEGER, "
+                + SmartDialDbColumns.IS_PRIMARY + " INTEGER"
+                + ");");
 
-        db.execSQL("CREATE TABLE " + Tables.PREFIX_TABLE + " (" +
-                PrefixColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
-                PrefixColumns.PREFIX + " TEXT COLLATE NOCASE, " +
-                PrefixColumns.CONTACT_ID + " INTEGER" +
-                ");");
+        db.execSQL("CREATE TABLE " + Tables.PREFIX_TABLE + " ("
+                + PrefixColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+                + PrefixColumns.PREFIX + " TEXT COLLATE NOCASE, "
+                + PrefixColumns.CONTACT_ID + " INTEGER"
+                + ");");
 
-        db.execSQL("CREATE TABLE " + Tables.PROPERTIES + " (" +
-                PropertiesColumns.PROPERTY_KEY + " TEXT PRIMARY KEY, " +
-                PropertiesColumns.PROPERTY_VALUE + " TEXT " +
-                ");");
+        db.execSQL("CREATE TABLE " + Tables.PROPERTIES + " ("
+                + PropertiesColumns.PROPERTY_KEY + " TEXT PRIMARY KEY, "
+                + PropertiesColumns.PROPERTY_VALUE + " TEXT "
+                + ");");
 
+        // This will need to also be updated in setupTablesForFilteredNumberTest and onUpgrade.
+        // Hardcoded so we know on glance what columns are updated in setupTables,
+        // and to be able to guarantee the state of the DB at each upgrade step.
+        db.execSQL("CREATE TABLE " + Tables.FILTERED_NUMBER_TABLE + " ("
+                + FilteredNumberColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+                + FilteredNumberColumns.NORMALIZED_NUMBER + " TEXT,"
+                + FilteredNumberColumns.COUNTRY_ISO + " TEXT,"
+                + FilteredNumberColumns.TIMES_FILTERED + " INTEGER,"
+                + FilteredNumberColumns.LAST_TIME_FILTERED + " LONG,"
+                + FilteredNumberColumns.CREATION_TIME + " LONG,"
+                + FilteredNumberColumns.TYPE + " INTEGER,"
+                + FilteredNumberColumns.SOURCE + " INTEGER"
+                + ");");
         setProperty(db, DATABASE_VERSION_PROPERTY, String.valueOf(DATABASE_VERSION));
-        resetSmartDialLastUpdatedTime();
+        if (!mIsTestInstance) {
+            resetSmartDialLastUpdatedTime();
+        }
     }
 
     public void dropTables(SQLiteDatabase db) {
@@ -414,6 +438,20 @@
             return;
         }
 
+        if (oldVersion < 5) {
+            db.execSQL("CREATE TABLE " + Tables.FILTERED_NUMBER_TABLE + " ("
+                    + FilteredNumberColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+                    + FilteredNumberColumns.NORMALIZED_NUMBER + " TEXT,"
+                    + FilteredNumberColumns.COUNTRY_ISO + " TEXT,"
+                    + FilteredNumberColumns.TIMES_FILTERED + " INTEGER,"
+                    + FilteredNumberColumns.LAST_TIME_FILTERED + " LONG,"
+                    + FilteredNumberColumns.CREATION_TIME + " LONG,"
+                    + FilteredNumberColumns.TYPE + " INTEGER,"
+                    + FilteredNumberColumns.SOURCE + " INTEGER"
+                    + ");");
+            oldVersion = 5;
+        }
+
         if (oldVersion != DATABASE_VERSION) {
             throw new IllegalStateException(
                     "error upgrading the database to version " + DATABASE_VERSION);
diff --git a/src/com/android/dialer/database/FilteredNumberContract.java b/src/com/android/dialer/database/FilteredNumberContract.java
new file mode 100644
index 0000000..1fb2363
--- /dev/null
+++ b/src/com/android/dialer/database/FilteredNumberContract.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2015 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.dialer.database;
+
+import android.net.Uri;
+import android.provider.BaseColumns;
+
+/**
+ * <p>
+ * The contract between the filtered number provider and applications. Contains
+ * definitions for the supported URIs and columns.
+ * Currently only accessible within Dialer.
+ * </p>
+ */
+public final class FilteredNumberContract {
+
+    /** The authority for the filtered numbers provider */
+    public static final String AUTHORITY = "com.android.dialer";
+
+    /** A content:// style uri to the authority for the filtered numbers provider */
+    public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
+
+    /** The type of filtering to be applied, e.g. block the number or whitelist the number. */
+    public interface FilteredNumberTypes {
+        static final int UNDEFINED = 0;
+        /**
+         * Dialer will disconnect the call without sending the caller to voicemail.
+         */
+        static final int BLOCKED_NUMBER = 1;
+    }
+
+    /** The original source of the filtered number, e.g. the user manually added it. */
+    public interface FilteredNumberSources {
+        static final int UNDEFINED = 0;
+        /**
+         * The user manually added this number through Dialer (e.g. from the call log or InCallUI).
+         */
+        static final int USER = 1;
+    }
+
+    public interface FilteredNumberColumns {
+        // TYPE: INTEGER
+        static final String _ID = "id";
+        /**
+         * Represents the number to be filtered, normalized to compare phone numbers for equality.
+         *
+         * TYPE: TEXT
+         */
+        static final String NORMALIZED_NUMBER = "normalized_number";
+        /**
+         * The country code representing the country detected when
+         * the phone number was added to the database.
+         * Most numbers don't have the country code, so a best guess is provided by
+         * the country detector system. The country iso is also needed in order to format
+         * phone numbers correctly.
+         *
+         * TYPE: TEXT
+         */
+        static final String COUNTRY_ISO = "country_iso";
+        /**
+         * The number of times the number has been filtered by Dialer.
+         * When this number is incremented, LAST_TIME_FILTERED should also be updated to
+         * the current time.
+         *
+         * TYPE: INTEGER
+         */
+        static final String TIMES_FILTERED = "times_filtered";
+        /**
+         * Set to the current time when the phone number is filtered.
+         * When this is updated, TIMES_FILTERED should also be incremented.
+         *
+         * TYPE: LONG
+         */
+        static final String LAST_TIME_FILTERED = "last_time_filtered";
+        // TYPE: LONG
+        static final String CREATION_TIME = "creation_time";
+        /**
+         * Indicates the type of filtering to be applied.
+         *
+         * TYPE: INTEGER
+         * See {@link FilteredNumberTypes}
+         */
+        static final String TYPE = "type";
+        /**
+         * Integer representing the original source of the filtered number.
+         *
+         * TYPE: INTEGER
+         * See {@link FilteredNumberSources}
+         */
+        static final String SOURCE = "source";
+    }
+
+    /**
+     * <p>
+     * Constants for the table of filtered numbers.
+     * </p>
+     * <h3>Operations</h3>
+     * <dl>
+     * <dt><b>Insert</b></dt>
+     * <dd>Required fields: NORMALIZED_NUMBER, TYPE, SOURCE.
+     * A default value will be used for the other fields if left null.</dd>
+     * <dt><b>Update</b></dt>
+     * <dt><b>Delete</b></dt>
+     * <dt><b>Query</b></dt>
+     * <dd>{@link #CONTENT_URI} can be used for any query, append an ID to
+     * retrieve a specific filtered number entry.</dd>
+     * </dl>
+     */
+    public static class FilteredNumber implements BaseColumns {
+
+        public static final String FILTERED_NUMBERS_TABLE = "filtered_numbers_table";
+
+        public static final Uri CONTENT_URI = Uri.withAppendedPath(
+                AUTHORITY_URI,
+                FILTERED_NUMBERS_TABLE);
+
+        /**
+         * This utility class cannot be instantiated.
+         */
+        private FilteredNumber () {}
+
+        /**
+         * The MIME type of {@link #CONTENT_URI} providing a directory of
+         * filtered numbers.
+         */
+        public static final String CONTENT_TYPE = "vnd.android.cursor.dir/filtered_numbers_table";
+
+        /**
+         * The MIME type of a {@link #CONTENT_URI} single filtered number.
+         */
+        public static final String CONTENT_ITEM_TYPE =
+                "vnd.android.cursor.item/filtered_numbers_table";
+
+    }
+}
diff --git a/src/com/android/dialer/database/FilteredNumberProvider.java b/src/com/android/dialer/database/FilteredNumberProvider.java
new file mode 100644
index 0000000..2bacd89
--- /dev/null
+++ b/src/com/android/dialer/database/FilteredNumberProvider.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2015 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.dialer.database;
+
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.os.Binder;
+import android.text.TextUtils;
+import android.text.format.Time;
+import android.util.Log;
+
+import com.android.contacts.common.GeoUtil;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
+import com.android.dialerbind.DatabaseHelperManager;
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.Arrays;
+
+/**
+ * Filtered number content provider.
+ */
+public class FilteredNumberProvider extends ContentProvider {
+
+    private static String TAG = FilteredNumberProvider.class.getSimpleName();
+
+    private DialerDatabaseHelper mDialerDatabaseHelper;
+
+    private static final int FILTERED_NUMBERS_TABLE = 1;
+    private static final int FILTERED_NUMBERS_TABLE_ID = 2;
+
+    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+    static {
+        sUriMatcher.addURI(FilteredNumberContract.AUTHORITY,
+                FilteredNumberContract.FilteredNumber.FILTERED_NUMBERS_TABLE,
+                FILTERED_NUMBERS_TABLE);
+        sUriMatcher.addURI(FilteredNumberContract.AUTHORITY,
+                FilteredNumberContract.FilteredNumber.FILTERED_NUMBERS_TABLE + "/#",
+                FILTERED_NUMBERS_TABLE_ID);
+    }
+
+    @Override
+    public boolean onCreate() {
+        mDialerDatabaseHelper = getDatabaseHelper(getContext());
+        if (mDialerDatabaseHelper == null) {
+            return false;
+        }
+        return true;
+    }
+
+    @VisibleForTesting
+    protected DialerDatabaseHelper getDatabaseHelper(Context context) {
+        return DatabaseHelperManager.getDatabaseHelper(context);
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+                        String sortOrder) {
+        Log.d(TAG, "query: uri=" + uri + "  projection=" + Arrays.toString(projection) +
+                "  selection=[" + selection + "]  args=" + Arrays.toString(selectionArgs) +
+                "  order=[" + sortOrder + "] CPID=" + Binder.getCallingPid());
+
+        final SQLiteDatabase db = mDialerDatabaseHelper.getReadableDatabase();
+        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+        qb.setTables(DialerDatabaseHelper.Tables.FILTERED_NUMBER_TABLE);
+        final int match = sUriMatcher.match(uri);
+        switch (match) {
+            case FILTERED_NUMBERS_TABLE:
+                break;
+            case FILTERED_NUMBERS_TABLE_ID:
+                qb.appendWhere(FilteredNumberColumns._ID + "=" + ContentUris.parseId(uri));
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown uri: " + uri);
+        }
+        final Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, null);
+        if (c != null) {
+            c.setNotificationUri(getContext().getContentResolver(),
+                    FilteredNumberContract.FilteredNumber.CONTENT_URI);
+        } else {
+            Log.d(TAG, "CURSOR WAS NULL");
+        }
+        return c;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return FilteredNumberContract.FilteredNumber.CONTENT_ITEM_TYPE;
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        SQLiteDatabase db = mDialerDatabaseHelper.getWritableDatabase();
+        setDefaultValues(values);
+        long id = db.insert(DialerDatabaseHelper.Tables.FILTERED_NUMBER_TABLE, null, values);
+        if (id < 0) {
+            return null;
+        }
+        notifyChange(uri);
+        return ContentUris.withAppendedId(uri, id);
+    }
+
+    @VisibleForTesting
+    protected long getCurrentTimeMs() {
+        Time timeNow = new Time();
+        timeNow.setToNow();
+        return timeNow.toMillis(false);
+    }
+
+    private void setDefaultValues(ContentValues values) {
+        if (values.getAsString(FilteredNumberColumns.COUNTRY_ISO) == null) {
+            values.put(FilteredNumberColumns.COUNTRY_ISO,
+                    GeoUtil.getCurrentCountryIso(getContext()));
+        }
+        if (values.getAsInteger(FilteredNumberColumns.TIMES_FILTERED) == null) {
+            values.put(FilteredNumberContract.FilteredNumberColumns.TIMES_FILTERED, 0);
+        }
+        if (values.getAsLong(FilteredNumberColumns.CREATION_TIME) == null) {
+            values.put(FilteredNumberColumns.CREATION_TIME, getCurrentTimeMs());
+        }
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        SQLiteDatabase db = mDialerDatabaseHelper.getWritableDatabase();
+        final int match = sUriMatcher.match(uri);
+        switch (match) {
+            case FILTERED_NUMBERS_TABLE:
+                break;
+            case FILTERED_NUMBERS_TABLE_ID:
+                selection = getSelectionWithId(selection, ContentUris.parseId(uri));
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown uri: " + uri);
+        }
+        int rows = db.delete(DialerDatabaseHelper.Tables.FILTERED_NUMBER_TABLE,
+                selection,
+                selectionArgs);
+        if (rows > 0) {
+            notifyChange(uri);
+        }
+        return rows;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        SQLiteDatabase db = mDialerDatabaseHelper.getWritableDatabase();
+        final int match = sUriMatcher.match(uri);
+        switch (match) {
+            case FILTERED_NUMBERS_TABLE:
+                break;
+            case FILTERED_NUMBERS_TABLE_ID:
+                selection = getSelectionWithId(selection, ContentUris.parseId(uri));
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown uri: " + uri);
+        }
+        int rows = db.update(DialerDatabaseHelper.Tables.FILTERED_NUMBER_TABLE,
+                values,
+                selection,
+                selectionArgs);
+        if (rows > 0 ) {
+            notifyChange(uri);
+        }
+        return rows;
+    }
+
+    private String getSelectionWithId(String selection, long id) {
+        if (TextUtils.isEmpty(selection)) {
+            return FilteredNumberContract.FilteredNumberColumns._ID + "=" + id;
+        } else {
+            return selection + "AND " + FilteredNumberContract.FilteredNumberColumns._ID + "=" + id;
+        }
+    }
+
+    private void notifyChange(Uri uri) {
+        getContext().getContentResolver().notifyChange(uri, null);
+    }
+}
diff --git a/tests/src/com/android/dialer/database/FilteredNumberProviderTest.java b/tests/src/com/android/dialer/database/FilteredNumberProviderTest.java
new file mode 100644
index 0000000..94bf6e6
--- /dev/null
+++ b/tests/src/com/android/dialer/database/FilteredNumberProviderTest.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2015 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.dialer.database;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.net.Uri;
+import android.test.ProviderTestCase2;
+
+public class FilteredNumberProviderTest extends
+        ProviderTestCase2<FilteredNumberProviderTest.TestFilteredNumberProvider> {
+    private ContentResolver mResolver;
+
+    private static final String TIME_ZONE_AMERICA_LOS_ANGELES = "America/Los_Angeles";
+    private static final String DEFAULT_TIMEZONE = TIME_ZONE_AMERICA_LOS_ANGELES;
+    private static final String DEFAULT_COUNTRY_ISO = "US";
+    private static final String TEST_NUMBER = "+1234567890";
+    private static final long TEST_TIME = 1439936706;
+
+    public FilteredNumberProviderTest () {
+        super(TestFilteredNumberProvider.class, FilteredNumberContract.AUTHORITY);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mResolver = getMockContentResolver();
+    }
+
+    public void testInsert() {
+        // Insert row
+        Uri uri = mResolver.insert(
+                FilteredNumberContract.FilteredNumber.CONTENT_URI,
+                getTestValues(null));
+        assertNotNull(uri);
+        long id = ContentUris.parseId(uri);
+        assertTrue(id > 0);
+    }
+
+    public void testQuery() {
+        Cursor cursor = mResolver.query(
+                FilteredNumberContract.FilteredNumber.CONTENT_URI, null, null, null, null);
+        assertNotNull(cursor);
+        assertEquals(cursor.getCount(), 0);
+        cursor.close();
+    }
+
+    public void testInsertAndQuery() {
+        // Insert row
+        ContentValues testValues = getTestValues(null);
+        Uri uri = mResolver.insert(FilteredNumberContract.FilteredNumber.CONTENT_URI, testValues);
+
+        // Query
+        Cursor cursor = mResolver.query(uri, null, null, null, null);
+        assertNotNull(cursor);
+        assertEquals(cursor.getCount(), 1);
+
+        cursor.moveToFirst();
+        assertCursorValues(cursor, testValues);
+        cursor.close();
+    }
+
+    public void testIllegalUri() {
+        try {
+            mResolver.query(
+                    Uri.withAppendedPath(
+                            FilteredNumberContract.FilteredNumber.CONTENT_URI,
+                            "ILLEGAL"), null, null, null, null);
+            fail("Expecting exception but none was thrown.");
+        } catch (IllegalArgumentException e) {
+        }
+    }
+
+    public void testQueryWithId() {
+        // Insert row
+        ContentValues testValues = getTestValues(null);
+        Uri uri = mResolver.insert(FilteredNumberContract.FilteredNumber.CONTENT_URI, testValues);
+        long id = ContentUris.parseId(uri);
+
+        // Query
+        Cursor cursor = mResolver.query(
+                ContentUris.withAppendedId(
+                        FilteredNumberContract.FilteredNumber.CONTENT_URI,
+                        id), null, null, null, null);
+        assertNotNull(cursor);
+        assertEquals(cursor.getCount(), 1);
+
+        cursor.moveToFirst();
+        assertCursorValues(cursor, testValues);
+        cursor.close();
+    }
+
+    public void testDelete() {
+        // Insert row
+        Uri uri = mResolver.insert(
+                FilteredNumberContract.FilteredNumber.CONTENT_URI,
+                getTestValues(null));
+        long id = ContentUris.parseId(uri);
+
+        // Delete row
+        int rows = mResolver.delete(
+                FilteredNumberContract.FilteredNumber.CONTENT_URI,
+                "id = ?",
+                new String[]{Long.toString(id)});
+        assertEquals(rows, 1);
+
+        // Query
+        Cursor cursor =  mResolver.query(uri, null, null, null, null);
+        assertNotNull(cursor);
+        assertEquals(cursor.getCount(), 0);
+        cursor.close();
+    }
+
+    public void testUpdate() {
+        // Insert row
+        Uri uri = mResolver.insert(
+                FilteredNumberContract.FilteredNumber.CONTENT_URI,
+                getTestValues(null));
+
+        // Update row
+        ContentValues v = new ContentValues();
+        v.put(FilteredNumberContract.FilteredNumberColumns.TIMES_FILTERED, 3);
+        v.put(FilteredNumberContract.FilteredNumberColumns.LAST_TIME_FILTERED, TEST_TIME);
+        int rows = mResolver.update(FilteredNumberContract.FilteredNumber.CONTENT_URI, v,
+                FilteredNumberContract.FilteredNumberColumns.NORMALIZED_NUMBER + " = ?",
+                new String[]{TEST_NUMBER});
+        assertEquals(rows, 1);
+
+        ContentValues expected = getTestValues(TEST_TIME);
+        expected.put(FilteredNumberContract.FilteredNumberColumns.TIMES_FILTERED, 3);
+        expected.put(FilteredNumberContract.FilteredNumberColumns.LAST_TIME_FILTERED, TEST_TIME);
+
+        // Re-query
+        Cursor cursor =  mResolver.query(uri, null, null, null, null);
+        assertNotNull(cursor);
+        assertEquals(cursor.getCount(), 1);
+        cursor.moveToFirst();
+        assertCursorValues(cursor, expected);
+        cursor.close();
+    }
+
+    public void testInsertDefaultValues() {
+        // Insert row
+        ContentValues v = getTestValues(null);
+        Uri uri = mResolver.insert(FilteredNumberContract.FilteredNumber.CONTENT_URI, v);
+        assertNotNull(uri);
+        long id = ContentUris.parseId(uri);
+        assertTrue(id > 0);
+
+        // Query
+        Cursor cursor =  mResolver.query(uri, null, null, null, null);
+        assertNotNull(cursor);
+        assertEquals(cursor.getCount(), 1);
+
+        int creationTimeIndex =
+                cursor.getColumnIndex(FilteredNumberContract.FilteredNumberColumns.CREATION_TIME);
+        cursor.moveToFirst();
+        assertEquals(cursor.getLong(creationTimeIndex), TEST_TIME);
+        cursor.close();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        getProvider().closeDb();
+        super.tearDown();
+    }
+
+    private ContentValues getTestValues(Long timeNow) {
+        ContentValues v = new ContentValues();
+        v.putNull(FilteredNumberContract.FilteredNumberColumns._ID);
+        v.put(FilteredNumberContract.FilteredNumberColumns.NORMALIZED_NUMBER, TEST_NUMBER);
+        v.put(FilteredNumberContract.FilteredNumberColumns.COUNTRY_ISO, DEFAULT_COUNTRY_ISO);
+        v.put(FilteredNumberContract.FilteredNumberColumns.TIMES_FILTERED, 0);
+        v.putNull(FilteredNumberContract.FilteredNumberColumns.LAST_TIME_FILTERED);
+        v.put(FilteredNumberContract.FilteredNumberColumns.CREATION_TIME, timeNow);
+        v.put(FilteredNumberContract.FilteredNumberColumns.SOURCE, 1);
+        v.put(FilteredNumberContract.FilteredNumberColumns.TYPE, 1);
+        return v;
+    }
+
+    private void assertCursorValues(Cursor cursor, ContentValues expectedValues) {
+        ContentValues v = new ContentValues();
+        DatabaseUtils.cursorRowToContentValues(cursor, v);
+        v.remove(FilteredNumberContract.FilteredNumberColumns._ID);
+        expectedValues.remove(FilteredNumberContract.FilteredNumberColumns._ID);
+        assertEquals(v.toString(), expectedValues.toString());
+    }
+
+    public static class TestFilteredNumberProvider extends FilteredNumberProvider {
+        private DialerDatabaseHelper mDialerDatabaseHelper;
+
+        @Override
+        protected DialerDatabaseHelper getDatabaseHelper(Context context) {
+            if (mDialerDatabaseHelper == null) {
+                mDialerDatabaseHelper = DialerDatabaseHelper.getNewInstanceForTest(context);
+            }
+            return mDialerDatabaseHelper;
+        }
+
+        protected void closeDb() {
+            mDialerDatabaseHelper.close();
+        }
+
+        @Override
+        protected long getCurrentTimeMs() {
+            return TEST_TIME;
+        }
+    }
+}