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