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