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 17 package com.android.globalsearch; 18 19 import android.app.SearchManager; 20 import android.content.BroadcastReceiver; 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.IntentFilter; 25 import android.content.SharedPreferences; 26 import android.content.pm.PackageManager.NameNotFoundException; 27 import android.database.ContentObserver; 28 import android.os.Handler; 29 import android.provider.Settings; 30 import android.server.search.SearchableInfo; 31 import android.util.Log; 32 33 import java.util.ArrayList; 34 import java.util.Collection; 35 import java.util.Collections; 36 import java.util.HashMap; 37 import java.util.HashSet; 38 import java.util.List; 39 40 /** 41 * Maintains the list of all suggestion sources. 42 */ 43 public class SuggestionSources implements SourceLookup { 44 45 // set to true to enable the more verbose debug logging for this file 46 private static final boolean DBG = false; 47 private static final String TAG = "SuggestionSources"; 48 49 // Name of the preferences file used to store suggestion source preferences 50 public static final String PREFERENCES_NAME = "SuggestionSources"; 51 52 // The key for the preference that holds the selected web search source 53 public static final String WEB_SEARCH_SOURCE_PREF = "web_search_source"; 54 55 private final Context mContext; 56 private final SearchManager mSearchManager; 57 private final SharedPreferences mPreferences; 58 private HashSet<String> mTrustedPackages; 59 private boolean mLoaded; 60 61 // All available suggestion sources. 62 private SourceList mSuggestionSources; 63 64 // The web search source to use. This is the source selected in the preferences, 65 // or the default source if no source has been selected. 66 private SuggestionSource mSelectedWebSearchSource; 67 68 // All enabled suggestion sources. This does not include the web search source. 69 private ArrayList<SuggestionSource> mEnabledSuggestionSources; 70 71 // Updates the inclusion of the web search provider. 72 private ShowWebSuggestionsSettingChangeObserver mShowWebSuggestionsSettingChangeObserver; 73 74 /** 75 * 76 * @param context Used for looking up source information etc. 77 */ SuggestionSources(Context context)78 public SuggestionSources(Context context) { 79 mContext = context; 80 mSearchManager = (SearchManager) context.getSystemService(Context.SEARCH_SERVICE); 81 mPreferences = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE); 82 mLoaded = false; 83 } 84 85 /** 86 * Gets all suggestion sources. This does not include any web search sources. 87 * 88 * @return A list of suggestion sources, including sources that are not enabled. 89 * Callers must not modify the returned list. 90 */ getSuggestionSources()91 public synchronized Collection<SuggestionSource> getSuggestionSources() { 92 if (!mLoaded) { 93 Log.w(TAG, "getSuggestionSources() called, but sources not loaded."); 94 return Collections.<SuggestionSource>emptyList(); 95 } 96 return mSuggestionSources.values(); 97 } 98 99 /** {@inheritDoc} */ getSourceByComponentName(ComponentName componentName)100 public synchronized SuggestionSource getSourceByComponentName(ComponentName componentName) { 101 SuggestionSource source = mSuggestionSources.get(componentName); 102 103 // If the source was not found, back off to check the web source in case it's that. 104 if (source == null) { 105 if (mSelectedWebSearchSource != null && 106 mSelectedWebSearchSource.getComponentName().equals(componentName)) { 107 source = mSelectedWebSearchSource; 108 } 109 } 110 return source; 111 } 112 113 /** 114 * Gets all enabled suggestion sources. 115 * 116 * @return All enabled suggestion sources (does not include the web search source). 117 * Callers must not modify the returned list. 118 */ getEnabledSuggestionSources()119 public synchronized List<SuggestionSource> getEnabledSuggestionSources() { 120 if (!mLoaded) { 121 Log.w(TAG, "getEnabledSuggestionSources() called, but sources not loaded."); 122 return Collections.<SuggestionSource>emptyList(); 123 } 124 return mEnabledSuggestionSources; 125 } 126 127 /** 128 * Checks whether a suggestion source is enabled by default. For now, only trusted sources are. 129 */ isSourceDefaultEnabled(SuggestionSource source)130 public boolean isSourceDefaultEnabled(SuggestionSource source) { 131 return isTrustedSource(source); 132 } 133 134 /** {@inheritDoc} */ getSelectedWebSearchSource()135 public synchronized SuggestionSource getSelectedWebSearchSource() { 136 if (!mLoaded) { 137 Log.w(TAG, "getSelectedWebSearchSource() called, but sources not loaded."); 138 return null; 139 } 140 return mSelectedWebSearchSource; 141 } 142 143 /** 144 * Gets the preference key of the preference for whether the given source 145 * is enabled. The preference is stored in the {@link #PREFERENCES_NAME} 146 * preferences file. 147 */ getSourceEnabledPreference(SuggestionSource source)148 public String getSourceEnabledPreference(SuggestionSource source) { 149 return "enable_source_" + source.getComponentName().flattenToString(); 150 } 151 152 // Broadcast receiver for package change notifications 153 private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 154 @Override 155 public void onReceive(Context context, Intent intent) { 156 String action = intent.getAction(); 157 if (SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED.equals(action) 158 || SearchManager.INTENT_ACTION_SEARCH_SETTINGS_CHANGED.equals(action)) { 159 // TODO: Instead of rebuilding the whole list on every change, 160 // just add, remove or update the application that has changed. 161 // Adding and updating seem tricky, since I can't see an easy way to list the 162 // launchable activities in a given package. 163 updateSources(); 164 } 165 } 166 }; 167 168 /** 169 * After calling, clients must call {@link #close()} when done with this object. 170 */ load()171 public synchronized void load() { 172 if (mLoaded) { 173 Log.w(TAG, "Already loaded, ignoring call to load()."); 174 return; 175 } 176 177 loadTrustedPackages(); 178 179 // Listen for searchables changes. 180 mContext.registerReceiver(mBroadcastReceiver, 181 new IntentFilter(SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED)); 182 183 // Listen for search preference changes. 184 mContext.registerReceiver(mBroadcastReceiver, 185 new IntentFilter(SearchManager.INTENT_ACTION_SEARCH_SETTINGS_CHANGED)); 186 187 mShowWebSuggestionsSettingChangeObserver = new ShowWebSuggestionsSettingChangeObserver(); 188 mContext.getContentResolver().registerContentObserver( 189 Settings.System.getUriFor(Settings.System.SHOW_WEB_SUGGESTIONS), 190 true, 191 mShowWebSuggestionsSettingChangeObserver); 192 193 // update list of sources 194 updateSources(); 195 mLoaded = true; 196 } 197 198 /** 199 * Releases all resources used by this object. It is possible to call 200 * {@link #load()} again after calling this method. 201 */ close()202 public synchronized void close() { 203 if (!mLoaded) { 204 Log.w(TAG, "Not loaded, ignoring call to close()."); 205 return; 206 } 207 mContext.unregisterReceiver(mBroadcastReceiver); 208 mContext.getContentResolver().unregisterContentObserver( 209 mShowWebSuggestionsSettingChangeObserver); 210 211 mSuggestionSources = null; 212 mSelectedWebSearchSource = null; 213 mEnabledSuggestionSources = null; 214 mLoaded = false; 215 } 216 loadTrustedPackages()217 private void loadTrustedPackages() { 218 mTrustedPackages = new HashSet<String>(); 219 220 // Get the list of trusted packages from a resource, which allows vendor overlays. 221 String[] trustedPackages = mContext.getResources().getStringArray( 222 R.array.trusted_search_providers); 223 224 if (trustedPackages == null) { 225 Log.w(TAG, "Could not load list of trusted search providers, trusting none"); 226 return; 227 } 228 229 for (String trustedPackage : trustedPackages) { 230 mTrustedPackages.add(trustedPackage); 231 } 232 } 233 234 /** 235 * Loads the list of suggestion sources. This method is package private so that 236 * it can be called efficiently from inner classes. 237 */ updateSources()238 /* package */ synchronized void updateSources() { 239 mSuggestionSources = new SourceList(); 240 addExternalSources(); 241 242 mEnabledSuggestionSources = findEnabledSuggestionSources(); 243 mSelectedWebSearchSource = findWebSearchSource(); 244 } 245 addExternalSources()246 private void addExternalSources() { 247 ArrayList<SuggestionSource> trusted = new ArrayList<SuggestionSource>(); 248 ArrayList<SuggestionSource> untrusted = new ArrayList<SuggestionSource>(); 249 for (SearchableInfo searchable : mSearchManager.getSearchablesInGlobalSearch()) { 250 try { 251 SuggestionSource source = new SearchableSuggestionSource(mContext, searchable); 252 if (isTrustedSource(source)) { 253 trusted.add(source); 254 } else { 255 untrusted.add(source); 256 } 257 } catch (NameNotFoundException ex) { 258 Log.e(TAG, "Searchable activity not found: " + ex.getMessage()); 259 } 260 } 261 for (SuggestionSource s : trusted) { 262 addSuggestionSource(s); 263 } 264 for (SuggestionSource s : untrusted) { 265 addSuggestionSource(s); 266 } 267 } 268 addSuggestionSource(SuggestionSource source)269 private void addSuggestionSource(SuggestionSource source) { 270 if (DBG) Log.d(TAG, "Adding source: " + source); 271 SuggestionSource old = mSuggestionSources.put(source); 272 if (old != null) { 273 Log.w(TAG, "Replaced source " + old + " for " + source.getComponentName()); 274 } 275 } 276 277 /** 278 * Computes the list of enabled suggestion sources. 279 */ findEnabledSuggestionSources()280 private ArrayList<SuggestionSource> findEnabledSuggestionSources() { 281 ArrayList<SuggestionSource> enabledSources = new ArrayList<SuggestionSource>(); 282 for (SuggestionSource source : mSuggestionSources.values()) { 283 if (isSourceEnabled(source)) { 284 if (DBG) Log.d(TAG, "Adding enabled source " + source); 285 enabledSources.add(source); 286 } 287 } 288 return enabledSources; 289 } 290 isSourceEnabled(SuggestionSource source)291 private boolean isSourceEnabled(SuggestionSource source) { 292 boolean defaultEnabled = isSourceDefaultEnabled(source); 293 if (mPreferences == null) { 294 Log.w(TAG, "Search preferences " + PREFERENCES_NAME + " not found."); 295 return true; 296 } 297 String sourceEnabledPref = getSourceEnabledPreference(source); 298 return mPreferences.getBoolean(sourceEnabledPref, defaultEnabled); 299 } 300 isTrustedSource(SuggestionSource source)301 public boolean isTrustedSource(SuggestionSource source) { 302 if (source == null) return false; 303 final String packageName = source.getComponentName().getPackageName(); 304 return mTrustedPackages != null && mTrustedPackages.contains(packageName); 305 } 306 307 /** 308 * Finds the selected web search source. 309 */ findWebSearchSource()310 private SuggestionSource findWebSearchSource() { 311 SuggestionSource webSearchSource = null; 312 if (Settings.System.getInt(mContext.getContentResolver(), 313 Settings.System.SHOW_WEB_SUGGESTIONS, 314 1 /* default on until user actually changes it */) == 1) { 315 SearchableInfo webSearchable = mSearchManager.getDefaultSearchableForWebSearch(); 316 if (webSearchable != null) { 317 if (DBG) Log.d(TAG, "Adding web source " + webSearchable.getSearchActivity()); 318 // Construct a SearchableSuggestionSource around the web search source. Allow 319 // the web search source to provide a larger number of results with 320 // WEB_RESULTS_OVERRIDE_LIMIT. 321 try { 322 webSearchSource = 323 new SearchableSuggestionSource(mContext, webSearchable, true); 324 } catch (NameNotFoundException ex) { 325 Log.e(TAG, "Searchable activity not found: " + ex.getMessage()); 326 } 327 } 328 } 329 return webSearchSource; 330 } 331 332 /** 333 * This works like a map from ComponentName to SuggestionSource, 334 * but supports a zero-allocation method for listing all the sources. 335 */ 336 private static class SourceList { 337 338 private HashMap<ComponentName,SuggestionSource> mSourcesByComponent; 339 private ArrayList<SuggestionSource> mSources; 340 SourceList()341 public SourceList() { 342 mSourcesByComponent = new HashMap<ComponentName,SuggestionSource>(); 343 mSources = new ArrayList<SuggestionSource>(); 344 } 345 get(ComponentName componentName)346 public SuggestionSource get(ComponentName componentName) { 347 return mSourcesByComponent.get(componentName); 348 } 349 350 /** 351 * Adds a source. Replaces any previous source with the same component name. 352 * 353 * @return The previous source that was replaced, if any. 354 */ put(SuggestionSource source)355 public SuggestionSource put(SuggestionSource source) { 356 if (source == null) { 357 return null; 358 } 359 SuggestionSource old = mSourcesByComponent.put(source.getComponentName(), source); 360 if (old != null) { 361 // linear search is ok here, since addSource() is only called when the 362 // list of sources is updated, which is infrequent. Also, collisions would only 363 // happen if there are two sources with the same component name, which should 364 // only happen as long as we have hard-coded sources. 365 mSources.remove(old); 366 } 367 mSources.add(source); 368 return old; 369 } 370 371 /** 372 * Gets the suggestion sources. 373 */ values()374 public ArrayList<SuggestionSource> values() { 375 return mSources; 376 } 377 378 /** 379 * Checks whether the list is empty. 380 */ isEmpty()381 public boolean isEmpty() { 382 return mSources.isEmpty(); 383 } 384 } 385 386 /** 387 * ContentObserver which updates the list of enabled sources to include or exclude 388 * the web search provider depending on the state of the 389 * {@link Settings.System#SHOW_WEB_SUGGESTIONS} setting. 390 */ 391 private class ShowWebSuggestionsSettingChangeObserver extends ContentObserver { ShowWebSuggestionsSettingChangeObserver()392 public ShowWebSuggestionsSettingChangeObserver() { 393 super(new Handler()); 394 } 395 396 @Override onChange(boolean selfChange)397 public void onChange(boolean selfChange) { 398 updateSources(); 399 } 400 } 401 } 402