Bug 1030277 - new content provider for search history. r=rnewman
authorEric Edens <eedens@mozilla.com>
Tue, 01 Jul 2014 08:58:24 -0700
changeset 191871 1b9e9be1db2985a8145618ee7177f72c112294c8
parent 191870 b47ca036ce971e00390afdb0393dd27e020be7b5
child 191872 919383a600ee775de182bad247e88b598d0920ff
push id45685
push usercbook@mozilla.com
push dateWed, 02 Jul 2014 13:09:48 +0000
treeherdermozilla-inbound@60133a85f8ae [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrnewman
bugs1030277
milestone33.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1030277 - new content provider for search history. 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 = 18;
+    public static final int DATABASE_VERSION = 19;
     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;
@@ -744,16 +745,30 @@ final class BrowserDatabaseHelper extend
         createHistoryWithFaviconsView(db);
         createCombinedViewOn16(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, " +
@@ -1373,16 +1388,20 @@ final class BrowserDatabaseHelper extend
         } finally {
             if (cursor != null) {
                 cursor.close();
             }
             db.endTransaction();
         }
     }
 
+    private void upgradeDatabaseFrom18to19(SQLiteDatabase db) {
+        createSearchHistoryTable(db);
+    }
+
     @Override
     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
         debug("Upgrading browser.db: " + db.getPath() + " from " +
                 oldVersion + " to " + newVersion);
 
         // We have to do incremental upgrades until we reach the current
         // database schema version.
         for (int v = oldVersion + 1; v <= newVersion; v++) {
@@ -1449,16 +1468,20 @@ final class BrowserDatabaseHelper extend
 
                 case 17:
                     upgradeDatabaseFrom16to17(db);
                     break;
 
                 case 18:
                     upgradeDatabaseFrom17to18(db);
                     break;
+
+                case 19:
+                    upgradeDatabaseFrom18to19(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
@@ -1561,9 +1584,9 @@ final class BrowserDatabaseHelper extend
                 bookmark.put(Bookmarks.TYPE, Bookmarks.TYPE_BOOKMARK);
             } else {
                 bookmark.put(Bookmarks.TYPE, Bookmarks.TYPE_FOLDER);
             }
 
             bookmark.remove("folder");
         }
     }
-}
\ No newline at end of file
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/SearchHistoryProvider.java
@@ -0,0 +1,112 @@
+/* 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.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.text.TextUtils;
+
+public class SearchHistoryProvider extends SharedBrowserDatabaseProvider {
+
+    /**
+     * 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);
+
+        /*
+         * FIRST: Try incrementing the VISITS counter and updating 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();
+        }
+
+        /*
+         * SECOND: If the update failed, then insert a new record.
+         */
+        cv.put(SearchHistory.QUERY, query);
+        cv.put(SearchHistory.VISITS, 1);
+        cv.put(SearchHistory.DATE_LAST_VISITED, System.currentTimeMillis());
+
+        long id = db.insert(SearchHistory.TABLE_NAME, null, cv);
+
+        if (id < 0) {
+            return null;
+        }
+
+        return ContentUris.withAppendedId(uri, id);
+    }
+
+    @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',
     'DoorHangerPopup.java',
     'DynamicToolbar.java',
--- a/mobile/android/base/tests/robocop.ini
+++ b/mobile/android/base/tests/robocop.ini
@@ -75,16 +75,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,269 @@
+/* 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);
+            }
+
+            Cursor c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            mAsserter.is(c.getCount(), testStrings.length,
+                    "Should have one row for each insert");
+
+            c.close();
+        }
+    }
+
+    /**
+     * Verify that we can insert values into the DB, including unicode.
+     */
+    private class TestUnicodeQuery extends TestCase {
+        @Override
+        public void test() throws Exception {
+            ContentValues cv;
+            Cursor c = null;
+            String selection = SearchHistory.QUERY + " = ?";
+
+            for (int i = 0; i < testStrings.length; i++) {
+                cv = new ContentValues();
+                cv.put(SearchHistory.QUERY, testStrings[i]);
+                mProvider.insert(SearchHistory.CONTENT_URI, cv);
+
+                c = mProvider.query(SearchHistory.CONTENT_URI, null, selection,
+                        new String[]{ testStrings[i] }, null);
+                mAsserter.is(c.getCount(), 1,
+                        "Should have one row for insert of " + testStrings[i]);
+            }
+
+            if (c != null) {
+                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);
+            Cursor c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            c.moveToFirst();
+            t1Db = c.getLong(c.getColumnIndex(SearchHistory.DATE_LAST_VISITED));
+            c.close();
+            insertFinish = System.currentTimeMillis();
+            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);
+            c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            c.moveToFirst();
+            t2Db = c.getLong(c.getColumnIndex(SearchHistory.DATE_LAST_VISITED));
+            c.close();
+            insertFinish = System.currentTimeMillis();
+
+            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);
+
+            Cursor c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            mAsserter.is(c.getCount(), 1, "Should have one value");
+            mProvider.delete(SearchHistory.CONTENT_URI, null, null);
+            c.close();
+
+            c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            mAsserter.is(c.getCount(), 0, "Should be empty");
+            mProvider.insert(SearchHistory.CONTENT_URI, cv);
+            c.close();
+
+            c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            mAsserter.is(c.getCount(), 1, "Should have one value");
+            c.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);
+            c.moveToFirst();
+            mAsserter.is(c.getCount(), 1, "Should have one result");
+            mAsserter.is(c.getInt(c.getColumnIndex(SearchHistory.VISITS)), 2,
+                    "Counter should be 2");
+            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);
+            mAsserter.is(c.getCount(), 2, "Should have two results");
+            c.close();
+        }
+    }
+}