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