/* * 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.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.SearchRecentSuggestionsProvider; 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 * {@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,
 *              MySuggestionsProvider.AUTHORITY, MySuggestionsProvider.MODE);
 *      suggestions.saveRecentQuery(queryString, null);
 * 
* *

For a working example, see SearchSuggestionSampleProvider and SearchQueryResults in * samples/ApiDemos/app. * *

*

Developer Guides

*

For information about using search suggestions in your application, read the * Adding Recent Query * Suggestions developer guide.

*
*/ public class SearchRecentSuggestions { // debugging support private static final String LOG_TAG = "SearchSuggestions"; // 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"; public static final String DISPLAY2 = "display2"; 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.DATE, 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.DATE, SuggestionColumns.QUERY, SuggestionColumns.DISPLAY1, SuggestionColumns.DISPLAY2, }; /* these indices depend on QUERIES_PROJECTION_xxx */ /** Index into the provided query projections. For use with Cursor.update methods. */ public static final int QUERIES_PROJECTION_DATE_INDEX = 1; /** Index into the provided query projections. For use with Cursor.update methods. */ public static final int QUERIES_PROJECTION_QUERY_INDEX = 2; /** Index into the provided query projections. For use with Cursor.update methods. */ 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 /* * Set a cap on the count of items in the suggestions table, to * prevent db and layout operations from dragging to a crawl. Revisit this * cap when/if db/layout performance improvements are made. */ private static final int MAX_HISTORY_COUNT = 250; // client-provided configuration values 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}. * * @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) || ((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"); } /** * 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 * 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(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(); values.put(SuggestionColumns.DISPLAY1, queryString); if (mTwoLineDisplay) { values.put(SuggestionColumns.DISPLAY2, line2); } values.put(SuggestionColumns.QUERY, queryString); values.put(SuggestionColumns.DATE, now); cr.insert(mSuggestionsUri, values); } 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 * searches to be replayed by other users of the device (via suggestions). */ public void clearHistory() { ContentResolver cr = mContext.getContentResolver(); truncateHistory(cr, 0); } /** * 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. */ protected void truncateHistory(ContentResolver cr, int maxEntries) { if (maxEntries < 0) { throw new IllegalArgumentException(); } try { // null means "delete all". otherwise "delete but leave n newest" String selection = null; if (maxEntries > 0) { selection = "_id IN " + "(SELECT _id FROM suggestions" + " ORDER BY " + SuggestionColumns.DATE + " DESC" + " LIMIT -1 OFFSET " + String.valueOf(maxEntries) + ")"; } cr.delete(mSuggestionsUri, selection, null); } catch (RuntimeException e) { Log.e(LOG_TAG, "truncateHistory", e); } } }