| /* |
| * Copyright (C) 2009 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.globalsearch; |
| |
| import android.app.SearchManager; |
| import android.content.BroadcastReceiver; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.SharedPreferences; |
| import android.database.ContentObserver; |
| import android.os.Handler; |
| import android.provider.Settings; |
| import android.server.search.SearchableInfo; |
| import android.util.Log; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| |
| /** |
| * Maintains the list of all suggestion sources. |
| */ |
| public class SuggestionSources implements SourceLookup { |
| |
| // set to true to enable the more verbose debug logging for this file |
| private static final boolean DBG = false; |
| private static final String TAG = "SuggestionSources"; |
| |
| // Name of the preferences file used to store suggestion source preferences |
| public static final String PREFERENCES_NAME = "SuggestionSources"; |
| |
| // The key for the preference that holds the selected web search source |
| public static final String WEB_SEARCH_SOURCE_PREF = "web_search_source"; |
| |
| // An override value for web search providers to provide more results than others. |
| private static final int WEB_RESULTS_OVERRIDE_LIMIT = 20; |
| |
| private Context mContext; |
| private SearchManager mSearchManager; |
| private SharedPreferences mPreferences; |
| private HashSet<String> mTrustedPackages; |
| private boolean mLoaded; |
| |
| // All available suggestion sources. |
| private SourceList mSuggestionSources; |
| |
| // The web search source to use. This is the source selected in the preferences, |
| // or the default source if no source has been selected. |
| private SuggestionSource mSelectedWebSearchSource; |
| |
| // All enabled suggestion sources. This does not include the web search source. |
| private ArrayList<SuggestionSource> mEnabledSuggestionSources; |
| |
| // Updates the inclusion of the web search provider. |
| private ShowWebSuggestionsSettingChangeObserver mShowWebSuggestionsSettingChangeObserver; |
| |
| /** |
| * |
| * @param context Used for looking up source information etc. |
| */ |
| public SuggestionSources(Context context) { |
| mContext = context; |
| mSearchManager = (SearchManager) context.getSystemService(Context.SEARCH_SERVICE); |
| mPreferences = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE); |
| mLoaded = false; |
| } |
| |
| /** |
| * Gets all suggestion sources. This does not include any web search sources. |
| * |
| * @return A list of suggestion sources, including sources that are not enabled. |
| * Callers must not modify the returned list. |
| */ |
| public synchronized Collection<SuggestionSource> getSuggestionSources() { |
| if (!mLoaded) { |
| Log.w(TAG, "getSuggestionSources() called, but sources not loaded."); |
| return Collections.<SuggestionSource>emptyList(); |
| } |
| return mSuggestionSources.values(); |
| } |
| |
| /** {@inheritDoc} */ |
| public synchronized SuggestionSource getSourceByComponentName(ComponentName componentName) { |
| SuggestionSource source = mSuggestionSources.get(componentName); |
| |
| // If the source was not found, back off to check the web source in case it's that. |
| if (source == null) { |
| if (mSelectedWebSearchSource != null && |
| mSelectedWebSearchSource.getComponentName().equals(componentName)) { |
| source = mSelectedWebSearchSource; |
| } |
| } |
| return source; |
| } |
| |
| /** |
| * Gets all enabled suggestion sources. |
| * |
| * @return All enabled suggestion sources (does not include the web search source). |
| * Callers must not modify the returned list. |
| */ |
| public synchronized List<SuggestionSource> getEnabledSuggestionSources() { |
| if (!mLoaded) { |
| Log.w(TAG, "getEnabledSuggestionSources() called, but sources not loaded."); |
| return Collections.<SuggestionSource>emptyList(); |
| } |
| return mEnabledSuggestionSources; |
| } |
| |
| /** |
| * Checks whether a suggestion source is enabled by default. |
| */ |
| public boolean isSourceDefaultEnabled(SuggestionSource source) { |
| return true; // TODO: get from source? |
| } |
| |
| /** {@inheritDoc} */ |
| public synchronized SuggestionSource getSelectedWebSearchSource() { |
| if (!mLoaded) { |
| Log.w(TAG, "getSelectedWebSearchSource() called, but sources not loaded."); |
| return null; |
| } |
| return mSelectedWebSearchSource; |
| } |
| |
| /** |
| * Gets the preference key of the preference for whether the given source |
| * is enabled. The preference is stored in the {@link #PREFERENCES_NAME} |
| * preferences file. |
| */ |
| public String getSourceEnabledPreference(SuggestionSource source) { |
| return "enable_source_" + source.getComponentName().flattenToString(); |
| } |
| |
| // Broadcast receiver for package change notifications |
| private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| String action = intent.getAction(); |
| if (SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED.equals(action) |
| || SearchManager.INTENT_ACTION_SEARCH_SETTINGS_CHANGED.equals(action)) { |
| // TODO: Instead of rebuilding the whole list on every change, |
| // just add, remove or update the application that has changed. |
| // Adding and updating seem tricky, since I can't see an easy way to list the |
| // launchable activities in a given package. |
| updateSources(); |
| } |
| } |
| }; |
| |
| /** |
| * After calling, clients must call {@link #close()} when done with this object. |
| */ |
| public synchronized void load() { |
| if (mLoaded) { |
| Log.w(TAG, "Already loaded, ignoring call to load()."); |
| return; |
| } |
| |
| loadTrustedPackages(); |
| |
| // Listen for searchables changes. |
| mContext.registerReceiver(mBroadcastReceiver, |
| new IntentFilter(SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED)); |
| |
| // Listen for search preference changes. |
| mContext.registerReceiver(mBroadcastReceiver, |
| new IntentFilter(SearchManager.INTENT_ACTION_SEARCH_SETTINGS_CHANGED)); |
| |
| mShowWebSuggestionsSettingChangeObserver = new ShowWebSuggestionsSettingChangeObserver(); |
| mContext.getContentResolver().registerContentObserver( |
| Settings.System.getUriFor(Settings.System.SHOW_WEB_SUGGESTIONS), |
| true, |
| mShowWebSuggestionsSettingChangeObserver); |
| |
| // update list of sources |
| updateSources(); |
| mLoaded = true; |
| } |
| |
| /** |
| * Releases all resources used by this object. It is possible to call |
| * {@link #load()} again after calling this method. |
| */ |
| public synchronized void close() { |
| if (!mLoaded) { |
| Log.w(TAG, "Not loaded, ignoring call to close()."); |
| return; |
| } |
| mContext.unregisterReceiver(mBroadcastReceiver); |
| mContext.getContentResolver().unregisterContentObserver( |
| mShowWebSuggestionsSettingChangeObserver); |
| |
| mSuggestionSources = null; |
| mSelectedWebSearchSource = null; |
| mEnabledSuggestionSources = null; |
| mLoaded = false; |
| } |
| |
| // TODO: should get this form a resource file, to allow vendor overlays |
| private void loadTrustedPackages() { |
| mTrustedPackages = new HashSet<String>(); |
| mTrustedPackages.add("com.android.contacts"); |
| mTrustedPackages.add("com.android.browser"); |
| mTrustedPackages.add("com.android.providers.applications"); |
| } |
| |
| /** |
| * Loads the list of suggestion sources. This method is package private so that |
| * it can be called efficiently from inner classes. |
| */ |
| /* package */ synchronized void updateSources() { |
| mSuggestionSources = new SourceList(); |
| addExternalSources(); |
| |
| mEnabledSuggestionSources = findEnabledSuggestionSources(); |
| mSelectedWebSearchSource = findWebSearchSource(); |
| } |
| |
| private void addExternalSources() { |
| ArrayList<SuggestionSource> trusted = new ArrayList<SuggestionSource>(); |
| ArrayList<SuggestionSource> untrusted = new ArrayList<SuggestionSource>(); |
| for (SearchableInfo searchable : mSearchManager.getSearchablesInGlobalSearch()) { |
| SuggestionSource source = new SearchableSuggestionSource(mContext, searchable); |
| if (isTrustedSource(source)) { |
| trusted.add(source); |
| } else { |
| untrusted.add(source); |
| } |
| } |
| for (SuggestionSource s : trusted) { |
| addSuggestionSource(s); |
| } |
| for (SuggestionSource s : untrusted) { |
| addSuggestionSource(s); |
| } |
| } |
| |
| private void addSuggestionSource(SuggestionSource source) { |
| if (DBG) Log.d(TAG, "Adding source: " + source); |
| SuggestionSource old = mSuggestionSources.put(source); |
| if (old != null) { |
| Log.w(TAG, "Replaced source " + old + " for " + source.getComponentName()); |
| } |
| } |
| |
| /** |
| * Computes the list of enabled suggestion sources. |
| */ |
| private ArrayList<SuggestionSource> findEnabledSuggestionSources() { |
| ArrayList<SuggestionSource> enabledSources = new ArrayList<SuggestionSource>(); |
| for (SuggestionSource source : mSuggestionSources.values()) { |
| if (isSourceEnabled(source)) { |
| if (DBG) Log.d(TAG, "Adding enabled source " + source); |
| enabledSources.add(source); |
| } |
| } |
| return enabledSources; |
| } |
| |
| private boolean isSourceEnabled(SuggestionSource source) { |
| boolean defaultEnabled = isSourceDefaultEnabled(source); |
| if (mPreferences == null) { |
| Log.w(TAG, "Search preferences " + PREFERENCES_NAME + " not found."); |
| return true; |
| } |
| String sourceEnabledPref = getSourceEnabledPreference(source); |
| return mPreferences.getBoolean(sourceEnabledPref, defaultEnabled); |
| } |
| |
| public boolean isTrustedSource(SuggestionSource source) { |
| if (source == null) return false; |
| final String packageName = source.getComponentName().getPackageName(); |
| return mTrustedPackages != null && mTrustedPackages.contains(packageName); |
| } |
| |
| /** |
| * Finds the selected web search source. |
| */ |
| private SuggestionSource findWebSearchSource() { |
| SuggestionSource webSearchSource = null; |
| if (Settings.System.getInt(mContext.getContentResolver(), |
| Settings.System.SHOW_WEB_SUGGESTIONS, |
| 1 /* default on until user actually changes it */) == 1) { |
| SearchableInfo webSearchable = mSearchManager.getDefaultSearchableForWebSearch(); |
| if (webSearchable != null) { |
| if (DBG) Log.d(TAG, "Adding web source " + webSearchable.getSearchActivity()); |
| // Construct a SearchableSuggestionSource around the web search source. Allow |
| // the web search source to provide a larger number of results with |
| // WEB_RESULTS_OVERRIDE_LIMIT. |
| webSearchSource = SearchableSuggestionSource.create( |
| mContext, webSearchable.getSearchActivity(), WEB_RESULTS_OVERRIDE_LIMIT); |
| } |
| } |
| return webSearchSource; |
| } |
| |
| /** |
| * This works like a map from ComponentName to SuggestionSource, |
| * but supports a zero-allocation method for listing all the sources. |
| */ |
| private static class SourceList { |
| |
| private HashMap<ComponentName,SuggestionSource> mSourcesByComponent; |
| private ArrayList<SuggestionSource> mSources; |
| |
| public SourceList() { |
| mSourcesByComponent = new HashMap<ComponentName,SuggestionSource>(); |
| mSources = new ArrayList<SuggestionSource>(); |
| } |
| |
| public SuggestionSource get(ComponentName componentName) { |
| return mSourcesByComponent.get(componentName); |
| } |
| |
| /** |
| * Adds a source. Replaces any previous source with the same component name. |
| * |
| * @return The previous source that was replaced, if any. |
| */ |
| public SuggestionSource put(SuggestionSource source) { |
| if (source == null) { |
| return null; |
| } |
| SuggestionSource old = mSourcesByComponent.put(source.getComponentName(), source); |
| if (old != null) { |
| // linear search is ok here, since addSource() is only called when the |
| // list of sources is updated, which is infrequent. Also, collisions would only |
| // happen if there are two sources with the same component name, which should |
| // only happen as long as we have hard-coded sources. |
| mSources.remove(old); |
| } |
| mSources.add(source); |
| return old; |
| } |
| |
| /** |
| * Gets the suggestion sources. |
| */ |
| public ArrayList<SuggestionSource> values() { |
| return mSources; |
| } |
| |
| /** |
| * Checks whether the list is empty. |
| */ |
| public boolean isEmpty() { |
| return mSources.isEmpty(); |
| } |
| } |
| |
| /** |
| * ContentObserver which updates the list of enabled sources to include or exclude |
| * the web search provider depending on the state of the |
| * {@link Settings.System#SHOW_WEB_SUGGESTIONS} setting. |
| */ |
| private class ShowWebSuggestionsSettingChangeObserver extends ContentObserver { |
| public ShowWebSuggestionsSettingChangeObserver() { |
| super(new Handler()); |
| } |
| |
| @Override |
| public void onChange(boolean selfChange) { |
| updateSources(); |
| } |
| } |
| } |