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