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