From 553a53ef9ff789dff8b5a74dfea4d6f37feeb263 Mon Sep 17 00:00:00 2001 From: Ficus Kirkpatrick Date: Wed, 3 Nov 2010 17:38:49 -0700 Subject: Make saveRecentQuery() async. Bug: 3163612 Change-Id: Idd3c1925e0f1dc3272dd1303d8f2907c5c5fca8b --- .../android/provider/SearchRecentSuggestions.java | 112 +++--- .../SearchRecentSuggestionsProviderTest.java | 402 --------------------- .../SearchRecentSuggestionsProviderTest.java | 390 ++++++++++++++++++++ .../src/android/provider/TestProvider.java | 33 ++ 4 files changed, 485 insertions(+), 452 deletions(-) delete mode 100644 core/tests/coretests/src/android/content/SearchRecentSuggestionsProviderTest.java create mode 100644 core/tests/coretests/src/android/provider/SearchRecentSuggestionsProviderTest.java create mode 100644 core/tests/coretests/src/android/provider/TestProvider.java diff --git a/core/java/android/provider/SearchRecentSuggestions.java b/core/java/android/provider/SearchRecentSuggestions.java index 0632d9498b84..13ad9d345a49 100644 --- a/core/java/android/provider/SearchRecentSuggestions.java +++ b/core/java/android/provider/SearchRecentSuggestions.java @@ -20,36 +20,35 @@ import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.SearchRecentSuggestionsProvider; -import android.database.Cursor; import android.net.Uri; import android.text.TextUtils; import android.util.Log; +import java.util.concurrent.Semaphore; + /** - * This is a utility class providing access to + * This is a utility class providing access to * {@link android.content.SearchRecentSuggestionsProvider}. - * + * *

Unlike some utility classes, this one must be instantiated and properly initialized, so that * it can be configured to operate with the search suggestions provider that you have created. - * + * *

Typically, you will do this in your searchable activity, each time you receive an incoming * {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH} Intent. The code to record each * incoming query is as follows: *

- *      SearchSuggestions suggestions = new SearchSuggestions(this, 
+ *      SearchSuggestions suggestions = new SearchSuggestions(this,
  *              MySuggestionsProvider.AUTHORITY, MySuggestionsProvider.MODE);
  *      suggestions.saveRecentQuery(queryString, null);
  * 
- * + * *

For a working example, see SearchSuggestionSampleProvider and SearchQueryResults in * samples/ApiDemos/app. */ public class SearchRecentSuggestions { // debugging support private static final String LOG_TAG = "SearchSuggestions"; - // DELETE ME (eventually) - private static final int DBG_SUGGESTION_TIMESTAMPS = 0; - + // This is a superset of all possible column names (need not all be in table) private static class SuggestionColumns implements BaseColumns { public static final String DISPLAY1 = "display1"; @@ -57,27 +56,28 @@ public class SearchRecentSuggestions { public static final String QUERY = "query"; public static final String DATE = "date"; } - + /* if you change column order you must also change indices below */ /** * This is the database projection that can be used to view saved queries, when * configured for one-line operation. */ public static final String[] QUERIES_PROJECTION_1LINE = new String[] { - SuggestionColumns._ID, + SuggestionColumns._ID, SuggestionColumns.DATE, - SuggestionColumns.QUERY, + SuggestionColumns.QUERY, SuggestionColumns.DISPLAY1, }; + /* if you change column order you must also change indices below */ /** * This is the database projection that can be used to view saved queries, when * configured for two-line operation. */ public static final String[] QUERIES_PROJECTION_2LINE = new String[] { - SuggestionColumns._ID, + SuggestionColumns._ID, SuggestionColumns.DATE, - SuggestionColumns.QUERY, + SuggestionColumns.QUERY, SuggestionColumns.DISPLAY1, SuggestionColumns.DISPLAY2, }; @@ -91,11 +91,6 @@ public class SearchRecentSuggestions { public static final int QUERIES_PROJECTION_DISPLAY1_INDEX = 3; /** Index into the provided query projections. For use with Cursor.update methods. */ public static final int QUERIES_PROJECTION_DISPLAY2_INDEX = 4; // only when 2line active - - /* columns needed to determine whether to truncate history */ - private static final String[] TRUNCATE_HISTORY_PROJECTION = new String[] { - SuggestionColumns._ID, SuggestionColumns.DATE - }; /* * Set a cap on the count of items in the suggestions table, to @@ -103,73 +98,90 @@ public class SearchRecentSuggestions { * cap when/if db/layout performance improvements are made. */ private static final int MAX_HISTORY_COUNT = 250; - + // client-provided configuration values - private Context mContext; - private String mAuthority; - private boolean mTwoLineDisplay; - private Uri mSuggestionsUri; - private String[] mQueriesProjection; + private final Context mContext; + private final String mAuthority; + private final boolean mTwoLineDisplay; + private final Uri mSuggestionsUri; + + /** Released once per completion of async write. Used for tests. */ + private static final Semaphore sWritesInProgress = new Semaphore(0); /** * Although provider utility classes are typically static, this one must be constructed - * because it needs to be initialized using the same values that you provided in your - * {@link android.content.SearchRecentSuggestionsProvider}. - * + * because it needs to be initialized using the same values that you provided in your + * {@link android.content.SearchRecentSuggestionsProvider}. + * * @param authority This must match the authority that you've declared in your manifest. * @param mode You can use mode flags here to determine certain functional aspects of your * database. Note, this value should not change from run to run, because when it does change, * your suggestions database may be wiped. - * + * * @see android.content.SearchRecentSuggestionsProvider * @see android.content.SearchRecentSuggestionsProvider#setupSuggestions */ public SearchRecentSuggestions(Context context, String authority, int mode) { - if (TextUtils.isEmpty(authority) || + if (TextUtils.isEmpty(authority) || ((mode & SearchRecentSuggestionsProvider.DATABASE_MODE_QUERIES) == 0)) { throw new IllegalArgumentException(); } // unpack mode flags mTwoLineDisplay = (0 != (mode & SearchRecentSuggestionsProvider.DATABASE_MODE_2LINES)); - + // saved values mContext = context; mAuthority = new String(authority); - + // derived values mSuggestionsUri = Uri.parse("content://" + mAuthority + "/suggestions"); - - if (mTwoLineDisplay) { - mQueriesProjection = QUERIES_PROJECTION_2LINE; - } else { - mQueriesProjection = QUERIES_PROJECTION_1LINE; - } } /** - * Add a query to the recent queries list. - * + * Add a query to the recent queries list. Returns immediately, performing the save + * in the background. + * * @param queryString The string as typed by the user. This string will be displayed as * the suggestion, and if the user clicks on the suggestion, this string will be sent to your * searchable activity (as a new search query). - * @param line2 If you have configured your recent suggestions provider with - * {@link android.content.SearchRecentSuggestionsProvider#DATABASE_MODE_2LINES}, you can + * @param line2 If you have configured your recent suggestions provider with + * {@link android.content.SearchRecentSuggestionsProvider#DATABASE_MODE_2LINES}, you can * pass a second line of text here. It will be shown in a smaller font, below the primary * suggestion. When typing, matches in either line of text will be displayed in the list. * If you did not configure two-line mode, or if a given suggestion does not have any * additional text to display, you can pass null here. */ - public void saveRecentQuery(String queryString, String line2) { + public void saveRecentQuery(final String queryString, final String line2) { if (TextUtils.isEmpty(queryString)) { return; } if (!mTwoLineDisplay && !TextUtils.isEmpty(line2)) { throw new IllegalArgumentException(); } - + + new Thread("saveRecentQuery") { + @Override + public void run() { + saveRecentQueryBlocking(queryString, line2); + sWritesInProgress.release(); + } + }.start(); + } + + // Visible for testing. + void waitForSave() { + // Acquire writes semaphore until there is nothing available. + // This is to clean up after any previous callers to saveRecentQuery + // who did not also call waitForSave(). + do { + sWritesInProgress.acquireUninterruptibly(); + } while (sWritesInProgress.availablePermits() > 0); + } + + private void saveRecentQueryBlocking(String queryString, String line2) { ContentResolver cr = mContext.getContentResolver(); long now = System.currentTimeMillis(); - + // Use content resolver (not cursor) to insert/update this query try { ContentValues values = new ContentValues(); @@ -183,14 +195,14 @@ public class SearchRecentSuggestions { } catch (RuntimeException e) { Log.e(LOG_TAG, "saveRecentQuery", e); } - + // Shorten the list (if it has become too long) truncateHistory(cr, MAX_HISTORY_COUNT); } - + /** * Completely delete the history. Use this call to implement a "clear history" UI. - * + * * Any application that implements search suggestions based on previous actions (such as * recent queries, page/items viewed, etc.) should provide a way for the user to clear the * history. This gives the user a measure of privacy, if they do not wish for their recent @@ -203,7 +215,7 @@ public class SearchRecentSuggestions { /** * Reduces the length of the history table, to prevent it from growing too large. - * + * * @param cr Convenience copy of the content resolver. * @param maxEntries Max entries to leave in the table. 0 means remove all entries. */ @@ -211,7 +223,7 @@ public class SearchRecentSuggestions { if (maxEntries < 0) { throw new IllegalArgumentException(); } - + try { // null means "delete all". otherwise "delete but leave n newest" String selection = null; diff --git a/core/tests/coretests/src/android/content/SearchRecentSuggestionsProviderTest.java b/core/tests/coretests/src/android/content/SearchRecentSuggestionsProviderTest.java deleted file mode 100644 index a4c33f5bde44..000000000000 --- a/core/tests/coretests/src/android/content/SearchRecentSuggestionsProviderTest.java +++ /dev/null @@ -1,402 +0,0 @@ -/* - * Copyright (C) 2008 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 android.content; - -import android.app.SearchManager; -import android.database.Cursor; -import android.net.Uri; -import android.provider.SearchRecentSuggestions; -import android.test.ProviderTestCase2; -import android.test.suitebuilder.annotation.Suppress; - -/** - * Very simple provider that I can instantiate right here. - */ -class TestProvider extends SearchRecentSuggestionsProvider { - final static String AUTHORITY = "android.content.TestProvider"; - final static int MODE = DATABASE_MODE_QUERIES + DATABASE_MODE_2LINES; - - public TestProvider() { - super(); - setupSuggestions(AUTHORITY, MODE); - } -} - -/** - * ProviderTestCase that performs unit tests of SearchRecentSuggestionsProvider. - * - * You can run this test in isolation via the commands: - * - * $ (cd tests/FrameworkTests/ && mm) && adb sync - * $ adb shell am instrument -w \ - * -e class android.content.SearchRecentSuggestionsProviderTest - * com.android.frameworktest.tests/android.test.InstrumentationTestRunner - */ -// Suppress these until bug http://b/issue?id=1416586 is fixed. -@Suppress -public class SearchRecentSuggestionsProviderTest extends ProviderTestCase2 { - - // Elements prepared by setUp() - SearchRecentSuggestions mSearchHelper; - - public SearchRecentSuggestionsProviderTest() { - super(TestProvider.class, TestProvider.AUTHORITY); - } - - /** - * During setup, grab a helper for DB access - */ - @Override - public void setUp() throws Exception { - super.setUp(); - - // Use the recent suggestions helper. As long as we pass in our isolated context, - // it should correctly access the provider under test. - mSearchHelper = new SearchRecentSuggestions(getMockContext(), - TestProvider.AUTHORITY, TestProvider.MODE); - - // test for empty database at setup time - checkOpenCursorCount(0); - } - - /** - * Simple test to see if we can instantiate the whole mess. - */ - public void testSetup() { - assertTrue(true); - } - - /** - * Simple test to see if we can write and read back a single query - */ - public void testOneQuery() { - final String TEST_LINE1 = "test line 1"; - final String TEST_LINE2 = "test line 2"; - mSearchHelper.saveRecentQuery(TEST_LINE1, TEST_LINE2); - - // make sure that there are is exactly one entry returned by a non-filtering cursor - checkOpenCursorCount(1); - - // test non-filtering cursor for correct entry - checkResultCounts(null, 1, 1, TEST_LINE1, TEST_LINE2); - - // test filtering cursor for correct entry - checkResultCounts(TEST_LINE1, 1, 1, TEST_LINE1, TEST_LINE2); - checkResultCounts(TEST_LINE2, 1, 1, TEST_LINE1, TEST_LINE2); - - // test that a different filter returns zero results - checkResultCounts("bad filter", 0, 0, null, null); - } - - /** - * Simple test to see if we can write and read back a diverse set of queries - */ - public void testMixedQueries() { - // we'll make 10 queries named "query x" and 10 queries named "test x" - final String TEST_GROUP_1 = "query "; - final String TEST_GROUP_2 = "test "; - final String TEST_LINE2 = "line2 "; - final int GROUP_COUNT = 10; - - writeEntries(GROUP_COUNT, TEST_GROUP_1, TEST_LINE2); - writeEntries(GROUP_COUNT, TEST_GROUP_2, TEST_LINE2); - - // check counts - checkOpenCursorCount(2 * GROUP_COUNT); - - // check that each query returns the right result counts - checkResultCounts(TEST_GROUP_1, GROUP_COUNT, GROUP_COUNT, null, null); - checkResultCounts(TEST_GROUP_2, GROUP_COUNT, GROUP_COUNT, null, null); - checkResultCounts(TEST_LINE2, 2 * GROUP_COUNT, 2 * GROUP_COUNT, null, null); - } - - /** - * Test that the reordering code works properly. The most recently injected queries - * should replace existing queries and be sorted to the top of the list. - */ - public void testReordering() { - // first we'll make 10 queries named "group1 x" - final int GROUP_1_COUNT = 10; - final String GROUP_1_QUERY = "group1 "; - final String GROUP_1_LINE2 = "line2 "; - writeEntries(GROUP_1_COUNT, GROUP_1_QUERY, GROUP_1_LINE2); - - // check totals - checkOpenCursorCount(GROUP_1_COUNT); - - // guarantee that group 1 has older timestamps - writeDelay(); - - // next we'll add 10 entries named "group2 x" - final int GROUP_2_COUNT = 10; - final String GROUP_2_QUERY = "group2 "; - final String GROUP_2_LINE2 = "line2 "; - writeEntries(GROUP_2_COUNT, GROUP_2_QUERY, GROUP_2_LINE2); - - // check totals - checkOpenCursorCount(GROUP_1_COUNT + GROUP_2_COUNT); - - // guarantee that group 2 has older timestamps - writeDelay(); - - // now refresh 5 of the 10 from group 1 - // change line2 so they can be more easily tracked - final int GROUP_3_COUNT = 5; - final String GROUP_3_QUERY = GROUP_1_QUERY; - final String GROUP_3_LINE2 = "refreshed "; - writeEntries(GROUP_3_COUNT, GROUP_3_QUERY, GROUP_3_LINE2); - - // confirm that the total didn't change (those were replacements, not adds) - checkOpenCursorCount(GROUP_1_COUNT + GROUP_2_COUNT); - - // confirm that the are now 5 in group 1, 10 in group 2, and 5 in group 3 - int newGroup1Count = GROUP_1_COUNT - GROUP_3_COUNT; - checkResultCounts(GROUP_1_QUERY, newGroup1Count, newGroup1Count, null, GROUP_1_LINE2); - checkResultCounts(GROUP_2_QUERY, GROUP_2_COUNT, GROUP_2_COUNT, null, null); - checkResultCounts(GROUP_3_QUERY, GROUP_3_COUNT, GROUP_3_COUNT, null, GROUP_3_LINE2); - - // finally, spot check that the right groups are in the right places - // the ordering should be group 3 (newest), group 2, group 1 (oldest) - Cursor c = getQueryCursor(null); - int colQuery = c.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_QUERY); - int colDisplay1 = c.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_TEXT_1); - int colDisplay2 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2); - - // Spot check the first and last expected entries of group 3 - c.moveToPosition(0); - assertTrue("group 3 did not properly reorder to head of list", - checkRow(c, colQuery, colDisplay1, colDisplay2, GROUP_3_QUERY, GROUP_3_LINE2)); - c.move(GROUP_3_COUNT - 1); - assertTrue("group 3 did not properly reorder to head of list", - checkRow(c, colQuery, colDisplay1, colDisplay2, GROUP_3_QUERY, GROUP_3_LINE2)); - - // Spot check the first and last expected entries of group 2 - c.move(1); - assertTrue("group 2 not in expected position after reordering", - checkRow(c, colQuery, colDisplay1, colDisplay2, GROUP_2_QUERY, GROUP_2_LINE2)); - c.move(GROUP_2_COUNT - 1); - assertTrue("group 2 not in expected position after reordering", - checkRow(c, colQuery, colDisplay1, colDisplay2, GROUP_2_QUERY, GROUP_2_LINE2)); - - // Spot check the first and last expected entries of group 1 - c.move(1); - assertTrue("group 1 not in expected position after reordering", - checkRow(c, colQuery, colDisplay1, colDisplay2, GROUP_1_QUERY, GROUP_1_LINE2)); - c.move(newGroup1Count - 1); - assertTrue("group 1 not in expected position after reordering", - checkRow(c, colQuery, colDisplay1, colDisplay2, GROUP_1_QUERY, GROUP_1_LINE2)); - - c.close(); - } - - /** - * Test that the pruning code works properly, The database should not go beyond 250 entries, - * and the oldest entries should always be discarded first. - * - * TODO: This is a slow test, do we have annotation for that? - */ - public void testPruning() { - // first we'll make 50 queries named "group1 x" - final int GROUP_1_COUNT = 50; - final String GROUP_1_QUERY = "group1 "; - final String GROUP_1_LINE2 = "line2 "; - writeEntries(GROUP_1_COUNT, GROUP_1_QUERY, GROUP_1_LINE2); - - // check totals - checkOpenCursorCount(GROUP_1_COUNT); - - // guarantee that group 1 has older timestamps (and will be pruned first) - writeDelay(); - - // next we'll add 200 entries named "group2 x" - final int GROUP_2_COUNT = 200; - final String GROUP_2_QUERY = "group2 "; - final String GROUP_2_LINE2 = "line2 "; - writeEntries(GROUP_2_COUNT, GROUP_2_QUERY, GROUP_2_LINE2); - - // check totals - checkOpenCursorCount(GROUP_1_COUNT + GROUP_2_COUNT); - - // Finally we'll add 10 more entries named "group3 x" - // These should push out 10 entries from group 1 - final int GROUP_3_COUNT = 10; - final String GROUP_3_QUERY = "group3 "; - final String GROUP_3_LINE2 = "line2 "; - writeEntries(GROUP_3_COUNT, GROUP_3_QUERY, GROUP_3_LINE2); - - // total should still be 250 - checkOpenCursorCount(GROUP_1_COUNT + GROUP_2_COUNT); - - // there should be 40 group 1, 200 group 2, and 10 group 3 - int group1NewCount = GROUP_1_COUNT-GROUP_3_COUNT; - checkResultCounts(GROUP_1_QUERY, group1NewCount, group1NewCount, null, null); - checkResultCounts(GROUP_2_QUERY, GROUP_2_COUNT, GROUP_2_COUNT, null, null); - checkResultCounts(GROUP_3_QUERY, GROUP_3_COUNT, GROUP_3_COUNT, null, null); - } - - /** - * Test that the clear history code works properly. - */ - public void testClear() { - // first we'll make 10 queries named "group1 x" - final int GROUP_1_COUNT = 10; - final String GROUP_1_QUERY = "group1 "; - final String GROUP_1_LINE2 = "line2 "; - writeEntries(GROUP_1_COUNT, GROUP_1_QUERY, GROUP_1_LINE2); - - // next we'll add 10 entries named "group2 x" - final int GROUP_2_COUNT = 10; - final String GROUP_2_QUERY = "group2 "; - final String GROUP_2_LINE2 = "line2 "; - writeEntries(GROUP_2_COUNT, GROUP_2_QUERY, GROUP_2_LINE2); - - // check totals - checkOpenCursorCount(GROUP_1_COUNT + GROUP_2_COUNT); - - // delete all - mSearchHelper.clearHistory(); - - // check totals - checkOpenCursorCount(0); - } - - /** - * Write a sequence of queries into the database, with incrementing counters in the strings. - */ - private void writeEntries(int groupCount, String line1Base, String line2Base) { - for (int i = 0; i < groupCount; i++) { - final String line1 = line1Base + i; - final String line2 = line2Base + i; - mSearchHelper.saveRecentQuery(line1, line2); - } - } - - /** - * A very slight delay to ensure that successive groups of queries in the DB cannot - * have the same timestamp. - */ - private void writeDelay() { - try { - Thread.sleep(10); - } catch (InterruptedException e) { - fail("Interrupted sleep."); - } - } - - /** - * Access an "open" (no selection) suggestions cursor and confirm that it has the specified - * number of entries. - * - * @param expectCount The expected number of entries returned by the cursor. - */ - private void checkOpenCursorCount(int expectCount) { - Cursor c = getQueryCursor(null); - assertEquals(expectCount, c.getCount()); - c.close(); - } - - /** - * Set up a filter cursor and then scan it for specific results. - * - * @param queryString The query string to apply. - * @param minRows The minimum number of matching rows that must be found. - * @param maxRows The maximum number of matching rows that must be found. - * @param matchDisplay1 If non-null, must match DISPLAY1 column if row counts as match - * @param matchDisplay2 If non-null, must match DISPLAY2 column if row counts as match - */ - private void checkResultCounts(String queryString, int minRows, int maxRows, - String matchDisplay1, String matchDisplay2) { - - // get the cursor and apply sanity checks to result - Cursor c = getQueryCursor(queryString); - assertNotNull(c); - assertTrue("Insufficient rows in filtered cursor", c.getCount() >= minRows); - - // look for minimum set of columns (note, display2 is optional) - int colQuery = c.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_QUERY); - int colDisplay1 = c.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_TEXT_1); - int colDisplay2 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2); - - // now loop through rows and look for desired rows - int foundRows = 0; - c.moveToFirst(); - while (!c.isAfterLast()) { - if (checkRow(c, colQuery, colDisplay1, colDisplay2, matchDisplay1, matchDisplay2)) { - foundRows++; - } - c.moveToNext(); - } - - // now check the results - assertTrue(minRows <= foundRows); - assertTrue(foundRows <= maxRows); - - c.close(); - } - - /** - * Check a single row for equality with target strings. - * - * @param c The cursor, already moved to the row - * @param colQuery The column # containing the query. The query must match display1. - * @param colDisp1 The column # containing display line 1. - * @param colDisp2 The column # containing display line 2, or -1 if no column - * @param matchDisplay1 If non-null, this must be the prefix of display1 - * @param matchDisplay2 If non-null, this must be the prefix of display2 - * @return Returns true if the row is a "match" - */ - private boolean checkRow(Cursor c, int colQuery, int colDisp1, int colDisp2, - String matchDisplay1, String matchDisplay2) { - // Get the data from the row - String query = c.getString(colQuery); - String display1 = c.getString(colDisp1); - String display2 = (colDisp2 >= 0) ? c.getString(colDisp2) : null; - - assertEquals(query, display1); - boolean result = true; - if (matchDisplay1 != null) { - result = result && (display1 != null) && display1.startsWith(matchDisplay1); - } - if (matchDisplay2 != null) { - result = result && (display2 != null) && display2.startsWith(matchDisplay2); - } - - return result; - } - - /** - * Generate a query cursor in a manner like the search dialog would. - * - * @param queryString The search string, or, null for "all" - * @return Returns a cursor, or null if there was some problem. Be sure to close the cursor - * when done with it. - */ - private Cursor getQueryCursor(String queryString) { - ContentResolver cr = getMockContext().getContentResolver(); - - String uriStr = "content://" + TestProvider.AUTHORITY + - '/' + SearchManager.SUGGEST_URI_PATH_QUERY; - Uri contentUri = Uri.parse(uriStr); - - String[] selArgs = new String[] {queryString}; - - Cursor c = cr.query(contentUri, null, null, selArgs, null); - - assertNotNull(c); - return c; - } -} diff --git a/core/tests/coretests/src/android/provider/SearchRecentSuggestionsProviderTest.java b/core/tests/coretests/src/android/provider/SearchRecentSuggestionsProviderTest.java new file mode 100644 index 000000000000..1512eab06c72 --- /dev/null +++ b/core/tests/coretests/src/android/provider/SearchRecentSuggestionsProviderTest.java @@ -0,0 +1,390 @@ +/* + * Copyright (C) 2008 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 android.provider; + +import android.app.SearchManager; +import android.content.ContentResolver; +import android.database.Cursor; +import android.net.Uri; +import android.test.ProviderTestCase2; +import android.test.suitebuilder.annotation.MediumTest; + +/** + * ProviderTestCase that performs unit tests of SearchRecentSuggestionsProvider. + * + * You can run this test in isolation via the commands: + * + * $ (cd tests/FrameworkTests/ && mm) && adb sync + * $ adb shell am instrument -w \ + * -e class android.provider.SearchRecentSuggestionsProviderTest + * com.android.frameworktest.tests/android.test.InstrumentationTestRunner + */ +@MediumTest +public class SearchRecentSuggestionsProviderTest extends ProviderTestCase2 { + + // Elements prepared by setUp() + SearchRecentSuggestions mSearchHelper; + + public SearchRecentSuggestionsProviderTest() { + super(TestProvider.class, TestProvider.AUTHORITY); + } + + /** + * During setup, grab a helper for DB access + */ + @Override + public void setUp() throws Exception { + super.setUp(); + + // Use the recent suggestions helper. As long as we pass in our isolated context, + // it should correctly access the provider under test. + mSearchHelper = new SearchRecentSuggestions(getMockContext(), + TestProvider.AUTHORITY, TestProvider.MODE); + + // test for empty database at setup time + checkOpenCursorCount(0); + } + + /** + * Simple test to see if we can instantiate the whole mess. + */ + public void testSetup() { + assertTrue(true); + } + + /** + * Simple test to see if we can write and read back a single query + */ + public void testOneQuery() { + final String TEST_LINE1 = "test line 1"; + final String TEST_LINE2 = "test line 2"; + mSearchHelper.saveRecentQuery(TEST_LINE1, TEST_LINE2); + mSearchHelper.waitForSave(); + + // make sure that there are is exactly one entry returned by a non-filtering cursor + checkOpenCursorCount(1); + + // test non-filtering cursor for correct entry + checkResultCounts(null, 1, 1, TEST_LINE1, TEST_LINE2); + + // test filtering cursor for correct entry + checkResultCounts(TEST_LINE1, 1, 1, TEST_LINE1, TEST_LINE2); + checkResultCounts(TEST_LINE2, 1, 1, TEST_LINE1, TEST_LINE2); + + // test that a different filter returns zero results + checkResultCounts("bad filter", 0, 0, null, null); + } + + /** + * Simple test to see if we can write and read back a diverse set of queries + */ + public void testMixedQueries() { + // we'll make 10 queries named "query x" and 10 queries named "test x" + final String TEST_GROUP_1 = "query "; + final String TEST_GROUP_2 = "test "; + final String TEST_LINE2 = "line2 "; + final int GROUP_COUNT = 10; + + writeEntries(GROUP_COUNT, TEST_GROUP_1, TEST_LINE2); + writeEntries(GROUP_COUNT, TEST_GROUP_2, TEST_LINE2); + + // check counts + checkOpenCursorCount(2 * GROUP_COUNT); + + // check that each query returns the right result counts + checkResultCounts(TEST_GROUP_1, GROUP_COUNT, GROUP_COUNT, null, null); + checkResultCounts(TEST_GROUP_2, GROUP_COUNT, GROUP_COUNT, null, null); + checkResultCounts(TEST_LINE2, 2 * GROUP_COUNT, 2 * GROUP_COUNT, null, null); + } + + /** + * Test that the reordering code works properly. The most recently injected queries + * should replace existing queries and be sorted to the top of the list. + */ + public void testReordering() { + // first we'll make 10 queries named "group1 x" + final int GROUP_1_COUNT = 10; + final String GROUP_1_QUERY = "group1 "; + final String GROUP_1_LINE2 = "line2 "; + writeEntries(GROUP_1_COUNT, GROUP_1_QUERY, GROUP_1_LINE2); + + // check totals + checkOpenCursorCount(GROUP_1_COUNT); + + // guarantee that group 1 has older timestamps + writeDelay(); + + // next we'll add 10 entries named "group2 x" + final int GROUP_2_COUNT = 10; + final String GROUP_2_QUERY = "group2 "; + final String GROUP_2_LINE2 = "line2 "; + writeEntries(GROUP_2_COUNT, GROUP_2_QUERY, GROUP_2_LINE2); + + // check totals + checkOpenCursorCount(GROUP_1_COUNT + GROUP_2_COUNT); + + // guarantee that group 2 has older timestamps + writeDelay(); + + // now refresh 5 of the 10 from group 1 + // change line2 so they can be more easily tracked + final int GROUP_3_COUNT = 5; + final String GROUP_3_QUERY = GROUP_1_QUERY; + final String GROUP_3_LINE2 = "refreshed "; + writeEntries(GROUP_3_COUNT, GROUP_3_QUERY, GROUP_3_LINE2); + + // confirm that the total didn't change (those were replacements, not adds) + checkOpenCursorCount(GROUP_1_COUNT + GROUP_2_COUNT); + + // confirm that the are now 5 in group 1, 10 in group 2, and 5 in group 3 + int newGroup1Count = GROUP_1_COUNT - GROUP_3_COUNT; + checkResultCounts(GROUP_1_QUERY, newGroup1Count, newGroup1Count, null, GROUP_1_LINE2); + checkResultCounts(GROUP_2_QUERY, GROUP_2_COUNT, GROUP_2_COUNT, null, null); + checkResultCounts(GROUP_3_QUERY, GROUP_3_COUNT, GROUP_3_COUNT, null, GROUP_3_LINE2); + + // finally, spot check that the right groups are in the right places + // the ordering should be group 3 (newest), group 2, group 1 (oldest) + Cursor c = getQueryCursor(null); + int colQuery = c.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_QUERY); + int colDisplay1 = c.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_TEXT_1); + int colDisplay2 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2); + + // Spot check the first and last expected entries of group 3 + c.moveToPosition(0); + assertTrue("group 3 did not properly reorder to head of list", + checkRow(c, colQuery, colDisplay1, colDisplay2, GROUP_3_QUERY, GROUP_3_LINE2)); + c.move(GROUP_3_COUNT - 1); + assertTrue("group 3 did not properly reorder to head of list", + checkRow(c, colQuery, colDisplay1, colDisplay2, GROUP_3_QUERY, GROUP_3_LINE2)); + + // Spot check the first and last expected entries of group 2 + c.move(1); + assertTrue("group 2 not in expected position after reordering", + checkRow(c, colQuery, colDisplay1, colDisplay2, GROUP_2_QUERY, GROUP_2_LINE2)); + c.move(GROUP_2_COUNT - 1); + assertTrue("group 2 not in expected position after reordering", + checkRow(c, colQuery, colDisplay1, colDisplay2, GROUP_2_QUERY, GROUP_2_LINE2)); + + // Spot check the first and last expected entries of group 1 + c.move(1); + assertTrue("group 1 not in expected position after reordering", + checkRow(c, colQuery, colDisplay1, colDisplay2, GROUP_1_QUERY, GROUP_1_LINE2)); + c.move(newGroup1Count - 1); + assertTrue("group 1 not in expected position after reordering", + checkRow(c, colQuery, colDisplay1, colDisplay2, GROUP_1_QUERY, GROUP_1_LINE2)); + + c.close(); + } + + /** + * Test that the pruning code works properly, The database should not go beyond 250 entries, + * and the oldest entries should always be discarded first. + * + * TODO: This is a slow test, do we have annotation for that? + */ + public void testPruning() { + // first we'll make 50 queries named "group1 x" + final int GROUP_1_COUNT = 50; + final String GROUP_1_QUERY = "group1 "; + final String GROUP_1_LINE2 = "line2 "; + writeEntries(GROUP_1_COUNT, GROUP_1_QUERY, GROUP_1_LINE2); + + // check totals + checkOpenCursorCount(GROUP_1_COUNT); + + // guarantee that group 1 has older timestamps (and will be pruned first) + writeDelay(); + + // next we'll add 200 entries named "group2 x" + final int GROUP_2_COUNT = 200; + final String GROUP_2_QUERY = "group2 "; + final String GROUP_2_LINE2 = "line2 "; + writeEntries(GROUP_2_COUNT, GROUP_2_QUERY, GROUP_2_LINE2); + + // check totals + checkOpenCursorCount(GROUP_1_COUNT + GROUP_2_COUNT); + + // Finally we'll add 10 more entries named "group3 x" + // These should push out 10 entries from group 1 + final int GROUP_3_COUNT = 10; + final String GROUP_3_QUERY = "group3 "; + final String GROUP_3_LINE2 = "line2 "; + writeEntries(GROUP_3_COUNT, GROUP_3_QUERY, GROUP_3_LINE2); + + // total should still be 250 + checkOpenCursorCount(GROUP_1_COUNT + GROUP_2_COUNT); + + // there should be 40 group 1, 200 group 2, and 10 group 3 + int group1NewCount = GROUP_1_COUNT-GROUP_3_COUNT; + checkResultCounts(GROUP_1_QUERY, group1NewCount, group1NewCount, null, null); + checkResultCounts(GROUP_2_QUERY, GROUP_2_COUNT, GROUP_2_COUNT, null, null); + checkResultCounts(GROUP_3_QUERY, GROUP_3_COUNT, GROUP_3_COUNT, null, null); + } + + /** + * Test that the clear history code works properly. + */ + public void testClear() { + // first we'll make 10 queries named "group1 x" + final int GROUP_1_COUNT = 10; + final String GROUP_1_QUERY = "group1 "; + final String GROUP_1_LINE2 = "line2 "; + writeEntries(GROUP_1_COUNT, GROUP_1_QUERY, GROUP_1_LINE2); + + // next we'll add 10 entries named "group2 x" + final int GROUP_2_COUNT = 10; + final String GROUP_2_QUERY = "group2 "; + final String GROUP_2_LINE2 = "line2 "; + writeEntries(GROUP_2_COUNT, GROUP_2_QUERY, GROUP_2_LINE2); + + // check totals + checkOpenCursorCount(GROUP_1_COUNT + GROUP_2_COUNT); + + // delete all + mSearchHelper.clearHistory(); + + // check totals + checkOpenCursorCount(0); + } + + /** + * Write a sequence of queries into the database, with incrementing counters in the strings. + */ + private void writeEntries(int groupCount, String line1Base, String line2Base) { + for (int i = 0; i < groupCount; i++) { + final String line1 = line1Base + i; + final String line2 = line2Base + i; + mSearchHelper.saveRecentQuery(line1, line2); + mSearchHelper.waitForSave(); + } + } + + /** + * A very slight delay to ensure that successive groups of queries in the DB cannot + * have the same timestamp. + */ + private void writeDelay() { + try { + Thread.sleep(10); + } catch (InterruptedException e) { + fail("Interrupted sleep."); + } + } + + /** + * Access an "open" (no selection) suggestions cursor and confirm that it has the specified + * number of entries. + * + * @param expectCount The expected number of entries returned by the cursor. + */ + private void checkOpenCursorCount(int expectCount) { + Cursor c = getQueryCursor(null); + assertEquals(expectCount, c.getCount()); + c.close(); + } + + /** + * Set up a filter cursor and then scan it for specific results. + * + * @param queryString The query string to apply. + * @param minRows The minimum number of matching rows that must be found. + * @param maxRows The maximum number of matching rows that must be found. + * @param matchDisplay1 If non-null, must match DISPLAY1 column if row counts as match + * @param matchDisplay2 If non-null, must match DISPLAY2 column if row counts as match + */ + private void checkResultCounts(String queryString, int minRows, int maxRows, + String matchDisplay1, String matchDisplay2) { + + // get the cursor and apply sanity checks to result + Cursor c = getQueryCursor(queryString); + assertNotNull(c); + assertTrue("Insufficient rows in filtered cursor", c.getCount() >= minRows); + + // look for minimum set of columns (note, display2 is optional) + int colQuery = c.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_QUERY); + int colDisplay1 = c.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_TEXT_1); + int colDisplay2 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2); + + // now loop through rows and look for desired rows + int foundRows = 0; + c.moveToFirst(); + while (!c.isAfterLast()) { + if (checkRow(c, colQuery, colDisplay1, colDisplay2, matchDisplay1, matchDisplay2)) { + foundRows++; + } + c.moveToNext(); + } + + // now check the results + assertTrue(minRows <= foundRows); + assertTrue(foundRows <= maxRows); + + c.close(); + } + + /** + * Check a single row for equality with target strings. + * + * @param c The cursor, already moved to the row + * @param colQuery The column # containing the query. The query must match display1. + * @param colDisp1 The column # containing display line 1. + * @param colDisp2 The column # containing display line 2, or -1 if no column + * @param matchDisplay1 If non-null, this must be the prefix of display1 + * @param matchDisplay2 If non-null, this must be the prefix of display2 + * @return Returns true if the row is a "match" + */ + private boolean checkRow(Cursor c, int colQuery, int colDisp1, int colDisp2, + String matchDisplay1, String matchDisplay2) { + // Get the data from the row + String query = c.getString(colQuery); + String display1 = c.getString(colDisp1); + String display2 = (colDisp2 >= 0) ? c.getString(colDisp2) : null; + + assertEquals(query, display1); + boolean result = true; + if (matchDisplay1 != null) { + result = result && (display1 != null) && display1.startsWith(matchDisplay1); + } + if (matchDisplay2 != null) { + result = result && (display2 != null) && display2.startsWith(matchDisplay2); + } + + return result; + } + + /** + * Generate a query cursor in a manner like the search dialog would. + * + * @param queryString The search string, or, null for "all" + * @return Returns a cursor, or null if there was some problem. Be sure to close the cursor + * when done with it. + */ + private Cursor getQueryCursor(String queryString) { + ContentResolver cr = getMockContext().getContentResolver(); + + String uriStr = "content://" + TestProvider.AUTHORITY + + '/' + SearchManager.SUGGEST_URI_PATH_QUERY; + Uri contentUri = Uri.parse(uriStr); + + String[] selArgs = new String[] {queryString}; + + Cursor c = cr.query(contentUri, null, null, selArgs, null); + + assertNotNull(c); + return c; + } +} diff --git a/core/tests/coretests/src/android/provider/TestProvider.java b/core/tests/coretests/src/android/provider/TestProvider.java new file mode 100644 index 000000000000..63c43b3f4a88 --- /dev/null +++ b/core/tests/coretests/src/android/provider/TestProvider.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2008 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 android.provider; + +import android.content.SearchRecentSuggestionsProvider; + +/** + * Very simple provider that I can instantiate right here. + */ +public class TestProvider extends SearchRecentSuggestionsProvider { + final static String AUTHORITY = "android.provider.TestProvider"; + final static int MODE = DATABASE_MODE_QUERIES + DATABASE_MODE_2LINES; + + public TestProvider() { + super(); + setupSuggestions(AUTHORITY, MODE); + } +} + -- cgit v1.2.3-59-g8ed1b