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 */ SearchRecentSuggestions(Context context, String authority, int mode)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 */ saveRecentQuery(final String queryString, final String line2)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. waitForSave()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 saveRecentQueryBlocking(String queryString, String line2)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 */ clearHistory()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 */ truncateHistory(ContentResolver cr, int maxEntries)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