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