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