blob: f121d8d25afe5667bfee83e31119543c372b4d25 [file] [log] [blame]
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.quicksearchbox;
import com.android.quicksearchbox.util.NamedTaskExecutor;
import com.android.quicksearchbox.util.Util;
import android.app.PendingIntent;
import android.app.SearchManager;
import android.app.SearchableInfo;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PathPermission;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.speech.RecognizerIntent;
import android.util.Log;
import java.util.Arrays;
/**
* Represents a single suggestion source, e.g. Contacts.
*/
public class SearchableSource extends AbstractSource {
private static final boolean DBG = false;
private static final String TAG = "QSB.SearchableSource";
// TODO: This should be exposed or moved to android-common, see http://b/issue?id=2440614
// The extra key used in an intent to the speech recognizer for in-app voice search.
private static final String EXTRA_CALLING_PACKAGE = "calling_package";
private final SearchableInfo mSearchable;
private final String mName;
private final ActivityInfo mActivityInfo;
private final int mVersionCode;
// Cached label for the activity
private CharSequence mLabel = null;
// Cached icon for the activity
private Drawable.ConstantState mSourceIcon = null;
private Uri mSuggestUriBase;
public SearchableSource(Context context, SearchableInfo searchable, Handler uiThread,
NamedTaskExecutor iconLoader) throws NameNotFoundException {
super(context, uiThread, iconLoader);
ComponentName componentName = searchable.getSearchActivity();
if (DBG) Log.d(TAG, "created Searchable for " + componentName);
mSearchable = searchable;
mName = componentName.flattenToShortString();
PackageManager pm = context.getPackageManager();
mActivityInfo = pm.getActivityInfo(componentName, 0);
PackageInfo pkgInfo = pm.getPackageInfo(componentName.getPackageName(), 0);
mVersionCode = pkgInfo.versionCode;
}
public SearchableInfo getSearchableInfo() {
return mSearchable;
}
/**
* Checks if the current process can read the suggestion provider in this source.
*/
public boolean canRead() {
String authority = mSearchable.getSuggestAuthority();
if (authority == null) {
// TODO: maybe we should have a way to distinguish between having suggestions
// and being readable.
return true;
}
Uri.Builder uriBuilder = new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(authority);
// if content path provided, insert it now
String contentPath = mSearchable.getSuggestPath();
if (contentPath != null) {
uriBuilder.appendEncodedPath(contentPath);
}
// append standard suggestion query path
uriBuilder.appendEncodedPath(SearchManager.SUGGEST_URI_PATH_QUERY);
Uri uri = uriBuilder.build();
return canRead(uri);
}
/**
* Checks if the current process can read the given content URI.
*
* TODO: Shouldn't this be a PackageManager / Context / ContentResolver method?
*/
private boolean canRead(Uri uri) {
ProviderInfo provider = getContext().getPackageManager().resolveContentProvider(
uri.getAuthority(), 0);
if (provider == null) {
Log.w(TAG, getName() + " has bad suggestion authority " + uri.getAuthority());
return false;
}
String readPermission = provider.readPermission;
if (readPermission == null) {
// No permission required to read anything in the content provider
return true;
}
int pid = android.os.Process.myPid();
int uid = android.os.Process.myUid();
if (getContext().checkPermission(readPermission, pid, uid)
== PackageManager.PERMISSION_GRANTED) {
// We have permission to read everything in the content provider
return true;
}
PathPermission[] pathPermissions = provider.pathPermissions;
if (pathPermissions == null || pathPermissions.length == 0) {
// We don't have the readPermission, and there are no pathPermissions
if (DBG) Log.d(TAG, "Missing " + readPermission);
return false;
}
String path = uri.getPath();
for (PathPermission perm : pathPermissions) {
String pathReadPermission = perm.getReadPermission();
if (pathReadPermission != null
&& perm.match(path)
&& getContext().checkPermission(pathReadPermission, pid, uid)
== PackageManager.PERMISSION_GRANTED) {
// We have the path permission
return true;
}
}
if (DBG) Log.d(TAG, "Missing " + readPermission + " and no path permission applies");
return false;
}
public ComponentName getIntentComponent() {
return mSearchable.getSearchActivity();
}
public int getVersionCode() {
return mVersionCode;
}
public String getName() {
return mName;
}
@Override
protected String getIconPackage() {
// Get icons from the package containing the suggestion provider, if any
String iconPackage = mSearchable.getSuggestPackage();
if (iconPackage != null) {
return iconPackage;
} else {
// Fall back to the package containing the searchable activity
return mSearchable.getSearchActivity().getPackageName();
}
}
public CharSequence getLabel() {
if (mLabel == null) {
// Load label lazily
mLabel = mActivityInfo.loadLabel(getContext().getPackageManager());
}
return mLabel;
}
public CharSequence getHint() {
return getText(mSearchable.getHintId());
}
public int getQueryThreshold() {
return mSearchable.getSuggestThreshold();
}
public CharSequence getSettingsDescription() {
return getText(mSearchable.getSettingsDescriptionId());
}
public Drawable getSourceIcon() {
if (mSourceIcon == null) {
Drawable icon = loadSourceIcon();
if (icon == null) {
icon = getContext().getResources().getDrawable(R.drawable.corpus_icon_default);
}
// Can't share Drawable instances, save constant state instead.
mSourceIcon = (icon != null) ? icon.getConstantState() : null;
// Optimization, return the Drawable the first time
return icon;
}
return (mSourceIcon != null) ? mSourceIcon.newDrawable() : null;
}
private Drawable loadSourceIcon() {
int iconRes = getSourceIconResource();
if (iconRes == 0) return null;
PackageManager pm = getContext().getPackageManager();
return pm.getDrawable(mActivityInfo.packageName, iconRes,
mActivityInfo.applicationInfo);
}
public Uri getSourceIconUri() {
int resourceId = getSourceIconResource();
if (resourceId == 0) {
return Util.getResourceUri(getContext(), R.drawable.corpus_icon_default);
} else {
return Util.getResourceUri(getContext(), mActivityInfo.applicationInfo, resourceId);
}
}
private int getSourceIconResource() {
return mActivityInfo.getIconResource();
}
public boolean voiceSearchEnabled() {
return mSearchable.getVoiceSearchEnabled();
}
public Intent createVoiceSearchIntent(Bundle appData) {
if (mSearchable.getVoiceSearchLaunchWebSearch()) {
return createVoiceWebSearchIntent(appData);
} else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
return createVoiceAppSearchIntent(appData);
}
return null;
}
/**
* Create and return an Intent that can launch the voice search activity, perform a specific
* voice transcription, and forward the results to the searchable activity.
*
* This code is copied from SearchDialog
*
* @return A completely-configured intent ready to send to the voice search activity
*/
private Intent createVoiceAppSearchIntent(Bundle appData) {
ComponentName searchActivity = mSearchable.getSearchActivity();
// create the necessary intent to set up a search-and-forward operation
// in the voice search system. We have to keep the bundle separate,
// because it becomes immutable once it enters the PendingIntent
Intent queryIntent = new Intent(Intent.ACTION_SEARCH);
queryIntent.setComponent(searchActivity);
PendingIntent pending = PendingIntent.getActivity(
getContext(), 0, queryIntent, PendingIntent.FLAG_ONE_SHOT);
// Now set up the bundle that will be inserted into the pending intent
// when it's time to do the search. We always build it here (even if empty)
// because the voice search activity will always need to insert "QUERY" into
// it anyway.
Bundle queryExtras = new Bundle();
if (appData != null) {
queryExtras.putBundle(SearchManager.APP_DATA, appData);
}
// Now build the intent to launch the voice search. Add all necessary
// extras to launch the voice recognizer, and then all the necessary extras
// to forward the results to the searchable activity
Intent voiceIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
voiceIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// Add all of the configuration options supplied by the searchable's metadata
String languageModel = getString(mSearchable.getVoiceLanguageModeId());
if (languageModel == null) {
languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM;
}
String prompt = getString(mSearchable.getVoicePromptTextId());
String language = getString(mSearchable.getVoiceLanguageId());
int maxResults = mSearchable.getVoiceMaxResults();
if (maxResults <= 0) {
maxResults = 1;
}
voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel);
voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults);
voiceIntent.putExtra(EXTRA_CALLING_PACKAGE,
searchActivity == null ? null : searchActivity.toShortString());
// Add the values that configure forwarding the results
voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending);
voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras);
return voiceIntent;
}
public SourceResult getSuggestions(String query, int queryLimit, boolean onlySource) {
try {
Cursor cursor = getSuggestions(getContext(), mSearchable, query, queryLimit);
if (DBG) Log.d(TAG, toString() + "[" + query + "] returned.");
return new CursorBackedSourceResult(this, query, cursor);
} catch (RuntimeException ex) {
Log.e(TAG, toString() + "[" + query + "] failed", ex);
return new CursorBackedSourceResult(this, query);
}
}
public SuggestionCursor refreshShortcut(String shortcutId, String extraData) {
Cursor cursor = null;
try {
cursor = getValidationCursor(getContext(), mSearchable, shortcutId, extraData);
if (DBG) Log.d(TAG, toString() + "[" + shortcutId + "] returned.");
if (cursor != null && cursor.getCount() > 0) {
cursor.moveToFirst();
}
return new CursorBackedSourceResult(this, null, cursor);
} catch (RuntimeException ex) {
Log.e(TAG, toString() + "[" + shortcutId + "] failed", ex);
if (cursor != null) {
cursor.close();
}
// TODO: Should we delete the shortcut even if the failure is temporary?
return null;
}
}
public String getSuggestUri() {
Uri uri = getSuggestUriBase(mSearchable);
if (uri == null) return null;
return uri.toString();
}
private synchronized Uri getSuggestUriBase(SearchableInfo searchable) {
if (searchable == null) {
return null;
}
if (mSuggestUriBase == null) {
String authority = searchable.getSuggestAuthority();
if (authority == null) {
return null;
}
Uri.Builder uriBuilder = new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(authority);
// if content path provided, insert it now
final String contentPath = searchable.getSuggestPath();
if (contentPath != null) {
uriBuilder.appendEncodedPath(contentPath);
}
// append standard suggestion query path
uriBuilder.appendPath(SearchManager.SUGGEST_URI_PATH_QUERY);
mSuggestUriBase = uriBuilder.build();
}
return mSuggestUriBase;
}
/**
* This is a copy of {@link SearchManager#getSuggestions(SearchableInfo, String)}.
*/
private Cursor getSuggestions(Context context, SearchableInfo searchable, String query,
int queryLimit) {
Uri base = getSuggestUriBase(searchable);
if (base == null) return null;
Uri.Builder uriBuilder = base.buildUpon();
// get the query selection, may be null
String selection = searchable.getSuggestSelection();
// inject query, either as selection args or inline
String[] selArgs = null;
if (selection != null) { // use selection if provided
selArgs = new String[] { query };
} else { // no selection, use REST pattern
uriBuilder.appendPath(query);
}
uriBuilder.appendQueryParameter("limit", String.valueOf(queryLimit));
Uri uri = uriBuilder.build();
// finally, make the query
if (DBG) {
Log.d(TAG, "query(" + uri + ",null," + selection + ","
+ Arrays.toString(selArgs) + ",null)");
}
Cursor c = context.getContentResolver().query(uri, null, selection, selArgs, null);
if (DBG) Log.d(TAG, "Got cursor from " + mName + ": " + c);
return c;
}
private static Cursor getValidationCursor(Context context, SearchableInfo searchable,
String shortcutId, String extraData) {
String authority = searchable.getSuggestAuthority();
if (authority == null) {
return null;
}
Uri.Builder uriBuilder = new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(authority);
// if content path provided, insert it now
final String contentPath = searchable.getSuggestPath();
if (contentPath != null) {
uriBuilder.appendEncodedPath(contentPath);
}
// append the shortcut path and id
uriBuilder.appendPath(SearchManager.SUGGEST_URI_PATH_SHORTCUT);
uriBuilder.appendPath(shortcutId);
Uri uri = uriBuilder
.appendQueryParameter(SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA, extraData)
.build();
if (DBG) Log.d(TAG, "Requesting refresh " + uri);
// finally, make the query
return context.getContentResolver().query(uri, null, null, null, null);
}
public int getMaxShortcuts(Config config) {
return config.getMaxShortcuts(getName());
}
public boolean includeInAll() {
return true;
}
public boolean queryAfterZeroResults() {
return mSearchable.queryAfterZeroResults();
}
public String getDefaultIntentAction() {
String action = mSearchable.getSuggestIntentAction();
if (action != null) return action;
return Intent.ACTION_SEARCH;
}
public String getDefaultIntentData() {
return mSearchable.getSuggestIntentData();
}
private CharSequence getText(int id) {
if (id == 0) return null;
return getContext().getPackageManager().getText(mActivityInfo.packageName, id,
mActivityInfo.applicationInfo);
}
private String getString(int id) {
CharSequence text = getText(id);
return text == null ? null : text.toString();
}
}