Bug 1030277 - Create content provider for search terms. r=rnewman
authorEric Edens <eedens@mozilla.com>
Tue, 01 Jul 2014 08:58:24 -0700
changeset 193277 a19387ced68e283a62090a0fa3e7bfcd13c647e8
parent 193276 e9a7523b1abd61e3a1785dc865cc905e52d19e82
child 193278 81ba79c9c36e3bc49eee5a5ce3ba297b52bf78b6
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersrnewman
bugs1030277
milestone33.0a1
Bug 1030277 - Create content provider for search terms. r=rnewman
mobile/android/base/db/BrowserContract.java
mobile/android/base/db/BrowserDatabaseHelper.java
mobile/android/base/db/SearchHistoryProvider.java
mobile/android/base/moz.build
mobile/android/base/tests/robocop.ini
mobile/android/base/tests/testSearchHistoryProvider.java
--- a/mobile/android/base/db/BrowserContract.java
+++ b/mobile/android/base/db/BrowserContract.java
@@ -28,16 +28,19 @@ public class BrowserContract {
     public static final Uri HOME_AUTHORITY_URI = Uri.parse("content://" + HOME_AUTHORITY);
 
     public static final String PROFILES_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".profiles";
     public static final Uri PROFILES_AUTHORITY_URI = Uri.parse("content://" + PROFILES_AUTHORITY);
 
     public static final String READING_LIST_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.readinglist";
     public static final Uri READING_LIST_AUTHORITY_URI = Uri.parse("content://" + READING_LIST_AUTHORITY);
 
+    public static final String SEARCH_HISTORY_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.searchhistory";
+    public static final Uri SEARCH_HISTORY_AUTHORITY_URI = Uri.parse("content://" + SEARCH_HISTORY_AUTHORITY);
+
     public static final String PARAM_PROFILE = "profile";
     public static final String PARAM_PROFILE_PATH = "profilePath";
     public static final String PARAM_LIMIT = "limit";
     public static final String PARAM_IS_SYNC = "sync";
     public static final String PARAM_SHOW_DELETED = "show_deleted";
     public static final String PARAM_IS_TEST = "test";
     public static final String PARAM_INSERT_IF_NEEDED = "insert_if_needed";
     public static final String PARAM_INCREMENT_VISITS = "increment_visits";
@@ -429,14 +432,25 @@ public class BrowserContract {
         public static final String BOOKMARK_ID = "bookmark_id";
         public static final String HISTORY_ID = "history_id";
         public static final String DISPLAY = "display";
 
         public static final String TYPE = "type";
     }
 
     @RobocopTarget
+    public static final class SearchHistory implements CommonColumns, HistoryColumns {
+        private SearchHistory() {}
+
+        public static final String CONTENT_TYPE = "vnd.android.cursor.dir/searchhistory";
+        public static final String QUERY = "query";
+        public static final String TABLE_NAME = "searchhistory";
+
+        public static final Uri CONTENT_URI = Uri.withAppendedPath(SEARCH_HISTORY_AUTHORITY_URI, "searchhistory");
+    }
+
+    @RobocopTarget
     public static final class SuggestedSites implements CommonColumns, URLColumns {
         private SuggestedSites() {}
 
         public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "suggestedsites");
     }
 }
--- a/mobile/android/base/db/BrowserDatabaseHelper.java
+++ b/mobile/android/base/db/BrowserDatabaseHelper.java
@@ -11,16 +11,17 @@ import java.util.List;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract.Bookmarks;
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserContract.FaviconColumns;
 import org.mozilla.gecko.db.BrowserContract.Favicons;
 import org.mozilla.gecko.db.BrowserContract.History;
 import org.mozilla.gecko.db.BrowserContract.Obsolete;
 import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
+import org.mozilla.gecko.db.BrowserContract.SearchHistory;
 import org.mozilla.gecko.db.BrowserContract.Thumbnails;
 import org.mozilla.gecko.sync.Utils;
 
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
 import android.database.DatabaseUtils;
 import android.database.SQLException;
