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.app.SearchManager;
     20 import android.database.Cursor;
     21 import android.database.sqlite.SQLiteDatabase;
     22 import android.database.sqlite.SQLiteOpenHelper;
     23 import android.net.Uri;
     24 import android.text.TextUtils;
     25 import 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  *     &lt;!-- Content provider for search suggestions --&gt;
     47  *     &lt;provider android:name="YourSuggestionProviderClass"
     48  *               android:authorities="your.suggestion.authority" /&gt;</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  */
     73 public 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