diff --git a/core/java/android/provider/SearchRecentSuggestions.java b/core/java/android/provider/SearchRecentSuggestions.java index 0632d9498b84d..13ad9d345a496 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/provider/SearchRecentSuggestionsProviderTest.java similarity index 92% rename from core/tests/coretests/src/android/content/SearchRecentSuggestionsProviderTest.java rename to core/tests/coretests/src/android/provider/SearchRecentSuggestionsProviderTest.java index a4c33f5bde44c..1512eab06c72f 100644 --- a/core/tests/coretests/src/android/content/SearchRecentSuggestionsProviderTest.java +++ b/core/tests/coretests/src/android/provider/SearchRecentSuggestionsProviderTest.java @@ -14,72 +14,58 @@ * limitations under the License. */ -package android.content; +package android.provider; import android.app.SearchManager; +import android.content.ContentResolver; 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); - } -} +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.content.SearchRecentSuggestionsProviderTest + * -e class android.provider.SearchRecentSuggestionsProviderTest * com.android.frameworktest.tests/android.test.InstrumentationTestRunner */ -// Suppress these until bug http://b/issue?id=1416586 is fixed. -@Suppress +@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 + * 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(), + 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 */ @@ -87,22 +73,23 @@ public class SearchRecentSuggestionsProviderTest extends ProviderTestCase2= 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); @@ -337,20 +325,20 @@ public class SearchRecentSuggestionsProviderTest extends ProviderTestCase2= 0) ? c.getString(colDisp2) : null; - + assertEquals(query, display1); boolean result = true; if (matchDisplay1 != null) { @@ -374,13 +362,13 @@ public class SearchRecentSuggestionsProviderTest extends ProviderTestCase2