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