• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 public class SearchRecentSuggestions {
49     // debugging support
50     private static final String LOG_TAG = "SearchSuggestions";
51 
52     // This is a superset of all possible column names (need not all be in table)
53     private static class SuggestionColumns implements BaseColumns {
54         public static final String DISPLAY1 = "display1";
55         public static final String DISPLAY2 = "display2";
56         public static final String QUERY = "query";
57         public static final String DATE = "date";
58     }
59 
60     /* if you change column order you must also change indices below */
61     /**
62      * This is the database projection that can be used to view saved queries, when
63      * configured for one-line operation.
64      */
65     public static final String[] QUERIES_PROJECTION_1LINE = new String[] {
66         SuggestionColumns._ID,
67         SuggestionColumns.DATE,
68         SuggestionColumns.QUERY,
69         SuggestionColumns.DISPLAY1,
70     };
71 
72     /* if you change column order you must also change indices below */
73     /**
74      * This is the database projection that can be used to view saved queries, when
75      * configured for two-line operation.
76      */
77     public static final String[] QUERIES_PROJECTION_2LINE = new String[] {
78         SuggestionColumns._ID,
79         SuggestionColumns.DATE,
80         SuggestionColumns.QUERY,
81         SuggestionColumns.DISPLAY1,
82         SuggestionColumns.DISPLAY2,
83     };
84 
85     /* these indices depend on QUERIES_PROJECTION_xxx */
86     /** Index into the provided query projections.  For use with Cursor.update methods. */
87     public static final int QUERIES_PROJECTION_DATE_INDEX = 1;
88     /** Index into the provided query projections.  For use with Cursor.update methods. */
89     public static final int QUERIES_PROJECTION_QUERY_INDEX = 2;
90     /** Index into the provided query projections.  For use with Cursor.update methods. */
91     public static final int QUERIES_PROJECTION_DISPLAY1_INDEX = 3;
92     /** Index into the provided query projections.  For use with Cursor.update methods. */
93     public static final int QUERIES_PROJECTION_DISPLAY2_INDEX = 4;  // only when 2line active
94 
95     /*
96      * Set a cap on the count of items in the suggestions table, to
97      * prevent db and layout operations from dragging to a crawl. Revisit this
98      * cap when/if db/layout performance improvements are made.
99      */
100     private static final int MAX_HISTORY_COUNT = 250;
101 
102     // client-provided configuration values
103     private final Context mContext;
104     private final String mAuthority;
105     private final boolean mTwoLineDisplay;
106     private final Uri mSuggestionsUri;
107 
108     /** Released once per completion of async write.  Used for tests. */
109     private static final Semaphore sWritesInProgress = new Semaphore(0);
110 
111     /**
112      * Although provider utility classes are typically static, this one must be constructed
113      * because it needs to be initialized using the same values that you provided in your
114      * {@link android.content.SearchRecentSuggestionsProvider}.
115      *
116      * @param authority This must match the authority that you've declared in your manifest.
117      * @param mode You can use mode flags here to determine certain functional aspects of your
118      * database.  Note, this value should not change from run to run, because when it does change,
119      * your suggestions database may be wiped.
120      *
121      * @see android.content.SearchRecentSuggestionsProvider
122      * @see android.content.SearchRecentSuggestionsProvider#setupSuggestions
123      */
SearchRecentSuggestions(Context context, String authority, int mode)124     public SearchRecentSuggestions(Context context, String authority, int mode) {
125         if (TextUtils.isEmpty(authority) ||
126                 ((mode & SearchRecentSuggestionsProvider.DATABASE_MODE_QUERIES) == 0)) {
127             throw new IllegalArgumentException();
128         }
129         // unpack mode flags
130         mTwoLineDisplay = (0 != (mode & SearchRecentSuggestionsProvider.DATABASE_MODE_2LINES));
131 
132         // saved values
133         mContext = context;
134         mAuthority = new String(authority);
135 
136         // derived values
137         mSuggestionsUri = Uri.parse("content://" + mAuthority + "/suggestions");
138     }
139 
140     /**
141      * Add a query to the recent queries list.  Returns immediately, performing the save
142      * in the background.
143      *
144      * @param queryString The string as typed by the user.  This string will be displayed as
145      * the suggestion, and if the user clicks on the suggestion, this string will be sent to your
146      * searchable activity (as a new search query).
147      * @param line2 If you have configured your recent suggestions provider with
148      * {@link android.content.SearchRecentSuggestionsProvider#DATABASE_MODE_2LINES}, you can
149      * pass a second line of text here.  It will be shown in a smaller font, below the primary
150      * suggestion.  When typing, matches in either line of text will be displayed in the list.
151      * If you did not configure two-line mode, or if a given suggestion does not have any
152      * additional text to display, you can pass null here.
153      */
saveRecentQuery(final String queryString, final String line2)154     public void saveRecentQuery(final String queryString, final String line2) {
155         if (TextUtils.isEmpty(queryString)) {
156             return;
157         }
158         if (!mTwoLineDisplay && !TextUtils.isEmpty(line2)) {
159             throw new IllegalArgumentException();
160         }
161 
162         new Thread("saveRecentQuery") {
163             @Override
164             public void run() {
165                 saveRecentQueryBlocking(queryString, line2);
166                 sWritesInProgress.release();
167             }
168         }.start();
169     }
170 
171     // Visible for testing.
waitForSave()172     void waitForSave() {
173         // Acquire writes semaphore until there is nothing available.
174         // This is to clean up after any previous callers to saveRecentQuery
175         // who did not also call waitForSave().
176         do {
177             sWritesInProgress.acquireUninterruptibly();
178         } while (sWritesInProgress.availablePermits() > 0);
179     }
180 
saveRecentQueryBlocking(String queryString, String line2)181     private void saveRecentQueryBlocking(String queryString, String line2) {
182         ContentResolver cr = mContext.getContentResolver();
183         long now = System.currentTimeMillis();
184 
185         // Use content resolver (not cursor) to insert/update this query
186         try {
187             ContentValues values = new ContentValues();
188             values.put(SuggestionColumns.DISPLAY1, queryString);
189             if (mTwoLineDisplay) {
190                 values.put(SuggestionColumns.DISPLAY2, line2);
191             }
192             values.put(SuggestionColumns.QUERY, queryString);
193             values.put(SuggestionColumns.DATE, now);
194             cr.insert(mSuggestionsUri, values);
195         } catch (RuntimeException e) {
196             Log.e(LOG_TAG, "saveRecentQuery", e);
197         }
198 
199         // Shorten the list (if it has become too long)
200         truncateHistory(cr, MAX_HISTORY_COUNT);
201     }
202 
203     /**
204      * Completely delete the history.  Use this call to implement a "clear history" UI.
205      *
206      * Any application that implements search suggestions based on previous actions (such as
207      * recent queries, page/items viewed, etc.) should provide a way for the user to clear the
208      * history.  This gives the user a measure of privacy, if they do not wish for their recent
209      * searches to be replayed by other users of the device (via suggestions).
210      */
clearHistory()211     public void clearHistory() {
212         ContentResolver cr = mContext.getContentResolver();
213         truncateHistory(cr, 0);
214     }
215 
216     /**
217      * Reduces the length of the history table, to prevent it from growing too large.
218      *
219      * @param cr Convenience copy of the content resolver.
220      * @param maxEntries Max entries to leave in the table. 0 means remove all entries.
221      */
truncateHistory(ContentResolver cr, int maxEntries)222     protected void truncateHistory(ContentResolver cr, int maxEntries) {
223         if (maxEntries < 0) {
224             throw new IllegalArgumentException();
225         }
226 
227         try {
228             // null means "delete all".  otherwise "delete but leave n newest"
229             String selection = null;
230             if (maxEntries > 0) {
231                 selection = "_id IN " +
232                         "(SELECT _id FROM suggestions" +
233                         " ORDER BY " + SuggestionColumns.DATE + " DESC" +
234                         " LIMIT -1 OFFSET " + String.valueOf(maxEntries) + ")";
235             }
236             cr.delete(mSuggestionsUri, selection, null);
237         } catch (RuntimeException e) {
238             Log.e(LOG_TAG, "truncateHistory", e);
239         }
240     }
241 }
242