SearchableSources.java revision 883c1bf364e38c5b133afb55f8493a14b65f4dd4
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.pm.PackageManager;
27import android.content.pm.PackageManager.NameNotFoundException;
28import android.database.DataSetObservable;
29import android.database.DataSetObserver;
30import android.os.Handler;
31import android.util.Log;
32
33import java.util.Collection;
34import java.util.HashMap;
35
36/**
37 * Maintains a list of search sources.
38 */
39public class SearchableSources implements Sources {
40
41    // set to true to enable the more verbose debug logging for this file
42    private static final boolean DBG = false;
43    private static final String TAG = "QSB.SearchableSources";
44
45    // The number of milliseconds that source update requests are delayed to
46    // allow grouping multiple requests.
47    private static final long UPDATE_SOURCES_DELAY_MILLIS = 200;
48
49    private final DataSetObservable mDataSetObservable = new DataSetObservable();
50
51    private final Context mContext;
52    private final SearchManager mSearchManager;
53    private boolean mLoaded;
54
55    // All suggestion sources.
56    private HashMap<ComponentName, Source> mSources;
57
58    // The web search source to use.
59    private Source mWebSearchSource;
60
61    private final Handler mUiThread;
62
63    private Runnable mUpdateSources = new Runnable() {
64        public void run() {
65            mUiThread.removeCallbacks(this);
66            updateSources();
67            notifyDataSetChanged();
68        }
69    };
70
71    /**
72     *
73     * @param context Used for looking up source information etc.
74     */
75    public SearchableSources(Context context, Handler uiThread) {
76        mContext = context;
77        mUiThread = uiThread;
78        mSearchManager = (SearchManager) context.getSystemService(Context.SEARCH_SERVICE);
79        mLoaded = false;
80    }
81
82    public Collection<Source> getSources() {
83        if (!mLoaded) {
84            throw new IllegalStateException("getSources(): sources not loaded.");
85        }
86        return mSources.values();
87    }
88
89    public Source getSource(ComponentName name) {
90        return mSources.get(name);
91    }
92
93    public Source getWebSearchSource() {
94        if (!mLoaded) {
95            throw new IllegalStateException("getWebSearchSource(): sources not loaded.");
96        }
97        return mWebSearchSource;
98    }
99
100    // Broadcast receiver for package change notifications
101    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
102        @Override
103        public void onReceive(Context context, Intent intent) {
104            String action = intent.getAction();
105            if (SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED.equals(action)
106                    || SearchManager.INTENT_ACTION_SEARCH_SETTINGS_CHANGED.equals(action)) {
107                if (DBG) Log.d(TAG, "onReceive(" + intent + ")");
108                // TODO: Instead of rebuilding the whole list on every change,
109                // just add, remove or update the application that has changed.
110                // Adding and updating seem tricky, since I can't see an easy way to list the
111                // launchable activities in a given package.
112                mUiThread.postDelayed(mUpdateSources, UPDATE_SOURCES_DELAY_MILLIS);
113            }
114        }
115    };
116
117    public void load() {
118        if (mLoaded) {
119            throw new IllegalStateException("load(): Already loaded.");
120        }
121
122        // Listen for searchables changes.
123        IntentFilter intentFilter = new IntentFilter();
124        intentFilter.addAction(SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED);
125        intentFilter.addAction(SearchManager.INTENT_ACTION_SEARCH_SETTINGS_CHANGED);
126        mContext.registerReceiver(mBroadcastReceiver, intentFilter);
127
128        // update list of sources
129        updateSources();
130
131        mLoaded = true;
132
133        notifyDataSetChanged();
134    }
135
136    public void close() {
137        mContext.unregisterReceiver(mBroadcastReceiver);
138
139        mDataSetObservable.unregisterAll();
140
141        mSources = null;
142        mLoaded = false;
143    }
144
145    /**
146     * Loads the list of suggestion sources.
147     */
148    private void updateSources() {
149        if (DBG) Log.d(TAG, "updateSources()");
150        mSources = new HashMap<ComponentName,Source>();
151        for (SearchableInfo searchable : mSearchManager.getSearchablesInGlobalSearch()) {
152            Source source = createSearchableSource(searchable);
153            if (source != null) {
154                if (DBG) Log.d(TAG, "Created source " + source);
155                addSource(source);
156            }
157        }
158
159        mWebSearchSource = createWebSearchSource();
160        addSource(mWebSearchSource);
161    }
162
163    private void addSource(Source source) {
164        mSources.put(source.getComponentName(), source);
165    }
166
167    private Source createWebSearchSource() {
168        ComponentName name = getWebSearchComponent();
169        SearchableInfo webSearchable = mSearchManager.getSearchableInfo(name);
170        if (webSearchable == null) {
171            Log.e(TAG, "Web search source " + name + " is not searchable.");
172            return null;
173        }
174        return createSearchableSource(webSearchable);
175    }
176
177    private ComponentName getWebSearchComponent() {
178        // Looks for an activity in the current package that handles ACTION_WEB_SEARCH.
179        // This indirect method is used to allow easy replacement of the web
180        // search activity when extending this package.
181        Intent webSearchIntent = new Intent(Intent.ACTION_WEB_SEARCH);
182        webSearchIntent.setPackage(mContext.getPackageName());
183        PackageManager pm = mContext.getPackageManager();
184        return webSearchIntent.resolveActivity(pm);
185    }
186
187    private Source createSearchableSource(SearchableInfo searchable) {
188        if (searchable == null) return null;
189        try {
190            return new SearchableSource(mContext, searchable);
191        } catch (NameNotFoundException ex) {
192            Log.e(TAG, "Source not found: " + ex);
193            return null;
194        }
195    }
196
197    public void registerDataSetObserver(DataSetObserver observer) {
198        mDataSetObservable.registerObserver(observer);
199    }
200
201    public void unregisterDataSetObserver(DataSetObserver observer) {
202        mDataSetObservable.unregisterObserver(observer);
203    }
204
205    protected void notifyDataSetChanged() {
206        mDataSetObservable.notifyChanged();
207    }
208}
209