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