@@ -29,17 +30,17 @@ import android.database.sqlite.SQLiteOpe
 import android.net.Uri;
 import android.os.Build;
 import android.util.Log;
 
 
 final class BrowserDatabaseHelper extends SQLiteOpenHelper {
 
     private static final String LOGTAG = "GeckoBrowserDBHelper";
-    public static final int DATABASE_VERSION = 19;
+    public static final int DATABASE_VERSION = 20;
     public static final String DATABASE_NAME = "browser.db";
 
     final protected Context mContext;
 
     static final String TABLE_BOOKMARKS = Bookmarks.TABLE_NAME;
     static final String TABLE_HISTORY = History.TABLE_NAME;
     static final String TABLE_FAVICONS = Favicons.TABLE_NAME;
     static final String TABLE_THUMBNAILS = Thumbnails.TABLE_NAME;
@@ -753,16 +754,30 @@ final class BrowserDatabaseHelper extend
         createHistoryWithFaviconsView(db);
         createCombinedViewOn19(db);
 
         createOrUpdateSpecialFolder(db, Bookmarks.PLACES_FOLDER_GUID,
             R.string.bookmarks_folder_places, 0);
 
         createOrUpdateAllSpecialFolders(db);
         createReadingListTable(db);
+        createSearchHistoryTable(db);
+    }
+
+    private void createSearchHistoryTable(SQLiteDatabase db) {
+        debug("Creating " + SearchHistory.TABLE_NAME + " table");
+
+        db.execSQL("CREATE TABLE " + SearchHistory.TABLE_NAME + "(" +
+                    SearchHistory._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
+                    SearchHistory.QUERY + " TEXT UNIQUE NOT NULL, " +
+                    SearchHistory.DATE_LAST_VISITED + " INTEGER, " +
+                    SearchHistory.VISITS + " INTEGER ) ");
+
+        db.execSQL("CREATE INDEX idx_search_history_last_visited ON " +
+                SearchHistory.TABLE_NAME + "(" + SearchHistory.DATE_LAST_VISITED + ")");
     }
 
     private void createReadingListTable(SQLiteDatabase db) {
         debug("Creating " + TABLE_READING_LIST + " table");
 
         db.execSQL("CREATE TABLE " + TABLE_READING_LIST + "(" +
                     ReadingListItems._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
                     ReadingListItems.URL + " TEXT NOT NULL, " +
@@ -1395,16 +1410,20 @@ final class BrowserDatabaseHelper extend
         db.execSQL("DELETE FROM " + TABLE_HISTORY + " WHERE " + History.URL + " IS NULL");
 
         // Similar for bookmark types. Replaces logic from the combined view, also shouldn't happen.
         db.execSQL("UPDATE " + TABLE_BOOKMARKS + " SET " +
                    Bookmarks.TYPE + " = " + Bookmarks.TYPE_BOOKMARK +
                    " WHERE " + Bookmarks.TYPE + " IS NULL");
     }
 
+    private void upgradeDatabaseFrom19to20(SQLiteDatabase db) {
+        createSearchHistoryTable(db);
+    }
+
     private void createV19CombinedView(SQLiteDatabase db) {
         db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED);
         db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED_WITH_FAVICONS);
 
         createCombinedViewOn19(db);
     }
 
     @Override
@@ -1482,16 +1501,20 @@ final class BrowserDatabaseHelper extend
 
                 case 18:
                     upgradeDatabaseFrom17to18(db);
                     break;
 
                 case 19:
                     upgradeDatabaseFrom18to19(db);
                     break;
+
+                case 20:
+                    upgradeDatabaseFrom19to20(db);
+                    break;
             }
         }
 
         // If an upgrade after 12->13 fails, the entire upgrade is rolled
         // back, but we can't undo the deletion of favicon_urls.db if we
         // delete this in step 13; therefore, we wait until all steps are
         // complete before removing it.
         if (oldVersion < 13 && newVersion >= 13
@@ -1595,8 +1618,9 @@ final class BrowserDatabaseHelper extend
             } else {
                 bookmark.put(Bookmarks.TYPE, Bookmarks.TYPE_FOLDER);
             }
 
             bookmark.remove("folder");
         }
     }
 }
