• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.globalsearch;
18 
19 import android.app.SearchManager;
20 import android.content.BroadcastReceiver;
21 import android.content.ComponentName;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.IntentFilter;
25 import android.content.SharedPreferences;
26 import android.content.pm.PackageManager.NameNotFoundException;
27 import android.database.ContentObserver;
28 import android.os.Handler;
29 import android.provider.Settings;
30 import android.server.search.SearchableInfo;
31 import android.util.Log;
32 
33 import java.util.ArrayList;
34 import java.util.Collection;
35 import java.util.Collections;
36 import java.util.HashMap;
37 import java.util.HashSet;
38 import java.util.List;
39 
40 /**
41  * Maintains the list of all suggestion sources.
42  */
43 public class SuggestionSources implements SourceLookup {
44 
45     // set to true to enable the more verbose debug logging for this file
46     private static final boolean DBG = false;
47     private static final String TAG = "SuggestionSources";
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 final Context mContext;
56     private final SearchManager mSearchManager;
57     private final SharedPreferences mPreferences;
58     private HashSet<String> mTrustedPackages;
59     private boolean mLoaded;
60 
61     // All available suggestion sources.
62     private SourceList mSuggestionSources;
63 
64     // The web search source to use. This is the source selected in the preferences,
65     // or the default source if no source has been selected.
66     private SuggestionSource mSelectedWebSearchSource;
67 
68     // All enabled suggestion sources. This does not include the web search source.
69     private ArrayList<SuggestionSource> mEnabledSuggestionSources;
70 
71     // Updates the inclusion of the web search provider.
72     private ShowWebSuggestionsSettingChangeObserver mShowWebSuggestionsSettingChangeObserver;
73 
74     /**
75      *
76      * @param context Used for looking up source information etc.
77      */
SuggestionSources(Context context)78     public SuggestionSources(Context context) {
79         mContext = context;
80         mSearchManager = (SearchManager) context.getSystemService(Context.SEARCH_SERVICE);
81         mPreferences = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
82         mLoaded = false;
83     }
84 
85     /**
86      * Gets all suggestion sources. This does not include any web search sources.
87      *
88      * @return A list of suggestion sources, including sources that are not enabled.
89      *         Callers must not modify the returned list.
90      */
getSuggestionSources()91     public synchronized Collection<SuggestionSource> getSuggestionSources() {
92         if (!mLoaded) {
93             Log.w(TAG, "getSuggestionSources() called, but sources not loaded.");
94             return Collections.<SuggestionSource>emptyList();
95         }
96         return mSuggestionSources.values();
97     }
98 
99     /** {@inheritDoc} */
getSourceByComponentName(ComponentName componentName)100     public synchronized SuggestionSource getSourceByComponentName(ComponentName componentName) {
101         SuggestionSource source = mSuggestionSources.get(componentName);
102 
103         // If the source was not found, back off to check the web source in case it's that.
104         if (source == null) {
105             if (mSelectedWebSearchSource != null &&
106                     mSelectedWebSearchSource.getComponentName().equals(componentName)) {
107                 source = mSelectedWebSearchSource;
108             }
109         }
110         return source;
111     }
112 
113     /**
114      * Gets all enabled suggestion sources.
115      *
116      * @return All enabled suggestion sources (does not include the web search source).
117      *         Callers must not modify the returned list.
118      */
getEnabledSuggestionSources()119     public synchronized List<SuggestionSource> getEnabledSuggestionSources() {
120         if (!mLoaded) {
121             Log.w(TAG, "getEnabledSuggestionSources() called, but sources not loaded.");
122             return Collections.<SuggestionSource>emptyList();
123         }
124         return mEnabledSuggestionSources;
125     }
126 
127     /**
128      * Checks whether a suggestion source is enabled by default. For now, only trusted sources are.
129      */
isSourceDefaultEnabled(SuggestionSource source)130     public boolean isSourceDefaultEnabled(SuggestionSource source) {
131         return isTrustedSource(source);
132     }
133 
134     /** {@inheritDoc} */
getSelectedWebSearchSource()135     public synchronized SuggestionSource getSelectedWebSearchSource() {
136         if (!mLoaded) {
137             Log.w(TAG, "getSelectedWebSearchSource() called, but sources not loaded.");
138             return null;
139         }
140         return mSelectedWebSearchSource;
141     }
142 
143     /**
144      * Gets the preference key of the preference for whether the given source
145      * is enabled. The preference is stored in the {@link #PREFERENCES_NAME}
146      * preferences file.
147      */
getSourceEnabledPreference(SuggestionSource source)148     public String getSourceEnabledPreference(SuggestionSource source) {
149         return "enable_source_" + source.getComponentName().flattenToString();
150     }
151 
152     // Broadcast receiver for package change notifications
153     private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
154         @Override
155         public void onReceive(Context context, Intent intent) {
156             String action = intent.getAction();
157             if (SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED.equals(action)
158                     || SearchManager.INTENT_ACTION_SEARCH_SETTINGS_CHANGED.equals(action)) {
159                 // TODO: Instead of rebuilding the whole list on every change,
160                 // just add, remove or update the application that has changed.
161                 // Adding and updating seem tricky, since I can't see an easy way to list the
162                 // launchable activities in a given package.
163                 updateSources();
164             }
165         }
166     };
167 
168     /**
169      * After calling, clients must call {@link #close()} when done with this object.
170      */
load()171     public synchronized void load() {
172         if (mLoaded) {
173             Log.w(TAG, "Already loaded, ignoring call to load().");
174             return;
175         }
176 
177         loadTrustedPackages();
178 
179         // Listen for searchables changes.
180         mContext.registerReceiver(mBroadcastReceiver,
181                 new IntentFilter(SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED));
182 
183         // Listen for search preference changes.
184         mContext.registerReceiver(mBroadcastReceiver,
185                 new IntentFilter(SearchManager.INTENT_ACTION_SEARCH_SETTINGS_CHANGED));
186 
187         mShowWebSuggestionsSettingChangeObserver = new ShowWebSuggestionsSettingChangeObserver();
188         mContext.getContentResolver().registerContentObserver(
189                 Settings.System.getUriFor(Settings.System.SHOW_WEB_SUGGESTIONS),
190                 true,
191                 mShowWebSuggestionsSettingChangeObserver);
192 
193         // update list of sources
194         updateSources();
195         mLoaded = true;
196     }
197 
198     /**
199      * Releases all resources used by this object. It is possible to call
200      * {@link #load()} again after calling this method.
201      */
close()202     public synchronized void close() {
203         if (!mLoaded) {
204             Log.w(TAG, "Not loaded, ignoring call to close().");
205             return;
206         }
207         mContext.unregisterReceiver(mBroadcastReceiver);
208         mContext.getContentResolver().unregisterContentObserver(
209                 mShowWebSuggestionsSettingChangeObserver);
210 
211         mSuggestionSources = null;
212         mSelectedWebSearchSource = null;
213         mEnabledSuggestionSources = null;
214         mLoaded = false;
215     }
216 
loadTrustedPackages()217     private void loadTrustedPackages() {
218         mTrustedPackages = new HashSet<String>();
219 
220         // Get the list of trusted packages from a resource, which allows vendor overlays.
221         String[] trustedPackages = mContext.getResources().getStringArray(
222                 R.array.trusted_search_providers);
223 
224         if (trustedPackages == null) {
225             Log.w(TAG, "Could not load list of trusted search providers, trusting none");
226             return;
227         }
228 
229         for (String trustedPackage : trustedPackages) {
230             mTrustedPackages.add(trustedPackage);
231         }
232     }
233 
234     /**
235      * Loads the list of suggestion sources. This method is package private so that
236      * it can be called efficiently from inner classes.
237      */
updateSources()238     /* package */ synchronized void updateSources() {
239         mSuggestionSources = new SourceList();
240         addExternalSources();
241 
242         mEnabledSuggestionSources = findEnabledSuggestionSources();
243         mSelectedWebSearchSource = findWebSearchSource();
244     }
245 
addExternalSources()246     private void addExternalSources() {
247         ArrayList<SuggestionSource> trusted = new ArrayList<SuggestionSource>();
248         ArrayList<SuggestionSource> untrusted = new ArrayList<SuggestionSource>();
249         for (SearchableInfo searchable : mSearchManager.getSearchablesInGlobalSearch()) {
250             try {
251                 SuggestionSource source = new SearchableSuggestionSource(mContext, searchable);
252                 if (isTrustedSource(source)) {
253                     trusted.add(source);
254                 } else {
255                     untrusted.add(source);
256                 }
257             } catch (NameNotFoundException ex) {
258                 Log.e(TAG, "Searchable activity not found: " + ex.getMessage());
259             }
260         }
261         for (SuggestionSource s : trusted) {
262             addSuggestionSource(s);
263         }
264         for (SuggestionSource s : untrusted) {
265             addSuggestionSource(s);
266         }
267     }
268 
addSuggestionSource(SuggestionSource source)269     private void addSuggestionSource(SuggestionSource source) {
270         if (DBG) Log.d(TAG, "Adding source: " + source);
271         SuggestionSource old = mSuggestionSources.put(source);
272         if (old != null) {
273             Log.w(TAG, "Replaced source " + old + " for " + source.getComponentName());
274         }
275     }
276 
277     /**
278      * Computes the list of enabled suggestion sources.
279      */
findEnabledSuggestionSources()280     private ArrayList<SuggestionSource> findEnabledSuggestionSources() {
281         ArrayList<SuggestionSource> enabledSources = new ArrayList<SuggestionSource>();
282         for (SuggestionSource source : mSuggestionSources.values()) {
283             if (isSourceEnabled(source)) {
284                 if (DBG) Log.d(TAG, "Adding enabled source " + source);
285                 enabledSources.add(source);
286             }
287         }
288         return enabledSources;
289     }
290 
isSourceEnabled(SuggestionSource source)291     private boolean isSourceEnabled(SuggestionSource source) {
292         boolean defaultEnabled = isSourceDefaultEnabled(source);
293         if (mPreferences == null) {
294             Log.w(TAG, "Search preferences " + PREFERENCES_NAME + " not found.");
295             return true;
296         }
297         String sourceEnabledPref = getSourceEnabledPreference(source);
298         return mPreferences.getBoolean(sourceEnabledPref, defaultEnabled);
299     }
300 
isTrustedSource(SuggestionSource source)301     public boolean isTrustedSource(SuggestionSource source) {
302         if (source == null) return false;
303         final String packageName = source.getComponentName().getPackageName();
304         return mTrustedPackages != null && mTrustedPackages.contains(packageName);
305     }
306 
307     /**
308      * Finds the selected web search source.
309      */
findWebSearchSource()310     private SuggestionSource findWebSearchSource() {
311         SuggestionSource webSearchSource = null;
312         if (Settings.System.getInt(mContext.getContentResolver(),
313                 Settings.System.SHOW_WEB_SUGGESTIONS,
314                 1 /* default on until user actually changes it */) == 1) {
315             SearchableInfo webSearchable = mSearchManager.getDefaultSearchableForWebSearch();
316             if (webSearchable != null) {
317                 if (DBG) Log.d(TAG, "Adding web source " + webSearchable.getSearchActivity());
318                 // Construct a SearchableSuggestionSource around the web search source. Allow
319                 // the web search source to provide a larger number of results with
320                 // WEB_RESULTS_OVERRIDE_LIMIT.
321                 try {
322                     webSearchSource =
323                             new SearchableSuggestionSource(mContext, webSearchable, true);
324                 } catch (NameNotFoundException ex) {
325                     Log.e(TAG, "Searchable activity not found: " + ex.getMessage());
326                 }
327             }
328         }
329         return webSearchSource;
330     }
331 
332     /**
333      * This works like a map from ComponentName to SuggestionSource,
334      * but supports a zero-allocation method for listing all the sources.
335      */
336     private static class SourceList {
337 
338         private HashMap<ComponentName,SuggestionSource> mSourcesByComponent;
339         private ArrayList<SuggestionSource> mSources;
340 
SourceList()341         public SourceList() {
342             mSourcesByComponent = new HashMap<ComponentName,SuggestionSource>();
343             mSources = new ArrayList<SuggestionSource>();
344         }
345 
get(ComponentName componentName)346         public SuggestionSource get(ComponentName componentName) {
347             return mSourcesByComponent.get(componentName);
348         }
349 
350         /**
351          * Adds a source. Replaces any previous source with the same component name.
352          *
353          * @return The previous source that was replaced, if any.
354          */
put(SuggestionSource source)355         public SuggestionSource put(SuggestionSource source) {
356             if (source == null) {
357                 return null;
358             }
359             SuggestionSource old = mSourcesByComponent.put(source.getComponentName(), source);
360             if (old != null) {
361                 // linear search is ok here, since addSource() is only called when the
362                 // list of sources is updated, which is infrequent. Also, collisions would only
363                 // happen if there are two sources with the same component name, which should
364                 // only happen as long as we have hard-coded sources.
365                 mSources.remove(old);
366             }
367             mSources.add(source);
368             return old;
369         }
370 
371         /**
372          * Gets the suggestion sources.
373          */
values()374         public ArrayList<SuggestionSource> values() {
375             return mSources;
376         }
377 
378         /**
379          * Checks whether the list is empty.
380          */
isEmpty()381         public boolean isEmpty() {
382             return mSources.isEmpty();
383         }
384     }
385 
386     /**
387      * ContentObserver which updates the list of enabled sources to include or exclude
388      * the web search provider depending on the state of the
389      * {@link Settings.System#SHOW_WEB_SUGGESTIONS} setting.
390      */
391     private class ShowWebSuggestionsSettingChangeObserver extends ContentObserver {
ShowWebSuggestionsSettingChangeObserver()392         public ShowWebSuggestionsSettingChangeObserver() {
393             super(new Handler());
394         }
395 
396         @Override
onChange(boolean selfChange)397         public void onChange(boolean selfChange) {
398             updateSources();
399         }
400     }
401 }
402