Home | History | Annotate | Download | only in provider
      1 /*
      2  * Copyright (C) 2008 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.provider;
     18 
     19 import android.content.ContentResolver;
     20 import android.content.ContentValues;
     21 import android.content.Context;
     22 import android.content.SearchRecentSuggestionsProvider;
     23 import android.net.Uri;
     24 import android.text.TextUtils;
     25 import android.util.Log;
     26 
     27 import java.util.concurrent.Semaphore;
     28 
     29 /**
     30  * This is a utility class providing access to
     31  * {@link android.content.SearchRecentSuggestionsProvider}.
     32  *
     33  * <p>Unlike some utility classes, this one must be instantiated and properly initialized, so that
     34  * it can be configured to operate with the search suggestions provider that you have created.
     35  *
     36  * <p>Typically, you will do this in your searchable activity, each time you receive an incoming
     37  * {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH} Intent.  The code to record each
     38  * incoming query is as follows:
     39  * <pre class="prettyprint">
     40  *      SearchSuggestions suggestions = new SearchSuggestions(this,
     41  *              MySuggestionsProvider.AUTHORITY, MySuggestionsProvider.MODE);
     42  *      suggestions.saveRecentQuery(queryString, null);
     43  * </pre>
     44  *
     45  * <p>For a working example, see SearchSuggestionSampleProvider and SearchQueryResults in
     46  * samples/ApiDemos/app.
     47  *
     48  * <div class="special reference">
     49  * <h3>Developer Guides</h3>
     50  * <p>For information about using search suggestions in your application, read the
     51  * <a href="{@docRoot}guide/topics/search/adding-recent-query-suggestions.html">Adding Recent Query
     52  * Suggestions</a> developer guide.</p>
     53  * </div>
     54  */
     55 public class SearchRecentSuggestions {
     56     // debugging support
     57     private static final String LOG_TAG = "SearchSuggestions";
     58 
     59     // This is a superset of all possible column names (need not all be in table)
     60     private static class SuggestionColumns implements BaseColumns {
     61         public static final String DISPLAY1 = "display1";
     62         public static final String DISPLAY2 = "display2";
     63         public static final String QUERY = "query";
     64         public static final String DATE = "date";
     65     }
     66 
     67     /* if you change column order you must also change indices below */
     68     /**
     69      * This is the database projection that can be used to view saved queries, when
     70      * configured for one-line operation.
     71      */
     72     public static final String[] QUERIES_PROJECTION_1LINE = new String[] {
     73         SuggestionColumns._ID,
     74         SuggestionColumns.DATE,
     75         SuggestionColumns.QUERY,
     76         SuggestionColumns.DISPLAY1,
     77     };
     78 
     79     /* if you change column order you must also change indices below */
     80     /**
     81      * This is the database projection that can be used to view saved queries, when
     82      * configured for two-line operation.
     83      */
     84     public static final String[] QUERIES_PROJECTION_2LINE = new String[] {
     85         SuggestionColumns._ID,
     86         SuggestionColumns.DATE,
     87         SuggestionColumns.QUERY,
     88         SuggestionColumns.DISPLAY1,
     89         SuggestionColumns.DISPLAY2,
     90     };
     91 
     92     /* these indices depend on QUERIES_PROJECTION_xxx */
     93     /** Index into the provided query projections.  For use with Cursor.update methods. */
     94     public static final int QUERIES_PROJECTION_DATE_INDEX = 1;
     95     /** Index into the provided query projections.  For use with Cursor.update methods. */
     96     public static final int QUERIES_PROJECTION_QUERY_INDEX = 2;
     97     /** Index into the provided query projections.  For use with Cursor.update methods. */
     98     public static final int QUERIES_PROJECTION_DISPLAY1_INDEX = 3;
     99     /** Index into the provided query projections.  For use with Cursor.update methods. */
    100     public static final int QUERIES_PROJECTION_DISPLAY2_INDEX = 4;  // only when 2line active
    101 
    102     /*
    103      * Set a cap on the count of items in the suggestions table, to
    104      * prevent db and layout operations from dragging to a crawl. Revisit this
    105      * cap when/if db/layout performance improvements are made.
    106      */
    107     private static final int MAX_HISTORY_COUNT = 250;
    108 
    109     // client-provided configuration values
    110     private final Context mContext;
    111     private final String mAuthority;
    112     private final boolean mTwoLineDisplay;
    113     private final Uri mSuggestionsUri;
    114 
    115     /** Released once per completion of async write.  Used for tests. */
    116     private static final Semaphore sWritesInProgress = new Semaphore(0);
    117 
    118     /**
    119      * Although provider utility classes are typically static, this one must be constructed
    120      * because it needs to be initialized using the same values that you provided in your
    121      * {@link android.content.SearchRecentSuggestionsProvider}.
    122      *
    123      * @param authority This must match the authority that you've declared in your manifest.
    124      * @param mode You can use mode flags here to determine certain functional aspects of your
    125      * database.  Note, this value should not change from run to run, because when it does change,
    126      * your suggestions database may be wiped.
    127      *
    128      * @see android.content.SearchRecentSuggestionsProvider
    129      * @see android.content.SearchRecentSuggestionsProvider#setupSuggestions
    130      */
    131     public SearchRecentSuggestions(Context context, String authority, int mode) {
    132         if (TextUtils.isEmpty(authority) ||
    133                 ((mode & SearchRecentSuggestionsProvider.DATABASE_MODE_QUERIES) == 0)) {
    134             throw new IllegalArgumentException();
    135         }
    136         // unpack mode flags
    137         mTwoLineDisplay = (0 != (mode & SearchRecentSuggestionsProvider.DATABASE_MODE_2LINES));
    138 
    139         // saved values
    140         mContext = context;
    141         mAuthority = new String(authority);
    142 
    143         // derived values
    144         mSuggestionsUri = Uri.parse("content://" + mAuthority + "/suggestions");
    145     }
    146 
    147     /**
    148      * Add a query to the recent queries list.  Returns immediately, performing the save
    149      * in the background.
    150      *
    151      * @param queryString The string as typed by the user.  This string will be displayed as
    152      * the suggestion, and if the user clicks on the suggestion, this string will be sent to your
    153      * searchable activity (as a new search query).
    154      * @param line2 If you have configured your recent suggestions provider with
    155      * {@link android.content.SearchRecentSuggestionsProvider#DATABASE_MODE_2LINES}, you can
    156      * pass a second line of text here.  It will be shown in a smaller font, below the primary
    157      * suggestion.  When typing, matches in either line of text will be displayed in the list.
    158      * If you did not configure two-line mode, or if a given suggestion does not have any
    159      * additional text to display, you can pass null here.
    160      */
    161     public void saveRecentQuery(final String queryString, final String line2) {
    162         if (TextUtils.isEmpty(queryString)) {
    163             return;
    164         }
    165         if (!mTwoLineDisplay && !TextUtils.isEmpty(line2)) {
    166             throw new IllegalArgumentException();
    167         }
    168 
    169         new Thread("saveRecentQuery") {
    170             @Override
    171             public void run() {
    172                 saveRecentQueryBlocking(queryString, line2);
    173                 sWritesInProgress.release();
    174             }
    175         }.start();
    176     }
    177 
    178     // Visible for testing.
    179     void waitForSave() {
    180         // Acquire writes semaphore until there is nothing available.
    181         // This is to clean up after any previous callers to saveRecentQuery
    182         // who did not also call waitForSave().
    183         do {
    184             sWritesInProgress.acquireUninterruptibly();
    185         } while (sWritesInProgress.availablePermits() > 0);
    186     }
    187 
    188     private void saveRecentQueryBlocking(String queryString, String line2) {
    189         ContentResolver cr = mContext.getContentResolver();
    190         long now = System.currentTimeMillis();
    191 
    192         // Use content resolver (not cursor) to insert/update this query
    193         try {
    194             ContentValues values = new ContentValues();
    195             values.put(SuggestionColumns.DISPLAY1, queryString);
    196             if (mTwoLineDisplay) {
    197                 values.put(SuggestionColumns.DISPLAY2, line2);
    198             }
    199             values.put(SuggestionColumns.QUERY, queryString);
    200             values.put(SuggestionColumns.DATE, now);
    201             cr.insert(mSuggestionsUri, values);
    202         } catch (RuntimeException e) {
    203             Log.e(LOG_TAG, "saveRecentQuery", e);
    204         }
    205 
    206         // Shorten the list (if it has become too long)
    207         truncateHistory(cr, MAX_HISTORY_COUNT);
    208     }
    209 
    210     /**
    211      * Completely delete the history.  Use this call to implement a "clear history" UI.
    212      *
    213      * Any application that implements search suggestions based on previous actions (such as
    214      * recent queries, page/items viewed, etc.) should provide a way for the user to clear the
    215      * history.  This gives the user a measure of privacy, if they do not wish for their recent
    216      * searches to be replayed by other users of the device (via suggestions).
    217      */
    218     public void clearHistory() {
    219         ContentResolver cr = mContext.getContentResolver();
    220         truncateHistory(cr, 0);
    221     }
    222 
    223     /**
    224      * Reduces the length of the history table, to prevent it from growing too large.
    225      *
    226      * @param cr Convenience copy of the content resolver.
    227      * @param maxEntries Max entries to leave in the table. 0 means remove all entries.
    228      */
    229     protected void truncateHistory(ContentResolver cr, int maxEntries) {
    230         if (maxEntries < 0) {
    231             throw new IllegalArgumentException();
    232         }
    233 
    234         try {
    235             // null means "delete all".  otherwise "delete but leave n newest"
    236             String selection = null;
    237             if (maxEntries > 0) {
    238                 selection = "_id IN " +
    239                         "(SELECT _id FROM suggestions" +
    240                         " ORDER BY " + SuggestionColumns.DATE + " DESC" +
    241                         " LIMIT -1 OFFSET " + String.valueOf(maxEntries) + ")";
    242             }
    243             cr.delete(mSuggestionsUri, selection, null);
    244         } catch (RuntimeException e) {
    245             Log.e(LOG_TAG, "truncateHistory", e);
    246         }
    247     }
    248 }
    249