+
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/SearchHistoryProvider.java
@@ -0,0 +1,124 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.db;
+
+import org.mozilla.gecko.db.BrowserContract.SearchHistory;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
+
+public class SearchHistoryProvider extends SharedBrowserDatabaseProvider {
+    private static final String LOG_TAG = "GeckoSearchProvider";
+    private static final boolean DEBUG_ENABLED = false;
+
+    /**
+     * Collapse whitespace.
+     */
+    private String stripWhitespace(String query) {
+        if (TextUtils.isEmpty(query)) {
+            return "";
+        }
+
+        // Collapse whitespace
+        return query.trim().replaceAll("\\s+", " ");
+    }
+
+
+    @Override
+    public Uri insertInTransaction(Uri uri, ContentValues cv) {
+        final String query = stripWhitespace(cv.getAsString(SearchHistory.QUERY));
+
+        // We don't support inserting empty search queries.
+        if (TextUtils.isEmpty(query)) {
+            return null;
+        }
+
+        final SQLiteDatabase db = getWritableDatabase(uri);
+        long id = -1;
+
+        /*
+         * Attempt to insert the query. The catch block handles the case when
+         * the query already exists in the DB.
+         */
+        try {
+            cv.put(SearchHistory.QUERY, query);
+            cv.put(SearchHistory.VISITS, 1);
+            cv.put(SearchHistory.DATE_LAST_VISITED, System.currentTimeMillis());
+
+            id = db.insertOrThrow(SearchHistory.TABLE_NAME, null, cv);
+
+            if (id > 0) {
+                return ContentUris.withAppendedId(uri, id);
+            }
+        } catch (SQLException e) {
+            // This happens when the column already exists for this term.
+            if (DEBUG_ENABLED) {
+                Log.w(LOG_TAG, String.format("Query `%s` already in db", query));
+            }
+        }
+
+        /*
+         * Increment the VISITS counter and update the DATE_LAST_VISITED.
+         */
+        final String sql = "UPDATE " + SearchHistory.TABLE_NAME + " SET " +
+                            SearchHistory.VISITS + " = " + SearchHistory.VISITS + " + 1, " +
+                            SearchHistory.DATE_LAST_VISITED + " = " + System.currentTimeMillis() +
+                            " WHERE " + SearchHistory.QUERY + " = ?";
+
+        final Cursor c = db.rawQuery(sql, new String[] { query });
+
+        try {
+            if (c.getCount() > 1) {
+                // There is a UNIQUE constraint on the QUERY column,
+                // so there should only be one match.
+                return null;
+            }
+            if (c.moveToFirst()) {
+                return ContentUris.withAppendedId(uri, c.getInt(c.getColumnIndex(SearchHistory._ID)));
+            }
+        } finally {
+            c.close();
+        }
+
+        return null;
+    }
+
+    @Override
+    public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
+        return getWritableDatabase(uri).delete(SearchHistory.TABLE_NAME,
+                                               selection, selectionArgs);
+    }
+
+    /**
+     * Since we are managing counts and the full-text db, an update
+     * could mangle the internal state. So we disable it.
+     */
+    @Override
+    public int updateInTransaction(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs) {
+        throw new UnsupportedOperationException("This content provider does not support updating items");
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection,
+            String[] selectionArgs, String sortOrder) {
+        String groupBy = null;
+        String having = null;
+        return getReadableDatabase(uri).query(SearchHistory.TABLE_NAME, projection,
+                                              selection, selectionArgs,
+                                              groupBy, having, sortOrder);
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return SearchHistory.CONTENT_TYPE;
+    }
+}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -145,16 +145,17 @@ gbjar.sources += [
     'db/DBUtils.java',
     'db/FormHistoryProvider.java',
     'db/HomeProvider.java',
     'db/LocalBrowserDB.java',
     'db/PasswordsProvider.java',
     'db/PerProfileDatabaseProvider.java',
     'db/PerProfileDatabases.java',
     'db/ReadingListProvider.java',
+    'db/SearchHistoryProvider.java',
     'db/SharedBrowserDatabaseProvider.java',
     'db/SQLiteBridgeContentProvider.java',
     'db/SuggestedSites.java',
     'db/TabsProvider.java',
     'db/TopSitesCursorWrapper.java',
     'distribution/Distribution.java',
     'distribution/ReferrerDescriptor.java',
     'distribution/ReferrerReceiver.java',
--- a/mobile/android/base/tests/robocop.ini
+++ b/mobile/android/base/tests/robocop.ini
@@ -68,16 +68,17 @@ skip-if = processor == "x86"
 [testPictureLinkContextMenu]
 [testPrefsObserver]
 [testPrivateBrowsing]
 [testPromptGridInput]
 # disabled on x86 only; bug 957185
 skip-if = processor == "x86"
 # [testReaderMode] # see bug 913254, 936224
 [testReadingListProvider]
+[testSearchHistoryProvider]
 [testSearchSuggestions]
 # disabled on x86; bug 907768
 skip-if = processor == "x86"
 [testSessionOOMSave]
 # disabled on x86 and 2.3; bug 945395
 skip-if = android_version == "10" || processor == "x86"
 [testSessionOOMRestore]
 # disabled on Android 2.3; bug 979600
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tests/testSearchHistoryProvider.java
@@ -0,0 +1,302 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import java.util.concurrent.Callable;
+
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserContract.SearchHistory;
+import org.mozilla.gecko.db.SearchHistoryProvider;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+
+public class testSearchHistoryProvider extends ContentProviderTest {
+
+    // Translations of "United Kingdom" in several different languages
+    private static final String[] testStrings = {"An Ríocht Aontaithe", // Irish
+            "Angli", // Albanian
+            "Britanniarum Regnum", // Latin
+            "Britio", // Esperanto
+            "Büyük Britanya", // Turkish
+            "Egyesült Királyság", // Hungarian
+            "Erresuma Batua", // Basque
+            "Inggris Raya", // Indonesian
+            "Ir-Renju Unit", // Maltese
+            "Iso-Britannia", // Finnish
+            "Jungtinė Karalystė", // Lithuanian
+            "Lielbritānija", // Latvian
+            "Regatul Unit", // Romanian
+            "Regne Unit", // Catalan, Valencian
+            "Regno Unito", // Italian
+            "Royaume-Uni", // French
+            "Spojené království", // Czech
+            "Spojené kráľovstvo", // Slovak
+            "Storbritannia", // Norwegian
+            "Storbritannien", // Danish
+            "Suurbritannia", // Estonian
+            "Ujedinjeno Kraljevstvo", // Bosnian
+            "United Alaeze", // Igbo
+            "United Kingdom", // English
+            "Vereinigtes Königreich", // German
+            "Verenigd Koninkrijk", // Dutch
+            "Verenigde Koninkryk", // Afrikaans
+            "Vương quốc Anh", // Vietnamese
+            "Wayòm Ini", // Haitian, Haitian Creole
+            "Y Deyrnas Unedig", // Welsh
+            "Združeno kraljestvo", // Slovene
+            "Zjednoczone Królestwo", // Polish
+            "Ηνωμένο Βασίλειο", // Greek (modern)
+            "Великобритания", // Russian
+            "Нэгдсэн Вант Улс", // Mongolian
+            "Обединетото Кралство", // Macedonian
+            "Уједињено Краљевство", // Serbian
+            "Միացյալ Թագավորություն", // Armenian
+            "בריטניה", // Hebrew (modern)
+            "פֿאַראייניקטע מלכות", // Yiddish
+            "المملكة المتحدة", // Arabic
+            "برطانیہ", // Urdu
+            "پادشاهی متحده", // Persian (Farsi)
+            "यूनाइटेड किंगडम", // Hindi
+            "संयुक्त राज्य", // Nepali
+            "যুক্তরাজ্য", // Bengali, Bangla
+            "યુનાઇટેડ કિંગડમ", // Gujarati
+            "ஐக்கிய ராஜ்யம்", // Tamil
+            "สหราชอาณาจักร", // Thai
+            "ສະ​ຫະ​ປະ​ຊາ​ຊະ​ອາ​ນາ​ຈັກ", // Lao
+            "გაერთიანებული სამეფო", // Georgian
+            "イギリス", // Japanese
+            "联合王国" // Chinese
+    };
+
+
+    private static final String DB_NAME = "searchhistory.db";
+
+    /**
+     * Boilerplate alert.
+     * <p/>
+     * Make sure this method is present and that it returns a new
+     * instance of your class.
+     */
+    private static Callable<ContentProvider> sProviderFactory =
+            new Callable<ContentProvider>() {
+                @Override
+                public ContentProvider call() {
+                    return new SearchHistoryProvider();
+                }
+            };
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp(sProviderFactory, BrowserContract.SEARCH_HISTORY_AUTHORITY, DB_NAME);
+        mTests.add(new TestInsert());
+        mTests.add(new TestUnicodeQuery());
+        mTests.add(new TestTimestamp());
+        mTests.add(new TestDelete());
+        mTests.add(new TestIncrement());
+    }
+
+    public void testSearchHistory() throws Exception {
+        for (Runnable test : mTests) {
+            String testName = test.getClass().getSimpleName();
+            setTestName(testName);
+            mAsserter.dumpLog(
+                    "testBrowserProvider: Database empty - Starting " + testName + ".");
+            // Clear the db
+            mProvider.delete(SearchHistory.CONTENT_URI, null, null);
+            test.run();
+        }
+    }
+
+    /**
+     * Verify that we can insert values into the DB, including unicode.
+     */
+    private class TestInsert extends TestCase {
+        @Override
+        public void test() throws Exception {
+            ContentValues cv;
+            for (int i = 0; i < testStrings.length; i++) {
+                cv = new ContentValues();
+                cv.put(SearchHistory.QUERY, testStrings[i]);
+                mProvider.insert(SearchHistory.CONTENT_URI, cv);
+            }
+
+            final Cursor c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            try {
+                mAsserter.is(c.getCount(), testStrings.length,
+                             "Should have one row for each insert");
+            } finally {
+                c.close();
+            }
+        }
+    }
+
+    /**
+     * Verify that we can insert values into the DB, including unicode.
+     */
+    private class TestUnicodeQuery extends TestCase {
+        @Override
+        public void test() throws Exception {
+            final String selection = SearchHistory.QUERY + " = ?";
+
+            for (int i = 0; i < testStrings.length; i++) {
+                final ContentValues cv = new ContentValues();
+                cv.put(SearchHistory.QUERY, testStrings[i]);
+                mProvider.insert(SearchHistory.CONTENT_URI, cv);
+
+                final Cursor c = mProvider.query(SearchHistory.CONTENT_URI, null, selection,
+                                                 new String[]{ testStrings[i] }, null);
+                try {
+                    mAsserter.is(c.getCount(), 1,
+                                 "Should have one row for insert of " + testStrings[i]);
+                } finally {
+                    c.close();
+                }
+            }
+        }
+    }
+
+    /**
+     * Verify that timestamps are updated on insert.
+     */
+    private class TestTimestamp extends TestCase {
+        @Override
+        public void test() throws Exception {
+            String insertedTerm = "Courtside Seats";
+            long insertStart;
+            long insertFinish;
+            long t1Db;
+            long t2Db;
+
+            ContentValues cv = new ContentValues();
+            cv.put(SearchHistory.QUERY, insertedTerm);
+
+            // First check that the DB has a value that is close to the
+            // system time.
+            insertStart = System.currentTimeMillis();
+            mProvider.insert(SearchHistory.CONTENT_URI, cv);
+            insertFinish = System.currentTimeMillis();
+
+            final Cursor c1 = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            try {
+                c1.moveToFirst();
+                t1Db = c1.getLong(c1.getColumnIndex(SearchHistory.DATE_LAST_VISITED));
+            } finally {
+                c1.close();
+            }
+
+            mAsserter.dumpLog("First insert:");
+            mAsserter.dumpLog("  insertStart " + insertStart);
+            mAsserter.dumpLog("  insertFinish " + insertFinish);
+            mAsserter.dumpLog("  t1Db " + t1Db);
+            mAsserter.ok(t1Db >= insertStart, "DATE_LAST_VISITED",
+                         "Date last visited should be set on insert.");
+            mAsserter.ok(t1Db <= insertFinish, "DATE_LAST_VISITED",
+                         "Date last visited should be set on insert.");
+
+            cv = new ContentValues();
+            cv.put(SearchHistory.QUERY, insertedTerm);
+
+            insertStart = System.currentTimeMillis();
+            mProvider.insert(SearchHistory.CONTENT_URI, cv);
+            insertFinish = System.currentTimeMillis();
+
+            final Cursor c2 = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            try {
+                c2.moveToFirst();
+                t2Db = c2.getLong(c2.getColumnIndex(SearchHistory.DATE_LAST_VISITED));
+            } finally {
+                c2.close();
+            }
+
+            mAsserter.dumpLog("Second insert:");
+            mAsserter.dumpLog("  insertStart " + insertStart);
+            mAsserter.dumpLog("  insertFinish " + insertFinish);
+            mAsserter.dumpLog("  t2Db " + t2Db);
+
+            mAsserter.ok(t2Db >= insertStart, "DATE_LAST_VISITED",
+                         "Date last visited should be set on insert.");
+            mAsserter.ok(t2Db <= insertFinish, "DATE_LAST_VISITED",
+                         "Date last visited should be set on insert.");
+            mAsserter.ok(t2Db >= t1Db, "DATE_LAST_VISITED",
+                         "Date last visited should be updated on key increment.");
+        }
+    }
+
+    /**
+     * Verify that sending a delete command empties the database.
+     */
+    private class TestDelete extends TestCase {
+        @Override
+        public void test() throws Exception {
+            String insertedTerm = "Courtside Seats";
+
+            ContentValues cv = new ContentValues();
+            cv.put(SearchHistory.QUERY, insertedTerm);
+            mProvider.insert(SearchHistory.CONTENT_URI, cv);
+
+            final Cursor c1 = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            try {
+                mAsserter.is(c1.getCount(), 1, "Should have one value");
+                mProvider.delete(SearchHistory.CONTENT_URI, null, null);
+            } finally {
+                c1.close();
+            }
+
+            final Cursor c2 = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            try {
+                mAsserter.is(c2.getCount(), 0, "Should be empty");
+                mProvider.insert(SearchHistory.CONTENT_URI, cv);
+            } finally {
+                c2.close();
+            }
+
+            final Cursor c3 = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            try {
+                mAsserter.is(c3.getCount(), 1, "Should have one value");
+            } finally {
+                c3.close();
+            }
+        }
+    }
+
+
+    /**
+     * Ensure that we only increment when the case matches.
+     */
+    private class TestIncrement extends TestCase {
+        @Override
+        public void test() throws Exception {
+            ContentValues cv = new ContentValues();
+            cv.put(SearchHistory.QUERY, "omaha");
+            mProvider.insert(SearchHistory.CONTENT_URI, cv);
+
+            cv = new ContentValues();
+            cv.put(SearchHistory.QUERY, "omaha");
+            mProvider.insert(SearchHistory.CONTENT_URI, cv);
+
+            Cursor c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            try {
+                c.moveToFirst();
+                mAsserter.is(c.getCount(), 1, "Should have one result");
+                mAsserter.is(c.getInt(c.getColumnIndex(SearchHistory.VISITS)), 2,
+                             "Counter should be 2");
+            } finally {
+                c.close();
+            }
+
+            cv = new ContentValues();
+            cv.put(SearchHistory.QUERY, "Omaha");
+            mProvider.insert(SearchHistory.CONTENT_URI, cv);
+            c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            try {
+                mAsserter.is(c.getCount(), 2, "Should have two results");
+            } finally {
+                c.close();
+            }
+        }
+    }
+}