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