| /* |
| * Copyright (C) 2008 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.content; |
| |
| import android.annotation.UnsupportedAppUsage; |
| import android.app.SearchManager; |
| import android.database.Cursor; |
| import android.database.sqlite.SQLiteDatabase; |
| import android.database.sqlite.SQLiteOpenHelper; |
| import android.net.Uri; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| /** |
| * This superclass can be used to create a simple search suggestions provider for your application. |
| * It creates suggestions (as the user types) based on recent queries and/or recent views. |
| * |
| * <p>In order to use this class, you must do the following. |
| * |
| * <ul> |
| * <li>Implement and test query search, as described in {@link android.app.SearchManager}. (This |
| * provider will send any suggested queries via the standard |
| * {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH} Intent, which you'll already |
| * support once you have implemented and tested basic searchability.)</li> |
| * <li>Create a Content Provider within your application by extending |
| * {@link android.content.SearchRecentSuggestionsProvider}. The class you create will be |
| * very simple - typically, it will have only a constructor. But the constructor has a very |
| * important responsibility: When it calls {@link #setupSuggestions(String, int)}, it |
| * <i>configures</i> the provider to match the requirements of your searchable activity.</li> |
| * <li>Create a manifest entry describing your provider. Typically this would be as simple |
| * as adding the following lines: |
| * <pre class="prettyprint"> |
| * <!-- Content provider for search suggestions --> |
| * <provider android:name="YourSuggestionProviderClass" |
| * android:authorities="your.suggestion.authority" /></pre> |
| * </li> |
| * <li>Please note that you <i>do not</i> instantiate this content provider directly from within |
| * your code. This is done automatically by the system Content Resolver, when the search dialog |
| * looks for suggestions.</li> |
| * <li>In order for the Content Resolver to do this, you must update your searchable activity's |
| * XML configuration file with information about your content provider. The following additions |
| * are usually sufficient: |
| * <pre class="prettyprint"> |
| * android:searchSuggestAuthority="your.suggestion.authority" |
| * android:searchSuggestSelection=" ? "</pre> |
| * </li> |
| * <li>In your searchable activities, capture any user-generated queries and record them |
| * for future searches by calling {@link android.provider.SearchRecentSuggestions#saveRecentQuery |
| * SearchRecentSuggestions.saveRecentQuery()}.</li> |
| * </ul> |
| * |
| * <div class="special reference"> |
| * <h3>Developer Guides</h3> |
| * <p>For information about using search suggestions in your application, read the |
| * <a href="{@docRoot}guide/topics/search/index.html">Search</a> developer guide.</p> |
| * </div> |
| * |
| * @see android.provider.SearchRecentSuggestions |
| */ |
| public class SearchRecentSuggestionsProvider extends ContentProvider { |
| // debugging support |
| private static final String TAG = "SuggestionsProvider"; |
| |
| // client-provided configuration values |
| private String mAuthority; |
| private int mMode; |
| private boolean mTwoLineDisplay; |
| |
| // general database configuration and tables |
| private SQLiteOpenHelper mOpenHelper; |
| private static final String sDatabaseName = "suggestions.db"; |
| private static final String sSuggestions = "suggestions"; |
| private static final String ORDER_BY = "date DESC"; |
| private static final String NULL_COLUMN = "query"; |
| |
| // Table of database versions. Don't forget to update! |
| // NOTE: These version values are shifted left 8 bits (x 256) in order to create space for |
| // a small set of mode bitflags in the version int. |
| // |
| // 1 original implementation with queries, and 1 or 2 display columns |
| // 1->2 added UNIQUE constraint to display1 column |
| private static final int DATABASE_VERSION = 2 * 256; |
| |
| /** |
| * This mode bit configures the database to record recent queries. <i>required</i> |
| * |
| * @see #setupSuggestions(String, int) |
| */ |
| public static final int DATABASE_MODE_QUERIES = 1; |
| /** |
| * This mode bit configures the database to include a 2nd annotation line with each entry. |
| * <i>optional</i> |
| * |
| * @see #setupSuggestions(String, int) |
| */ |
| public static final int DATABASE_MODE_2LINES = 2; |
| |
| // Uri and query support |
| private static final int URI_MATCH_SUGGEST = 1; |
| |
| private Uri mSuggestionsUri; |
| private UriMatcher mUriMatcher; |
| |
| private String mSuggestSuggestionClause; |
| @UnsupportedAppUsage |
| private String[] mSuggestionProjection; |
| |
| /** |
| * Builds the database. This version has extra support for using the version field |
| * as a mode flags field, and configures the database columns depending on the mode bits |
| * (features) requested by the extending class. |
| * |
| * @hide |
| */ |
| private static class DatabaseHelper extends SQLiteOpenHelper { |
| |
| private int mNewVersion; |
| |
| public DatabaseHelper(Context context, int newVersion) { |
| super(context, sDatabaseName, null, newVersion); |
| mNewVersion = newVersion; |
| } |
| |
| @Override |
| public void onCreate(SQLiteDatabase db) { |
| StringBuilder builder = new StringBuilder(); |
| builder.append("CREATE TABLE suggestions (" + |
| "_id INTEGER PRIMARY KEY" + |
| ",display1 TEXT UNIQUE ON CONFLICT REPLACE"); |
| if (0 != (mNewVersion & DATABASE_MODE_2LINES)) { |
| builder.append(",display2 TEXT"); |
| } |
| builder.append(",query TEXT" + |
| ",date LONG" + |
| ");"); |
| db.execSQL(builder.toString()); |
| } |
| |
| @Override |
| public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { |
| Log.w(TAG, "Upgrading database from version " + oldVersion + " to " |
| + newVersion + ", which will destroy all old data"); |
| db.execSQL("DROP TABLE IF EXISTS suggestions"); |
| onCreate(db); |
| } |
| } |
| |
| /** |
| * In order to use this class, you must extend it, and call this setup function from your |
| * constructor. In your application or activities, you must provide the same values when |
| * you create the {@link android.provider.SearchRecentSuggestions} helper. |
| * |
| * @param authority This must match the authority that you've declared in your manifest. |
| * @param mode You can use mode flags here to determine certain functional aspects of your |
| * database. Note, this value should not change from run to run, because when it does change, |
| * your suggestions database may be wiped. |
| * |
| * @see #DATABASE_MODE_QUERIES |
| * @see #DATABASE_MODE_2LINES |
| */ |
| protected void setupSuggestions(String authority, int mode) { |
| if (TextUtils.isEmpty(authority) || |
| ((mode & DATABASE_MODE_QUERIES) == 0)) { |
| throw new IllegalArgumentException(); |
| } |
| // unpack mode flags |
| mTwoLineDisplay = (0 != (mode & DATABASE_MODE_2LINES)); |
| |
| // saved values |
| mAuthority = new String(authority); |
| mMode = mode; |
| |
| // derived values |
| mSuggestionsUri = Uri.parse("content://" + mAuthority + "/suggestions"); |
| mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); |
| mUriMatcher.addURI(mAuthority, SearchManager.SUGGEST_URI_PATH_QUERY, URI_MATCH_SUGGEST); |
| |
| if (mTwoLineDisplay) { |
| mSuggestSuggestionClause = "display1 LIKE ? OR display2 LIKE ?"; |
| |
| mSuggestionProjection = new String [] { |
| "0 AS " + SearchManager.SUGGEST_COLUMN_FORMAT, |
| "'android.resource://system/" |
| + com.android.internal.R.drawable.ic_menu_recent_history + "' AS " |
| + SearchManager.SUGGEST_COLUMN_ICON_1, |
| "display1 AS " + SearchManager.SUGGEST_COLUMN_TEXT_1, |
| "display2 AS " + SearchManager.SUGGEST_COLUMN_TEXT_2, |
| "query AS " + SearchManager.SUGGEST_COLUMN_QUERY, |
| "_id" |
| }; |
| } else { |
| mSuggestSuggestionClause = "display1 LIKE ?"; |
| |
| mSuggestionProjection = new String [] { |
| "0 AS " + SearchManager.SUGGEST_COLUMN_FORMAT, |
| "'android.resource://system/" |
| + com.android.internal.R.drawable.ic_menu_recent_history + "' AS " |
| + SearchManager.SUGGEST_COLUMN_ICON_1, |
| "display1 AS " + SearchManager.SUGGEST_COLUMN_TEXT_1, |
| "query AS " + SearchManager.SUGGEST_COLUMN_QUERY, |
| "_id" |
| }; |
| } |
| |
| |
| } |
| |
| /** |
| * This method is provided for use by the ContentResolver. Do not override, or directly |
| * call from your own code. |
| */ |
| @Override |
| public int delete(Uri uri, String selection, String[] selectionArgs) { |
| SQLiteDatabase db = mOpenHelper.getWritableDatabase(); |
| |
| final int length = uri.getPathSegments().size(); |
| if (length != 1) { |
| throw new IllegalArgumentException("Unknown Uri"); |
| } |
| |
| final String base = uri.getPathSegments().get(0); |
| int count = 0; |
| if (base.equals(sSuggestions)) { |
| count = db.delete(sSuggestions, selection, selectionArgs); |
| } else { |
| throw new IllegalArgumentException("Unknown Uri"); |
| } |
| getContext().getContentResolver().notifyChange(uri, null); |
| return count; |
| } |
| |
| /** |
| * This method is provided for use by the ContentResolver. Do not override, or directly |
| * call from your own code. |
| */ |
| @Override |
| public String getType(Uri uri) { |
| if (mUriMatcher.match(uri) == URI_MATCH_SUGGEST) { |
| return SearchManager.SUGGEST_MIME_TYPE; |
| } |
| int length = uri.getPathSegments().size(); |
| if (length >= 1) { |
| String base = uri.getPathSegments().get(0); |
| if (base.equals(sSuggestions)) { |
| if (length == 1) { |
| return "vnd.android.cursor.dir/suggestion"; |
| } else if (length == 2) { |
| return "vnd.android.cursor.item/suggestion"; |
| } |
| } |
| } |
| throw new IllegalArgumentException("Unknown Uri"); |
| } |
| |
| /** |
| * This method is provided for use by the ContentResolver. Do not override, or directly |
| * call from your own code. |
| */ |
| @Override |
| public Uri insert(Uri uri, ContentValues values) { |
| SQLiteDatabase db = mOpenHelper.getWritableDatabase(); |
| |
| int length = uri.getPathSegments().size(); |
| if (length < 1) { |
| throw new IllegalArgumentException("Unknown Uri"); |
| } |
| // Note: This table has on-conflict-replace semantics, so insert() may actually replace() |
| long rowID = -1; |
| String base = uri.getPathSegments().get(0); |
| Uri newUri = null; |
| if (base.equals(sSuggestions)) { |
| if (length == 1) { |
| rowID = db.insert(sSuggestions, NULL_COLUMN, values); |
| if (rowID > 0) { |
| newUri = Uri.withAppendedPath(mSuggestionsUri, String.valueOf(rowID)); |
| } |
| } |
| } |
| if (rowID < 0) { |
| throw new IllegalArgumentException("Unknown Uri"); |
| } |
| getContext().getContentResolver().notifyChange(newUri, null); |
| return newUri; |
| } |
| |
| /** |
| * This method is provided for use by the ContentResolver. Do not override, or directly |
| * call from your own code. |
| */ |
| @Override |
| public boolean onCreate() { |
| if (mAuthority == null || mMode == 0) { |
| throw new IllegalArgumentException("Provider not configured"); |
| } |
| int mWorkingDbVersion = DATABASE_VERSION + mMode; |
| mOpenHelper = new DatabaseHelper(getContext(), mWorkingDbVersion); |
| |
| return true; |
| } |
| |
| /** |
| * This method is provided for use by the ContentResolver. Do not override, or directly |
| * call from your own code. |
| */ |
| // TODO: Confirm no injection attacks here, or rewrite. |
| @Override |
| public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, |
| String sortOrder) { |
| SQLiteDatabase db = mOpenHelper.getReadableDatabase(); |
| |
| // special case for actual suggestions (from search manager) |
| if (mUriMatcher.match(uri) == URI_MATCH_SUGGEST) { |
| String suggestSelection; |
| String[] myArgs; |
| if (TextUtils.isEmpty(selectionArgs[0])) { |
| suggestSelection = null; |
| myArgs = null; |
| } else { |
| String like = "%" + selectionArgs[0] + "%"; |
| if (mTwoLineDisplay) { |
| myArgs = new String [] { like, like }; |
| } else { |
| myArgs = new String [] { like }; |
| } |
| suggestSelection = mSuggestSuggestionClause; |
| } |
| // Suggestions are always performed with the default sort order |
| Cursor c = db.query(sSuggestions, mSuggestionProjection, |
| suggestSelection, myArgs, null, null, ORDER_BY, null); |
| c.setNotificationUri(getContext().getContentResolver(), uri); |
| return c; |
| } |
| |
| // otherwise process arguments and perform a standard query |
| int length = uri.getPathSegments().size(); |
| if (length != 1 && length != 2) { |
| throw new IllegalArgumentException("Unknown Uri"); |
| } |
| |
| String base = uri.getPathSegments().get(0); |
| if (!base.equals(sSuggestions)) { |
| throw new IllegalArgumentException("Unknown Uri"); |
| } |
| |
| String[] useProjection = null; |
| if (projection != null && projection.length > 0) { |
| useProjection = new String[projection.length + 1]; |
| System.arraycopy(projection, 0, useProjection, 0, projection.length); |
| useProjection[projection.length] = "_id AS _id"; |
| } |
| |
| StringBuilder whereClause = new StringBuilder(256); |
| if (length == 2) { |
| whereClause.append("(_id = ").append(uri.getPathSegments().get(1)).append(")"); |
| } |
| |
| // Tack on the user's selection, if present |
| if (selection != null && selection.length() > 0) { |
| if (whereClause.length() > 0) { |
| whereClause.append(" AND "); |
| } |
| |
| whereClause.append('('); |
| whereClause.append(selection); |
| whereClause.append(')'); |
| } |
| |
| // And perform the generic query as requested |
| Cursor c = db.query(base, useProjection, whereClause.toString(), |
| selectionArgs, null, null, sortOrder, |
| null); |
| c.setNotificationUri(getContext().getContentResolver(), uri); |
| return c; |
| } |
| |
| /** |
| * This method is provided for use by the ContentResolver. Do not override, or directly |
| * call from your own code. |
| */ |
| @Override |
| public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { |
| throw new UnsupportedOperationException("Not implemented"); |
| } |
| |
| } |