• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2009 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 com.android.quicksearchbox;
18 
19 import com.android.quicksearchbox.util.NamedTaskExecutor;
20 import com.android.quicksearchbox.util.Util;
21 
22 import android.app.PendingIntent;
23 import android.app.SearchManager;
24 import android.app.SearchableInfo;
25 import android.content.ComponentName;
26 import android.content.ContentResolver;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.pm.ActivityInfo;
30 import android.content.pm.PackageInfo;
31 import android.content.pm.PackageManager;
32 import android.content.pm.PackageManager.NameNotFoundException;
33 import android.content.pm.PathPermission;
34 import android.content.pm.ProviderInfo;
35 import android.database.Cursor;
36 import android.graphics.drawable.Drawable;
37 import android.net.Uri;
38 import android.os.Bundle;
39 import android.os.Handler;
40 import android.speech.RecognizerIntent;
41 import android.util.Log;
42 
43 import java.util.Arrays;
44 
45 /**
46  * Represents a single suggestion source, e.g. Contacts.
47  */
48 public class SearchableSource extends AbstractSource {
49 
50     private static final boolean DBG = false;
51     private static final String TAG = "QSB.SearchableSource";
52 
53     // TODO: This should be exposed or moved to android-common, see http://b/issue?id=2440614
54     // The extra key used in an intent to the speech recognizer for in-app voice search.
55     private static final String EXTRA_CALLING_PACKAGE = "calling_package";
56 
57     private final SearchableInfo mSearchable;
58 
59     private final String mName;
60 
61     private final ActivityInfo mActivityInfo;
62 
63     private final int mVersionCode;
64 
65     // Cached label for the activity
66     private CharSequence mLabel = null;
67 
68     // Cached icon for the activity
69     private Drawable.ConstantState mSourceIcon = null;
70 
71     private Uri mSuggestUriBase;
72 
SearchableSource(Context context, SearchableInfo searchable, Handler uiThread, NamedTaskExecutor iconLoader)73     public SearchableSource(Context context, SearchableInfo searchable, Handler uiThread,
74             NamedTaskExecutor iconLoader) throws NameNotFoundException {
75         super(context, uiThread, iconLoader);
76         ComponentName componentName = searchable.getSearchActivity();
77         if (DBG) Log.d(TAG, "created Searchable for " + componentName);
78         mSearchable = searchable;
79         mName = componentName.flattenToShortString();
80         PackageManager pm = context.getPackageManager();
81         mActivityInfo = pm.getActivityInfo(componentName, 0);
82         PackageInfo pkgInfo = pm.getPackageInfo(componentName.getPackageName(), 0);
83         mVersionCode = pkgInfo.versionCode;
84     }
85 
getSearchableInfo()86     public SearchableInfo getSearchableInfo() {
87         return mSearchable;
88     }
89 
90     /**
91      * Checks if the current process can read the suggestion provider in this source.
92      */
canRead()93     public boolean canRead() {
94         String authority = mSearchable.getSuggestAuthority();
95         if (authority == null) {
96             // TODO: maybe we should have a way to distinguish between having suggestions
97             // and being readable.
98             return true;
99         }
100 
101         Uri.Builder uriBuilder = new Uri.Builder()
102                 .scheme(ContentResolver.SCHEME_CONTENT)
103                 .authority(authority);
104         // if content path provided, insert it now
105         String contentPath = mSearchable.getSuggestPath();
106         if (contentPath != null) {
107             uriBuilder.appendEncodedPath(contentPath);
108         }
109         // append standard suggestion query path
110         uriBuilder.appendEncodedPath(SearchManager.SUGGEST_URI_PATH_QUERY);
111         Uri uri = uriBuilder.build();
112         return canRead(uri);
113     }
114 
115     /**
116      * Checks if the current process can read the given content URI.
117      *
118      * TODO: Shouldn't this be a PackageManager / Context / ContentResolver method?
119      */
canRead(Uri uri)120     private boolean canRead(Uri uri) {
121         ProviderInfo provider = getContext().getPackageManager().resolveContentProvider(
122                 uri.getAuthority(), 0);
123         if (provider == null) {
124             Log.w(TAG, getName() + " has bad suggestion authority " + uri.getAuthority());
125             return false;
126         }
127         String readPermission = provider.readPermission;
128         if (readPermission == null) {
129             // No permission required to read anything in the content provider
130             return true;
131         }
132         int pid = android.os.Process.myPid();
133         int uid = android.os.Process.myUid();
134         if (getContext().checkPermission(readPermission, pid, uid)
135                 == PackageManager.PERMISSION_GRANTED) {
136             // We have permission to read everything in the content provider
137             return true;
138         }
139         PathPermission[] pathPermissions = provider.pathPermissions;
140         if (pathPermissions == null || pathPermissions.length == 0) {
141             // We don't have the readPermission, and there are no pathPermissions
142             if (DBG) Log.d(TAG, "Missing " + readPermission);
143             return false;
144         }
145         String path = uri.getPath();
146         for (PathPermission perm : pathPermissions) {
147             String pathReadPermission = perm.getReadPermission();
148             if (pathReadPermission != null
149                     && perm.match(path)
150                     && getContext().checkPermission(pathReadPermission, pid, uid)
151                             == PackageManager.PERMISSION_GRANTED) {
152                 // We have the path permission
153                 return true;
154             }
155         }
156         if (DBG) Log.d(TAG, "Missing " + readPermission + " and no path permission applies");
157         return false;
158     }
159 
getIntentComponent()160     public ComponentName getIntentComponent() {
161         return mSearchable.getSearchActivity();
162     }
163 
getVersionCode()164     public int getVersionCode() {
165         return mVersionCode;
166     }
167 
getName()168     public String getName() {
169         return mName;
170     }
171 
172     @Override
getIconPackage()173     protected String getIconPackage() {
174         // Get icons from the package containing the suggestion provider, if any
175         String iconPackage = mSearchable.getSuggestPackage();
176         if (iconPackage != null) {
177             return iconPackage;
178         } else {
179             // Fall back to the package containing the searchable activity
180             return mSearchable.getSearchActivity().getPackageName();
181         }
182     }
183 
getLabel()184     public CharSequence getLabel() {
185         if (mLabel == null) {
186             // Load label lazily
187             mLabel = mActivityInfo.loadLabel(getContext().getPackageManager());
188         }
189         return mLabel;
190     }
191 
getHint()192     public CharSequence getHint() {
193         return getText(mSearchable.getHintId());
194     }
195 
getQueryThreshold()196     public int getQueryThreshold() {
197         return mSearchable.getSuggestThreshold();
198     }
199 
getSettingsDescription()200     public CharSequence getSettingsDescription() {
201         return getText(mSearchable.getSettingsDescriptionId());
202     }
203 
getSourceIcon()204     public Drawable getSourceIcon() {
205         if (mSourceIcon == null) {
206             Drawable icon = loadSourceIcon();
207             if (icon == null) {
208                 icon = getContext().getResources().getDrawable(R.drawable.corpus_icon_default);
209             }
210             // Can't share Drawable instances, save constant state instead.
211             mSourceIcon = (icon != null) ? icon.getConstantState() : null;
212             // Optimization, return the Drawable the first time
213             return icon;
214         }
215         return (mSourceIcon != null) ? mSourceIcon.newDrawable() : null;
216     }
217 
loadSourceIcon()218     private Drawable loadSourceIcon() {
219         int iconRes = getSourceIconResource();
220         if (iconRes == 0) return null;
221         PackageManager pm = getContext().getPackageManager();
222         return pm.getDrawable(mActivityInfo.packageName, iconRes,
223                 mActivityInfo.applicationInfo);
224     }
225 
getSourceIconUri()226     public Uri getSourceIconUri() {
227         int resourceId = getSourceIconResource();
228         if (resourceId == 0) {
229             return Util.getResourceUri(getContext(), R.drawable.corpus_icon_default);
230         } else {
231             return Util.getResourceUri(getContext(), mActivityInfo.applicationInfo, resourceId);
232         }
233     }
234 
getSourceIconResource()235     private int getSourceIconResource() {
236         return mActivityInfo.getIconResource();
237     }
238 
voiceSearchEnabled()239     public boolean voiceSearchEnabled() {
240         return mSearchable.getVoiceSearchEnabled();
241     }
242 
createVoiceSearchIntent(Bundle appData)243     public Intent createVoiceSearchIntent(Bundle appData) {
244         if (mSearchable.getVoiceSearchLaunchWebSearch()) {
245             return createVoiceWebSearchIntent(appData);
246         } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
247             return createVoiceAppSearchIntent(appData);
248         }
249         return null;
250     }
251 
252     /**
253      * Create and return an Intent that can launch the voice search activity, perform a specific
254      * voice transcription, and forward the results to the searchable activity.
255      *
256      * This code is copied from SearchDialog
257      *
258      * @return A completely-configured intent ready to send to the voice search activity
259      */
createVoiceAppSearchIntent(Bundle appData)260     private Intent createVoiceAppSearchIntent(Bundle appData) {
261         ComponentName searchActivity = mSearchable.getSearchActivity();
262 
263         // create the necessary intent to set up a search-and-forward operation
264         // in the voice search system.   We have to keep the bundle separate,
265         // because it becomes immutable once it enters the PendingIntent
266         Intent queryIntent = new Intent(Intent.ACTION_SEARCH);
267         queryIntent.setComponent(searchActivity);
268         PendingIntent pending = PendingIntent.getActivity(
269                 getContext(), 0, queryIntent, PendingIntent.FLAG_ONE_SHOT);
270 
271         // Now set up the bundle that will be inserted into the pending intent
272         // when it's time to do the search.  We always build it here (even if empty)
273         // because the voice search activity will always need to insert "QUERY" into
274         // it anyway.
275         Bundle queryExtras = new Bundle();
276         if (appData != null) {
277             queryExtras.putBundle(SearchManager.APP_DATA, appData);
278         }
279 
280         // Now build the intent to launch the voice search.  Add all necessary
281         // extras to launch the voice recognizer, and then all the necessary extras
282         // to forward the results to the searchable activity
283         Intent voiceIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
284         voiceIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
285 
286         // Add all of the configuration options supplied by the searchable's metadata
287         String languageModel = getString(mSearchable.getVoiceLanguageModeId());
288         if (languageModel == null) {
289             languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM;
290         }
291         String prompt = getString(mSearchable.getVoicePromptTextId());
292         String language = getString(mSearchable.getVoiceLanguageId());
293         int maxResults = mSearchable.getVoiceMaxResults();
294         if (maxResults <= 0) {
295             maxResults = 1;
296         }
297 
298         voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel);
299         voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
300         voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
301         voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults);
302         voiceIntent.putExtra(EXTRA_CALLING_PACKAGE,
303                 searchActivity == null ? null : searchActivity.toShortString());
304 
305         // Add the values that configure forwarding the results
306         voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending);
307         voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras);
308 
309         return voiceIntent;
310     }
311 
getSuggestions(String query, int queryLimit, boolean onlySource)312     public SourceResult getSuggestions(String query, int queryLimit, boolean onlySource) {
313         try {
314             Cursor cursor = getSuggestions(getContext(), mSearchable, query, queryLimit);
315             if (DBG) Log.d(TAG, toString() + "[" + query + "] returned.");
316             return new CursorBackedSourceResult(this, query, cursor);
317         } catch (RuntimeException ex) {
318             Log.e(TAG, toString() + "[" + query + "] failed", ex);
319             return new CursorBackedSourceResult(this, query);
320         }
321     }
322 
refreshShortcut(String shortcutId, String extraData)323     public SuggestionCursor refreshShortcut(String shortcutId, String extraData) {
324         Cursor cursor = null;
325         try {
326             cursor = getValidationCursor(getContext(), mSearchable, shortcutId, extraData);
327             if (DBG) Log.d(TAG, toString() + "[" + shortcutId + "] returned.");
328             if (cursor != null && cursor.getCount() > 0) {
329                 cursor.moveToFirst();
330             }
331             return new CursorBackedSourceResult(this, null, cursor);
332         } catch (RuntimeException ex) {
333             Log.e(TAG, toString() + "[" + shortcutId + "] failed", ex);
334             if (cursor != null) {
335                 cursor.close();
336             }
337             // TODO: Should we delete the shortcut even if the failure is temporary?
338             return null;
339         }
340     }
341 
getSuggestUri()342     public String getSuggestUri() {
343         Uri uri = getSuggestUriBase(mSearchable);
344         if (uri == null) return null;
345         return uri.toString();
346     }
347 
getSuggestUriBase(SearchableInfo searchable)348     private synchronized Uri getSuggestUriBase(SearchableInfo searchable) {
349         if (searchable == null) {
350             return null;
351         }
352         if (mSuggestUriBase == null) {
353 
354             String authority = searchable.getSuggestAuthority();
355             if (authority == null) {
356                 return null;
357             }
358 
359             Uri.Builder uriBuilder = new Uri.Builder()
360                     .scheme(ContentResolver.SCHEME_CONTENT)
361                     .authority(authority);
362 
363             // if content path provided, insert it now
364             final String contentPath = searchable.getSuggestPath();
365             if (contentPath != null) {
366                 uriBuilder.appendEncodedPath(contentPath);
367             }
368 
369             // append standard suggestion query path
370             uriBuilder.appendPath(SearchManager.SUGGEST_URI_PATH_QUERY);
371             mSuggestUriBase = uriBuilder.build();
372         }
373         return mSuggestUriBase;
374     }
375 
376     /**
377      * This is a copy of {@link SearchManager#getSuggestions(SearchableInfo, String)}.
378      */
getSuggestions(Context context, SearchableInfo searchable, String query, int queryLimit)379     private Cursor getSuggestions(Context context, SearchableInfo searchable, String query,
380             int queryLimit) {
381 
382         Uri base = getSuggestUriBase(searchable);
383         if (base == null) return null;
384         Uri.Builder uriBuilder = base.buildUpon();
385 
386         // get the query selection, may be null
387         String selection = searchable.getSuggestSelection();
388         // inject query, either as selection args or inline
389         String[] selArgs = null;
390         if (selection != null) {    // use selection if provided
391             selArgs = new String[] { query };
392         } else {                    // no selection, use REST pattern
393             uriBuilder.appendPath(query);
394         }
395 
396         uriBuilder.appendQueryParameter("limit", String.valueOf(queryLimit));
397 
398         Uri uri = uriBuilder.build();
399 
400         // finally, make the query
401         if (DBG) {
402             Log.d(TAG, "query(" + uri + ",null," + selection + ","
403                     + Arrays.toString(selArgs) + ",null)");
404         }
405         Cursor c = context.getContentResolver().query(uri, null, selection, selArgs, null);
406         if (DBG) Log.d(TAG, "Got cursor from " + mName + ": " + c);
407         return c;
408     }
409 
getValidationCursor(Context context, SearchableInfo searchable, String shortcutId, String extraData)410     private static Cursor getValidationCursor(Context context, SearchableInfo searchable,
411             String shortcutId, String extraData) {
412         String authority = searchable.getSuggestAuthority();
413         if (authority == null) {
414             return null;
415         }
416 
417         Uri.Builder uriBuilder = new Uri.Builder()
418                 .scheme(ContentResolver.SCHEME_CONTENT)
419                 .authority(authority);
420 
421         // if content path provided, insert it now
422         final String contentPath = searchable.getSuggestPath();
423         if (contentPath != null) {
424             uriBuilder.appendEncodedPath(contentPath);
425         }
426 
427         // append the shortcut path and id
428         uriBuilder.appendPath(SearchManager.SUGGEST_URI_PATH_SHORTCUT);
429         uriBuilder.appendPath(shortcutId);
430 
431         Uri uri = uriBuilder
432                 .appendQueryParameter(SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA, extraData)
433                 .build();
434 
435         if (DBG) Log.d(TAG, "Requesting refresh " + uri);
436         // finally, make the query
437         return context.getContentResolver().query(uri, null, null, null, null);
438     }
439 
getMaxShortcuts(Config config)440     public int getMaxShortcuts(Config config) {
441         return config.getMaxShortcuts(getName());
442     }
443 
includeInAll()444     public boolean includeInAll() {
445         return true;
446     }
447 
queryAfterZeroResults()448     public boolean queryAfterZeroResults() {
449         return mSearchable.queryAfterZeroResults();
450     }
451 
getDefaultIntentAction()452     public String getDefaultIntentAction() {
453         String action = mSearchable.getSuggestIntentAction();
454         if (action != null) return action;
455         return Intent.ACTION_SEARCH;
456     }
457 
getDefaultIntentData()458     public String getDefaultIntentData() {
459         return mSearchable.getSuggestIntentData();
460     }
461 
getText(int id)462     private CharSequence getText(int id) {
463         if (id == 0) return null;
464         return getContext().getPackageManager().getText(mActivityInfo.packageName, id,
465                 mActivityInfo.applicationInfo);
466     }
467 
getString(int id)468     private String getString(int id) {
469         CharSequence text = getText(id);
470         return text == null ? null : text.toString();
471     }
472 }
473