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.content; 18 19import android.app.SearchManager; 20import android.database.Cursor; 21import android.database.sqlite.SQLiteDatabase; 22import android.database.sqlite.SQLiteOpenHelper; 23import android.net.Uri; 24import android.text.TextUtils; 25import android.util.Log; 26 27/** 28 * This superclass can be used to create a simple search suggestions provider for your application. 29 * It creates suggestions (as the user types) based on recent queries and/or recent views. 30 * 31 * <p>In order to use this class, you must do the following. 32 * 33 * <ul> 34 * <li>Implement and test query search, as described in {@link android.app.SearchManager}. (This 35 * provider will send any suggested queries via the standard 36 * {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH} Intent, which you'll already 37 * support once you have implemented and tested basic searchability.)</li> 38 * <li>Create a Content Provider within your application by extending 39 * {@link android.content.SearchRecentSuggestionsProvider}. The class you create will be 40 * very simple - typically, it will have only a constructor. But the constructor has a very 41 * important responsibility: When it calls {@link #setupSuggestions(String, int)}, it 42 * <i>configures</i> the provider to match the requirements of your searchable activity.</li> 43 * <li>Create a manifest entry describing your provider. Typically this would be as simple 44 * as adding the following lines: 45 * <pre class="prettyprint"> 46 * <!-- Content provider for search suggestions --> 47 * <provider android:name="YourSuggestionProviderClass" 48 * android:authorities="your.suggestion.authority" /></pre> 49 * </li> 50 * <li>Please note that you <i>do not</i> instantiate this content provider directly from within 51 * your code. This is done automatically by the system Content Resolver, when the search dialog 52 * looks for suggestions.</li> 53 * <li>In order for the Content Resolver to do this, you must update your searchable activity's 54 * XML configuration file with information about your content provider. The following additions 55 * are usually sufficient: 56 * <pre class="prettyprint"> 57 * android:searchSuggestAuthority="your.suggestion.authority" 58 * android:searchSuggestSelection=" ? "</pre> 59 * </li> 60 * <li>In your searchable activities, capture any user-generated queries and record them 61 * for future searches by calling {@link android.provider.SearchRecentSuggestions#saveRecentQuery 62 * SearchRecentSuggestions.saveRecentQuery()}.</li> 63 * </ul> 64 * 65 * <div class="special reference"> 66 * <h3>Developer Guides</h3> 67 * <p>For information about using search suggestions in your application, read the 68 * <a href="{@docRoot}guide/topics/search/index.html">Search</a> developer guide.</p> 69 * </div> 70 * 71 * @see android.provider.SearchRecentSuggestions 72 */ 73public class SearchRecentSuggestionsProvider extends ContentProvider { 74 // debugging support 75 private static final String TAG = "SuggestionsProvider"; 76 77 // client-provided configuration values 78 private String mAuthority; 79 private int mMode; 80 private boolean mTwoLineDisplay; 81 82 // general database configuration and tables 83 private SQLiteOpenHelper mOpenHelper; 84 private static final String sDatabaseName = "suggestions.db"; 85 private static final String sSuggestions = "suggestions"; 86 private static final String ORDER_BY = "date DESC"; 87 private static final String NULL_COLUMN = "query"; 88 89 // Table of database versions. Don't forget to update! 90 // NOTE: These version values are shifted left 8 bits (x 256) in order to create space for 91 // a small set of mode bitflags in the version int. 92 // 93 // 1 original implementation with queries, and 1 or 2 display columns 94 // 1->2 added UNIQUE constraint to display1 column 95 private static final int DATABASE_VERSION = 2 * 256; 96 97 /** 98 * This mode bit configures the database to record recent queries. <i>required</i> 99 * 100 * @see #setupSuggestions(String, int) 101 */ 102 public static final int DATABASE_MODE_QUERIES = 1; 103 /** 104 * This mode bit configures the database to include a 2nd annotation line with each entry. 105 * <i>optional</i> 106 * 107 * @see #setupSuggestions(String, int) 108 */ 109 public static final int DATABASE_MODE_2LINES = 2; 110 111 // Uri and query support 112 private static final int URI_MATCH_SUGGEST = 1; 113 114 private Uri mSuggestionsUri; 115 private UriMatcher mUriMatcher; 116 117 private String mSuggestSuggestionClause; 118 private String[] mSuggestionProjection; 119 120 /** 121 * Builds the database. This version has extra support for using the version field 122 * as a mode flags field, and configures the database columns depending on the mode bits 123 * (features) requested by the extending class. 124 * 125 * @hide 126 */ 127 private static class DatabaseHelper extends SQLiteOpenHelper { 128 129 private int mNewVersion; 130 131 public DatabaseHelper(Context context, int newVersion) { 132 super(context, sDatabaseName, null, newVersion); 133 mNewVersion = newVersion; 134 } 135 136 @Override 137 public void onCreate(SQLiteDatabase db) { 138 StringBuilder builder = new StringBuilder(); 139 builder.append("CREATE TABLE suggestions (" + 140 "_id INTEGER PRIMARY KEY" + 141 ",display1 TEXT UNIQUE ON CONFLICT REPLACE"); 142 if (0 != (mNewVersion & DATABASE_MODE_2LINES)) { 143 builder.append(",display2 TEXT"); 144 } 145 builder.append(",query TEXT" + 146 ",date LONG" + 147 ");"); 148 db.execSQL(builder.toString()); 149 } 150 151 @Override 152 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 153 Log.w(TAG, "Upgrading database from version " + oldVersion + " to " 154 + newVersion + ", which will destroy all old data"); 155 db.execSQL("DROP TABLE IF EXISTS suggestions"); 156 onCreate(db); 157 } 158 } 159 160 /** 161 * In order to use this class, you must extend it, and call this setup function from your 162 * constructor. In your application or activities, you must provide the same values when 163 * you create the {@link android.provider.SearchRecentSuggestions} helper. 164 * 165 * @param authority This must match the authority that you've declared in your manifest. 166 * @param mode You can use mode flags here to determine certain functional aspects of your 167 * database. Note, this value should not change from run to run, because when it does change, 168 * your suggestions database may be wiped. 169 * 170 * @see #DATABASE_MODE_QUERIES 171 * @see #DATABASE_MODE_2LINES 172 */ 173 protected void setupSuggestions(String authority, int mode) { 174 if (TextUtils.isEmpty(authority) || 175 ((mode & DATABASE_MODE_QUERIES) == 0)) { 176 throw new IllegalArgumentException(); 177 } 178 // unpack mode flags 179 mTwoLineDisplay = (0 != (mode & DATABASE_MODE_2LINES)); 180 181 // saved values 182 mAuthority = new String(authority); 183 mMode = mode; 184 185 // derived values 186 mSuggestionsUri = Uri.parse("content://" + mAuthority + "/suggestions"); 187 mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 188 mUriMatcher.addURI(mAuthority, SearchManager.SUGGEST_URI_PATH_QUERY, URI_MATCH_SUGGEST); 189 190 if (mTwoLineDisplay) { 191 mSuggestSuggestionClause = "display1 LIKE ? OR display2 LIKE ?"; 192 193 mSuggestionProjection = new String [] { 194 "0 AS " + SearchManager.SUGGEST_COLUMN_FORMAT, 195 "'android.resource://system/" 196 + com.android.internal.R.drawable.ic_menu_recent_history + "' AS " 197 + SearchManager.SUGGEST_COLUMN_ICON_1, 198 "display1 AS " + SearchManager.SUGGEST_COLUMN_TEXT_1, 199 "display2 AS " + SearchManager.SUGGEST_COLUMN_TEXT_2, 200 "query AS " + SearchManager.SUGGEST_COLUMN_QUERY, 201 "_id" 202 }; 203 } else { 204 mSuggestSuggestionClause = "display1 LIKE ?"; 205 206 mSuggestionProjection = new String [] { 207 "0 AS " + SearchManager.SUGGEST_COLUMN_FORMAT, 208 "'android.resource://system/" 209 + com.android.internal.R.drawable.ic_menu_recent_history + "' AS " 210 + SearchManager.SUGGEST_COLUMN_ICON_1, 211 "display1 AS " + SearchManager.SUGGEST_COLUMN_TEXT_1, 212 "query AS " + SearchManager.SUGGEST_COLUMN_QUERY, 213 "_id" 214 }; 215 } 216 217 218 } 219 220 /** 221 * This method is provided for use by the ContentResolver. Do not override, or directly 222 * call from your own code. 223 */ 224 @Override 225 public int delete(Uri uri, String selection, String[] selectionArgs) { 226 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 227 228 final int length = uri.getPathSegments().size(); 229 if (length != 1) { 230 throw new IllegalArgumentException("Unknown Uri"); 231 } 232 233 final String base = uri.getPathSegments().get(0); 234 int count = 0; 235 if (base.equals(sSuggestions)) { 236 count = db.delete(sSuggestions, selection, selectionArgs); 237 } else { 238 throw new IllegalArgumentException("Unknown Uri"); 239 } 240 getContext().getContentResolver().notifyChange(uri, null); 241 return count; 242 } 243 244 /** 245 * This method is provided for use by the ContentResolver. Do not override, or directly 246 * call from your own code. 247 */ 248 @Override 249 public String getType(Uri uri) { 250 if (mUriMatcher.match(uri) == URI_MATCH_SUGGEST) { 251 return SearchManager.SUGGEST_MIME_TYPE; 252 } 253 int length = uri.getPathSegments().size(); 254 if (length >= 1) { 255 String base = uri.getPathSegments().get(0); 256 if (base.equals(sSuggestions)) { 257 if (length == 1) { 258 return "vnd.android.cursor.dir/suggestion"; 259 } else if (length == 2) { 260 return "vnd.android.cursor.item/suggestion"; 261 } 262 } 263 } 264 throw new IllegalArgumentException("Unknown Uri"); 265 } 266 267 /** 268 * This method is provided for use by the ContentResolver. Do not override, or directly 269 * call from your own code. 270 */ 271 @Override 272 public Uri insert(Uri uri, ContentValues values) { 273 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 274 275 int length = uri.getPathSegments().size(); 276 if (length < 1) { 277 throw new IllegalArgumentException("Unknown Uri"); 278 } 279 // Note: This table has on-conflict-replace semantics, so insert() may actually replace() 280 long rowID = -1; 281 String base = uri.getPathSegments().get(0); 282 Uri newUri = null; 283 if (base.equals(sSuggestions)) { 284 if (length == 1) { 285 rowID = db.insert(sSuggestions, NULL_COLUMN, values); 286 if (rowID > 0) { 287 newUri = Uri.withAppendedPath(mSuggestionsUri, String.valueOf(rowID)); 288 } 289 } 290 } 291 if (rowID < 0) { 292 throw new IllegalArgumentException("Unknown Uri"); 293 } 294 getContext().getContentResolver().notifyChange(newUri, null); 295 return newUri; 296 } 297 298 /** 299 * This method is provided for use by the ContentResolver. Do not override, or directly 300 * call from your own code. 301 */ 302 @Override 303 public boolean onCreate() { 304 if (mAuthority == null || mMode == 0) { 305 throw new IllegalArgumentException("Provider not configured"); 306 } 307 int mWorkingDbVersion = DATABASE_VERSION + mMode; 308 mOpenHelper = new DatabaseHelper(getContext(), mWorkingDbVersion); 309 310 return true; 311 } 312 313 /** 314 * This method is provided for use by the ContentResolver. Do not override, or directly 315 * call from your own code. 316 */ 317 // TODO: Confirm no injection attacks here, or rewrite. 318 @Override 319 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 320 String sortOrder) { 321 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 322 323 // special case for actual suggestions (from search manager) 324 if (mUriMatcher.match(uri) == URI_MATCH_SUGGEST) { 325 String suggestSelection; 326 String[] myArgs; 327 if (TextUtils.isEmpty(selectionArgs[0])) { 328 suggestSelection = null; 329 myArgs = null; 330 } else { 331 String like = "%" + selectionArgs[0] + "%"; 332 if (mTwoLineDisplay) { 333 myArgs = new String [] { like, like }; 334 } else { 335 myArgs = new String [] { like }; 336 } 337 suggestSelection = mSuggestSuggestionClause; 338 } 339 // Suggestions are always performed with the default sort order 340 Cursor c = db.query(sSuggestions, mSuggestionProjection, 341 suggestSelection, myArgs, null, null, ORDER_BY, null); 342 c.setNotificationUri(getContext().getContentResolver(), uri); 343 return c; 344 } 345 346 // otherwise process arguments and perform a standard query 347 int length = uri.getPathSegments().size(); 348 if (length != 1 && length != 2) { 349 throw new IllegalArgumentException("Unknown Uri"); 350 } 351 352 String base = uri.getPathSegments().get(0); 353 if (!base.equals(sSuggestions)) { 354 throw new IllegalArgumentException("Unknown Uri"); 355 } 356 357 String[] useProjection = null; 358 if (projection != null && projection.length > 0) { 359 useProjection = new String[projection.length + 1]; 360 System.arraycopy(projection, 0, useProjection, 0, projection.length); 361 useProjection[projection.length] = "_id AS _id"; 362 } 363 364 StringBuilder whereClause = new StringBuilder(256); 365 if (length == 2) { 366 whereClause.append("(_id = ").append(uri.getPathSegments().get(1)).append(")"); 367 } 368 369 // Tack on the user's selection, if present 370 if (selection != null && selection.length() > 0) { 371 if (whereClause.length() > 0) { 372 whereClause.append(" AND "); 373 } 374 375 whereClause.append('('); 376 whereClause.append(selection); 377 whereClause.append(')'); 378 } 379 380 // And perform the generic query as requested 381 Cursor c = db.query(base, useProjection, whereClause.toString(), 382 selectionArgs, null, null, sortOrder, 383 null); 384 c.setNotificationUri(getContext().getContentResolver(), uri); 385 return c; 386 } 387 388 /** 389 * This method is provided for use by the ContentResolver. Do not override, or directly 390 * call from your own code. 391 */ 392 @Override 393 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 394 throw new UnsupportedOperationException("Not implemented"); 395 } 396 397} 398