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 17package android.provider; 18 19import android.content.ContentResolver; 20import android.content.ContentValues; 21import android.content.Context; 22import android.content.SearchRecentSuggestionsProvider; 23import android.database.Cursor; 24import android.net.Uri; 25import android.text.TextUtils; 26import android.util.Log; 27 28/** 29 * This is a utility class providing access to 30 * {@link android.content.SearchRecentSuggestionsProvider}. 31 * 32 * <p>Unlike some utility classes, this one must be instantiated and properly initialized, so that 33 * it can be configured to operate with the search suggestions provider that you have created. 34 * 35 * <p>Typically, you will do this in your searchable activity, each time you receive an incoming 36 * {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH} Intent. The code to record each 37 * incoming query is as follows: 38 * <pre class="prettyprint"> 39 * SearchSuggestions suggestions = new SearchSuggestions(this, 40 * MySuggestionsProvider.AUTHORITY, MySuggestionsProvider.MODE); 41 * suggestions.saveRecentQuery(queryString, null); 42 * </pre> 43 * 44 * <p>For a working example, see SearchSuggestionSampleProvider and SearchQueryResults in 45 * samples/ApiDemos/app. 46 */ 47public class SearchRecentSuggestions { 48 // debugging support 49 private static final String LOG_TAG = "SearchSuggestions"; 50 // DELETE ME (eventually) 51 private static final int DBG_SUGGESTION_TIMESTAMPS = 0; 52 53 // This is a superset of all possible column names (need not all be in table) 54 private static class SuggestionColumns implements BaseColumns { 55 public static final String DISPLAY1 = "display1"; 56 public static final String DISPLAY2 = "display2"; 57 public static final String QUERY = "query"; 58 public static final String DATE = "date"; 59 } 60 61 /* if you change column order you must also change indices below */ 62 /** 63 * This is the database projection that can be used to view saved queries, when 64 * configured for one-line operation. 65 */ 66 public static final String[] QUERIES_PROJECTION_1LINE = new String[] { 67 SuggestionColumns._ID, 68 SuggestionColumns.DATE, 69 SuggestionColumns.QUERY, 70 SuggestionColumns.DISPLAY1, 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 /* columns needed to determine whether to truncate history */ 96 private static final String[] TRUNCATE_HISTORY_PROJECTION = new String[] { 97 SuggestionColumns._ID, SuggestionColumns.DATE 98 }; 99 100 /* 101 * Set a cap on the count of items in the suggestions table, to 102 * prevent db and layout operations from dragging to a crawl. Revisit this 103 * cap when/if db/layout performance improvements are made. 104 */ 105 private static final int MAX_HISTORY_COUNT = 250; 106 107 // client-provided configuration values 108 private Context mContext; 109 private String mAuthority; 110 private boolean mTwoLineDisplay; 111 private Uri mSuggestionsUri; 112 private String[] mQueriesProjection; 113 114 /** 115 * Although provider utility classes are typically static, this one must be constructed 116 * because it needs to be initialized using the same values that you provided in your 117 * {@link android.content.SearchRecentSuggestionsProvider}. 118 * 119 * @param authority This must match the authority that you've declared in your manifest. 120 * @param mode You can use mode flags here to determine certain functional aspects of your 121 * database. Note, this value should not change from run to run, because when it does change, 122 * your suggestions database may be wiped. 123 * 124 * @see android.content.SearchRecentSuggestionsProvider 125 * @see android.content.SearchRecentSuggestionsProvider#setupSuggestions 126 */ 127 public SearchRecentSuggestions(Context context, String authority, int mode) { 128 if (TextUtils.isEmpty(authority) || 129 ((mode & SearchRecentSuggestionsProvider.DATABASE_MODE_QUERIES) == 0)) { 130 throw new IllegalArgumentException(); 131 } 132 // unpack mode flags 133 mTwoLineDisplay = (0 != (mode & SearchRecentSuggestionsProvider.DATABASE_MODE_2LINES)); 134 135 // saved values 136 mContext = context; 137 mAuthority = new String(authority); 138 139 // derived values 140 mSuggestionsUri = Uri.parse("content://" + mAuthority + "/suggestions"); 141 142 if (mTwoLineDisplay) { 143 mQueriesProjection = QUERIES_PROJECTION_2LINE; 144 } else { 145 mQueriesProjection = QUERIES_PROJECTION_1LINE; 146 } 147 } 148 149 /** 150 * Add a query to the recent queries list. 151 * 152 * @param queryString The string as typed by the user. This string will be displayed as 153 * the suggestion, and if the user clicks on the suggestion, this string will be sent to your 154 * searchable activity (as a new search query). 155 * @param line2 If you have configured your recent suggestions provider with 156 * {@link android.content.SearchRecentSuggestionsProvider#DATABASE_MODE_2LINES}, you can 157 * pass a second line of text here. It will be shown in a smaller font, below the primary 158 * suggestion. When typing, matches in either line of text will be displayed in the list. 159 * If you did not configure two-line mode, or if a given suggestion does not have any 160 * additional text to display, you can pass null here. 161 */ 162 public void saveRecentQuery(String queryString, String line2) { 163 if (TextUtils.isEmpty(queryString)) { 164 return; 165 } 166 if (!mTwoLineDisplay && !TextUtils.isEmpty(line2)) { 167 throw new IllegalArgumentException(); 168 } 169 170 ContentResolver cr = mContext.getContentResolver(); 171 long now = System.currentTimeMillis(); 172 173 // Use content resolver (not cursor) to insert/update this query 174 try { 175 ContentValues values = new ContentValues(); 176 values.put(SuggestionColumns.DISPLAY1, queryString); 177 if (mTwoLineDisplay) { 178 values.put(SuggestionColumns.DISPLAY2, line2); 179 } 180 values.put(SuggestionColumns.QUERY, queryString); 181 values.put(SuggestionColumns.DATE, now); 182 cr.insert(mSuggestionsUri, values); 183 } catch (RuntimeException e) { 184 Log.e(LOG_TAG, "saveRecentQuery", e); 185 } 186 187 // Shorten the list (if it has become too long) 188 truncateHistory(cr, MAX_HISTORY_COUNT); 189 } 190 191 /** 192 * Completely delete the history. Use this call to implement a "clear history" UI. 193 * 194 * Any application that implements search suggestions based on previous actions (such as 195 * recent queries, page/items viewed, etc.) should provide a way for the user to clear the 196 * history. This gives the user a measure of privacy, if they do not wish for their recent 197 * searches to be replayed by other users of the device (via suggestions). 198 */ 199 public void clearHistory() { 200 ContentResolver cr = mContext.getContentResolver(); 201 truncateHistory(cr, 0); 202 } 203 204 /** 205 * Reduces the length of the history table, to prevent it from growing too large. 206 * 207 * @param cr Convenience copy of the content resolver. 208 * @param maxEntries Max entries to leave in the table. 0 means remove all entries. 209 */ 210 protected void truncateHistory(ContentResolver cr, int maxEntries) { 211 if (maxEntries < 0) { 212 throw new IllegalArgumentException(); 213 } 214 215 try { 216 // null means "delete all". otherwise "delete but leave n newest" 217 String selection = null; 218 if (maxEntries > 0) { 219 selection = "_id IN " + 220 "(SELECT _id FROM suggestions" + 221 " ORDER BY " + SuggestionColumns.DATE + " DESC" + 222 " LIMIT -1 OFFSET " + String.valueOf(maxEntries) + ")"; 223 } 224 cr.delete(mSuggestionsUri, selection, null); 225 } catch (RuntimeException e) { 226 Log.e(LOG_TAG, "truncateHistory", e); 227 } 228 } 229} 230