| /* |
| * 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}. |
| * |
| * <p>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. |
| * |
| * <p>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: |
| * <pre class="prettyprint"> |
| * SearchSuggestions suggestions = new SearchSuggestions(this, |
| * MySuggestionsProvider.AUTHORITY, MySuggestionsProvider.MODE); |
| * suggestions.saveRecentQuery(queryString, null); |
| * </pre> |
| * |
| * <p>For a working example, see SearchSuggestionSampleProvider and SearchQueryResults in |
| * samples/ApiDemos/app. |
| */ |
| 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); |
| } |
| } |
| } |