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 
DatabaseHelper(Context context, int newVersion)131         public DatabaseHelper(Context context, int newVersion) {
132             super(context, sDatabaseName, null, newVersion);
133             mNewVersion = newVersion;
134         }
135 
136         @Override
onCreate(SQLiteDatabase db)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
onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)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      */
setupSuggestions(String authority, int mode)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
delete(Uri uri, String selection, String[] selectionArgs)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
getType(Uri uri)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
insert(Uri uri, ContentValues values)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
onCreate()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
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)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
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)393     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
394         throw new UnsupportedOperationException("Not implemented");
395     }
396 
397 }
398