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