blob: c165487db6ff5dbb2df3cc878bd0495859ea006a [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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
17package android.provider;
18
19import android.content.ContentResolver;
20import android.content.ContentValues;
21import android.content.Context;
22import android.content.SearchRecentSuggestionsProvider;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080023import android.net.Uri;
24import android.text.TextUtils;
25import android.util.Log;
26
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -070027import java.util.concurrent.Semaphore;
28
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080029/**
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -070030 * This is a utility class providing access to
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080031 * {@link android.content.SearchRecentSuggestionsProvider}.
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -070032 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080033 * <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.
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -070035 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080036 * <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">
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -070040 * SearchSuggestions suggestions = new SearchSuggestions(this,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080041 * MySuggestionsProvider.AUTHORITY, MySuggestionsProvider.MODE);
42 * suggestions.saveRecentQuery(queryString, null);
43 * </pre>
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -070044 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080045 * <p>For a working example, see SearchSuggestionSampleProvider and SearchQueryResults in
46 * samples/ApiDemos/app.
Joe Fernandez3aef8e1d2011-12-20 10:38:34 -080047 *
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>
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080054 */
55public class SearchRecentSuggestions {
56 // debugging support
57 private static final String LOG_TAG = "SearchSuggestions";
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -070058
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080059 // 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 }
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -070066
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080067 /* 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[] {
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -070073 SuggestionColumns._ID,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080074 SuggestionColumns.DATE,
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -070075 SuggestionColumns.QUERY,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080076 SuggestionColumns.DISPLAY1,
77 };
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -070078
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080079 /* 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[] {
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -070085 SuggestionColumns._ID,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080086 SuggestionColumns.DATE,
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -070087 SuggestionColumns.QUERY,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080088 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
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800101
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;
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -0700108
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800109 // client-provided configuration values
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -0700110 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);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800117
118 /**
119 * Although provider utility classes are typically static, this one must be constructed
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -0700120 * because it needs to be initialized using the same values that you provided in your
121 * {@link android.content.SearchRecentSuggestionsProvider}.
122 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800123 * @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.
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -0700127 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800128 * @see android.content.SearchRecentSuggestionsProvider
129 * @see android.content.SearchRecentSuggestionsProvider#setupSuggestions
130 */
131 public SearchRecentSuggestions(Context context, String authority, int mode) {
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -0700132 if (TextUtils.isEmpty(authority) ||
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800133 ((mode & SearchRecentSuggestionsProvider.DATABASE_MODE_QUERIES) == 0)) {
134 throw new IllegalArgumentException();
135 }
136 // unpack mode flags
137 mTwoLineDisplay = (0 != (mode & SearchRecentSuggestionsProvider.DATABASE_MODE_2LINES));
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -0700138
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800139 // saved values
140 mContext = context;
141 mAuthority = new String(authority);
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -0700142
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800143 // derived values
144 mSuggestionsUri = Uri.parse("content://" + mAuthority + "/suggestions");
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800145 }
146
147 /**
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -0700148 * Add a query to the recent queries list. Returns immediately, performing the save
149 * in the background.
150 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800151 * @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).
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -0700154 * @param line2 If you have configured your recent suggestions provider with
155 * {@link android.content.SearchRecentSuggestionsProvider#DATABASE_MODE_2LINES}, you can
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800156 * 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 */
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -0700161 public void saveRecentQuery(final String queryString, final String line2) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800162 if (TextUtils.isEmpty(queryString)) {
163 return;
164 }
165 if (!mTwoLineDisplay && !TextUtils.isEmpty(line2)) {
166 throw new IllegalArgumentException();
167 }
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -0700168
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) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800189 ContentResolver cr = mContext.getContentResolver();
190 long now = System.currentTimeMillis();
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -0700191
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800192 // 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 }
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -0700205
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800206 // Shorten the list (if it has become too long)
207 truncateHistory(cr, MAX_HISTORY_COUNT);
208 }
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -0700209
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800210 /**
211 * Completely delete the history. Use this call to implement a "clear history" UI.
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -0700212 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800213 * 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.
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -0700225 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800226 * @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 }
Ficus Kirkpatrick553a53e2010-11-03 17:38:49 -0700233
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800234 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}