SearchableSource.java revision 94e8a2be78530170f50e7895a558bf8011bbf8e8
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
17package com.android.quicksearchbox;
18
19import android.app.SearchManager;
20import android.app.SearchableInfo;
21import android.content.ComponentName;
22import android.content.ContentResolver;
23import android.content.Context;
24import android.content.pm.ActivityInfo;
25import android.content.pm.PackageManager;
26import android.content.pm.PackageManager.NameNotFoundException;
27import android.database.Cursor;
28import android.graphics.drawable.Drawable;
29import android.net.Uri;
30import android.util.Log;
31
32import java.util.Arrays;
33
34/**
35 * Represents a single suggestion source, e.g. Contacts.
36 *
37 */
38public class SearchableSource implements Source {
39
40    private static final boolean DBG = true;
41    private static final String TAG = "QSB.SearchableSource";
42
43    private final Context mContext;
44
45    private final SearchableInfo mSearchable;
46
47    private final ActivityInfo mActivityInfo;
48
49    // Cached label for the activity
50    private CharSequence mLabel = null;
51
52    // Cached icon for the activity
53    private Drawable.ConstantState mSourceIcon = null;
54
55    private final boolean mIsWebSuggestionSource;
56
57    private final IconLoader mIconLoader;
58
59    public SearchableSource(Context context, SearchableInfo searchable)
60            throws NameNotFoundException {
61        this(context, searchable, false);
62    }
63
64    public SearchableSource(Context context, SearchableInfo searchable,
65            boolean isWebSuggestionSource) throws NameNotFoundException {
66        ComponentName componentName = searchable.getSearchActivity();
67        mContext = context;
68        mSearchable = searchable;
69        mActivityInfo = context.getPackageManager().getActivityInfo(componentName, 0);
70        mIsWebSuggestionSource = isWebSuggestionSource;
71
72        mIconLoader = createIconLoader(context, searchable.getSuggestPackage());
73    }
74
75    private IconLoader createIconLoader(Context context, String providerPackage) {
76        if (providerPackage == null) return null;
77        try {
78            return new CachingIconLoader(new PackageIconLoader(context, providerPackage));
79        } catch (PackageManager.NameNotFoundException ex) {
80            Log.e(TAG, "Suggestion provider package not found: " + providerPackage);
81            return null;
82        }
83    }
84
85    public ComponentName getComponentName() {
86        return mSearchable.getSearchActivity();
87    }
88
89    public String getFlattenedComponentName() {
90        return getComponentName().flattenToShortString();
91    }
92
93    public Drawable getIcon(String drawableId) {
94        return mIconLoader == null ? null : mIconLoader.getIcon(drawableId);
95    }
96
97    public Uri getIconUri(String drawableId) {
98        return mIconLoader == null ? null : mIconLoader.getIconUri(drawableId);
99    }
100
101    public CharSequence getLabel() {
102        if (mLabel == null) {
103            // Load label lazily
104            mLabel = mActivityInfo.loadLabel(mContext.getPackageManager());
105        }
106        return mLabel;
107    }
108
109    public int getQueryThreshold() {
110        return mSearchable.getSuggestThreshold();
111    }
112
113    public String getSettingsDescription() {
114        return mSearchable.getSettingsDescription();
115    }
116
117    public Drawable getSourceIcon() {
118        if (mSourceIcon == null) {
119            // Load icon lazily
120            int iconRes = getSourceIconResource();
121            PackageManager pm = mContext.getPackageManager();
122            Drawable icon = pm.getDrawable(mActivityInfo.packageName, iconRes,
123                    mActivityInfo.applicationInfo);
124            // Can't share Drawable instances, save constant state instead.
125            mSourceIcon = (icon != null) ? icon.getConstantState() : null;
126            // Optimization, return the Drawable the first time
127            return icon;
128        }
129        return (mSourceIcon != null) ? mSourceIcon.newDrawable() : null;
130    }
131
132    public Uri getSourceIconUri() {
133        int resourceId = getSourceIconResource();
134        return new Uri.Builder()
135                .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
136                .authority(getComponentName().getPackageName())
137                .appendEncodedPath(String.valueOf(resourceId))
138                .build();
139    }
140
141    private int getSourceIconResource() {
142        int icon = mActivityInfo.getIconResource();
143        return (icon != 0) ? icon : android.R.drawable.sym_def_app_icon;
144    }
145
146    public SuggestionCursor getSuggestions(String query, int queryLimit) {
147        try {
148            Cursor cursor = getSuggestions(mContext, mSearchable, query, queryLimit);
149            if (DBG) Log.d(TAG, toString() + "[" + query + "] returned.");
150            return new SourceResult(this, query, cursor);
151        } catch (RuntimeException ex) {
152            Log.e(TAG, toString() + "[" + query + "] failed", ex);
153            return new SourceResult(this, query);
154        }
155    }
156
157    public SuggestionCursor refreshShortcut(String shortcutId, String extraData) {
158        Cursor cursor = null;
159        try {
160            cursor = getValidationCursor(mContext, mSearchable, shortcutId, extraData);
161            if (DBG) Log.d(TAG, toString() + "[" + shortcutId + "] returned.");
162            if (cursor != null && cursor.getCount() > 0) {
163                cursor.moveToFirst();
164            }
165            return new SourceResult(this, null, cursor);
166        } catch (RuntimeException ex) {
167            Log.e(TAG, toString() + "[" + shortcutId + "] failed", ex);
168            if (cursor != null) {
169                cursor.close();
170            }
171            // TODO: Should we delete the shortcut even if the failure is temporary?
172            return null;
173        }
174    }
175
176    /**
177     * This is a copy of {@link SearchManager#getSuggestions(SearchableInfo, String)}.
178     */
179    private static Cursor getSuggestions(Context context, SearchableInfo searchable, String query,
180            int queryLimit) {
181        if (searchable == null) {
182            return null;
183        }
184
185        String authority = searchable.getSuggestAuthority();
186        if (authority == null) {
187            return null;
188        }
189
190        Uri.Builder uriBuilder = new Uri.Builder()
191                .scheme(ContentResolver.SCHEME_CONTENT)
192                .authority(authority);
193
194        // if content path provided, insert it now
195        final String contentPath = searchable.getSuggestPath();
196        if (contentPath != null) {
197            uriBuilder.appendEncodedPath(contentPath);
198        }
199
200        // append standard suggestion query path
201        uriBuilder.appendPath(SearchManager.SUGGEST_URI_PATH_QUERY);
202
203        // get the query selection, may be null
204        String selection = searchable.getSuggestSelection();
205        // inject query, either as selection args or inline
206        String[] selArgs = null;
207        if (selection != null) {    // use selection if provided
208            selArgs = new String[] { query };
209        } else {                    // no selection, use REST pattern
210            uriBuilder.appendPath(query);
211        }
212
213        uriBuilder.appendQueryParameter("limit", String.valueOf(queryLimit));
214
215        Uri uri = uriBuilder.build();
216
217        // finally, make the query
218        if (DBG) {
219            Log.d(TAG, "query(" + uri + ",null," + selection + ","
220                    + Arrays.toString(selArgs) + ",null)");
221        }
222        return context.getContentResolver().query(uri, null, selection, selArgs, null);
223    }
224
225    private static Cursor getValidationCursor(Context context, SearchableInfo searchable,
226            String shortcutId, String extraData) {
227        String authority = searchable.getSuggestAuthority();
228        if (authority == null) {
229            return null;
230        }
231
232        Uri.Builder uriBuilder = new Uri.Builder()
233                .scheme(ContentResolver.SCHEME_CONTENT)
234                .authority(authority);
235
236        // if content path provided, insert it now
237        final String contentPath = searchable.getSuggestPath();
238        if (contentPath != null) {
239            uriBuilder.appendEncodedPath(contentPath);
240        }
241
242        // append the shortcut path and id
243        uriBuilder.appendPath(SearchManager.SUGGEST_URI_PATH_SHORTCUT);
244        uriBuilder.appendPath(shortcutId);
245
246        Uri uri = uriBuilder
247                .appendQueryParameter(SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA, extraData)
248                .build();
249
250        if (DBG) Log.d(TAG, "Requesting refresh " + uri);
251        // finally, make the query
252        return context.getContentResolver().query(uri, null, null, null, null);
253    }
254
255    public boolean isWebSuggestionSource() {
256        return mIsWebSuggestionSource;
257    }
258
259    public boolean queryAfterZeroResults() {
260        return mSearchable.queryAfterZeroResults();
261    }
262
263    public boolean shouldRewriteQueryFromData() {
264        return mSearchable.shouldRewriteQueryFromData();
265    }
266
267    public boolean shouldRewriteQueryFromText() {
268        return mSearchable.shouldRewriteQueryFromText();
269    }
270
271    @Override
272    public boolean equals(Object o) {
273        if (o != null && o.getClass().equals(this.getClass())) {
274            SearchableSource s = (SearchableSource) o;
275            return s.mSearchable.getSearchActivity().equals(mSearchable.getSearchActivity());
276        }
277        return false;
278    }
279
280    @Override
281    public int hashCode() {
282        return mSearchable.getSearchActivity().hashCode();
283    }
284
285    @Override
286    public String toString() {
287        return "SearchableSource{component=" + getFlattenedComponentName() + "}";
288    }
289
290    public String getDefaultIntentAction() {
291        return mSearchable.getSuggestIntentAction();
292    }
293
294    public String getDefaultIntentData() {
295        return mSearchable.getSuggestIntentData();
296    }
297
298    public String getSuggestActionMsg(int keyCode) {
299        SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
300        if (actionKey == null) return null;
301        return actionKey.getSuggestActionMsg();
302    }
303
304    public String getSuggestActionMsgColumn(int keyCode) {
305        SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
306        if (actionKey == null) return null;
307        return actionKey.getSuggestActionMsgColumn();
308    }
309
310}
311