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 com.android.common.SharedPreferencesCompat;
20
21import android.app.SearchManager;
22import android.content.Context;
23import android.content.Intent;
24import android.content.SharedPreferences;
25import android.content.SharedPreferences.Editor;
26import android.util.Log;
27import android.view.Menu;
28import android.view.MenuInflater;
29import android.view.MenuItem;
30
31/**
32 * Manages user settings.
33 */
34public class SearchSettingsImpl implements SearchSettings {
35
36    private static final boolean DBG = false;
37    private static final String TAG = "QSB.SearchSettingsImpl";
38
39    // Name of the preferences file used to store search preference
40    public static final String PREFERENCES_NAME = "SearchSettings";
41
42    // Intent action that opens the "Searchable Items" preference
43    private static final String ACTION_SEARCHABLE_ITEMS =
44            "com.android.quicksearchbox.action.SEARCHABLE_ITEMS";
45
46    /**
47     * Preference key used for storing the index of the next voice search hint to show.
48     */
49    private static final String NEXT_VOICE_SEARCH_HINT_INDEX_PREF = "next_voice_search_hint";
50
51    /**
52     * Preference key used to store the time at which the first voice search hint was displayed.
53     */
54    private static final String FIRST_VOICE_HINT_DISPLAY_TIME = "first_voice_search_hint_time";
55
56    /**
57     * Preference key for the version of voice search we last got hints from.
58     */
59    private static final String LAST_SEEN_VOICE_SEARCH_VERSION = "voice_search_version";
60
61    /**
62     * Preference key for storing whether searches always go to google.com. Public
63     * so that it can be used by PreferenceControllers.
64     */
65    public static final String USE_GOOGLE_COM_PREF = "use_google_com";
66
67    /**
68     * Preference key for the base search URL. This value is normally set by
69     * a SearchBaseUrlHelper instance. Public so classes can listen to changes
70     * on this key.
71     */
72    public static final String SEARCH_BASE_DOMAIN_PREF = "search_base_domain";
73
74    /**
75     * This is the time at which the base URL was stored, and is set using
76     * @link{System.currentTimeMillis()}.
77     */
78    private static final String SEARCH_BASE_DOMAIN_APPLY_TIME = "search_base_domain_apply_time";
79
80    /**
81     * Prefix of per-corpus enable preference
82     */
83    private static final String CORPUS_ENABLED_PREF_PREFIX = "enable_corpus_";
84
85    private final Context mContext;
86
87    private final Config mConfig;
88
89    public SearchSettingsImpl(Context context, Config config) {
90        mContext = context;
91        mConfig = config;
92    }
93
94    protected Context getContext() {
95        return mContext;
96    }
97
98    protected Config getConfig() {
99        return mConfig;
100    }
101
102    public void upgradeSettingsIfNeeded() {
103    }
104
105    public Intent getSearchableItemsIntent() {
106        Intent intent = new Intent(ACTION_SEARCHABLE_ITEMS);
107        intent.setPackage(getContext().getPackageName());
108        return intent;
109    }
110
111    /**
112     * Gets the preference key of the preference for whether the given corpus
113     * is enabled. The preference is stored in the {@link #PREFERENCES_NAME}
114     * preferences file.
115     */
116    public static String getCorpusEnabledPreference(Corpus corpus) {
117        return CORPUS_ENABLED_PREF_PREFIX + corpus.getName();
118    }
119
120    public boolean isCorpusEnabled(Corpus corpus) {
121        boolean defaultEnabled = corpus.isCorpusDefaultEnabled();
122        String sourceEnabledPref = getCorpusEnabledPreference(corpus);
123        return getSearchPreferences().getBoolean(sourceEnabledPref, defaultEnabled);
124    }
125
126    public SharedPreferences getSearchPreferences() {
127        return getContext().getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
128    }
129
130    protected void storeBoolean(String name, boolean value) {
131        SharedPreferencesCompat.apply(getSearchPreferences().edit().putBoolean(name, value));
132    }
133
134    protected void storeInt(String name, int value) {
135        SharedPreferencesCompat.apply(getSearchPreferences().edit().putInt(name, value));
136    }
137
138    protected void storeLong(String name, long value) {
139        SharedPreferencesCompat.apply(getSearchPreferences().edit().putLong(name, value));
140    }
141
142    protected void storeString(String name, String value) {
143        SharedPreferencesCompat.apply(getSearchPreferences().edit().putString(name, value));
144    }
145
146    protected void removePref(String name) {
147        SharedPreferencesCompat.apply(getSearchPreferences().edit().remove(name));
148    }
149
150    /**
151     * Informs our listeners about the updated settings data.
152     */
153    public void broadcastSettingsChanged() {
154        // We use a message broadcast since the listeners could be in multiple processes.
155        Intent intent = new Intent(SearchManager.INTENT_ACTION_SEARCH_SETTINGS_CHANGED);
156        Log.i(TAG, "Broadcasting: " + intent);
157        getContext().sendBroadcast(intent);
158    }
159
160    public void addMenuItems(Menu menu, boolean showDisabled) {
161        MenuInflater inflater = new MenuInflater(getContext());
162        inflater.inflate(R.menu.settings, menu);
163        MenuItem item = menu.findItem(R.id.menu_settings);
164        item.setIntent(getSearchSettingsIntent());
165    }
166
167    public Intent getSearchSettingsIntent() {
168        Intent settings = new Intent(SearchManager.INTENT_ACTION_SEARCH_SETTINGS);
169        settings.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
170        settings.setPackage(getContext().getPackageName());
171        return settings;
172    }
173
174    public int getNextVoiceSearchHintIndex(int size) {
175            int i = getAndIncrementIntPreference(getSearchPreferences(),
176                    NEXT_VOICE_SEARCH_HINT_INDEX_PREF);
177            return i % size;
178    }
179
180    // TODO: Could this be made atomic to avoid races?
181    private int getAndIncrementIntPreference(SharedPreferences prefs, String name) {
182        int i = prefs.getInt(name, 0);
183        storeInt(name, i + 1);
184        return i;
185    }
186
187    public void resetVoiceSearchHintFirstSeenTime() {
188        storeLong(FIRST_VOICE_HINT_DISPLAY_TIME, System.currentTimeMillis());
189    }
190
191    public boolean haveVoiceSearchHintsExpired(int currentVoiceSearchVersion) {
192        SharedPreferences prefs = getSearchPreferences();
193
194        if (currentVoiceSearchVersion != 0) {
195            long currentTime = System.currentTimeMillis();
196            int lastVoiceSearchVersion = prefs.getInt(LAST_SEEN_VOICE_SEARCH_VERSION, 0);
197            long firstHintTime = prefs.getLong(FIRST_VOICE_HINT_DISPLAY_TIME, 0);
198            if (firstHintTime == 0 || currentVoiceSearchVersion != lastVoiceSearchVersion) {
199                SharedPreferencesCompat.apply(prefs.edit()
200                        .putInt(LAST_SEEN_VOICE_SEARCH_VERSION, currentVoiceSearchVersion)
201                        .putLong(FIRST_VOICE_HINT_DISPLAY_TIME, currentTime));
202                firstHintTime = currentTime;
203            }
204            if (currentTime - firstHintTime > getConfig().getVoiceSearchHintActivePeriod()) {
205                if (DBG) Log.d(TAG, "Voice seach hint period expired; not showing hints.");
206                return true;
207            } else {
208                return false;
209            }
210        } else {
211            if (DBG) Log.d(TAG, "Could not determine voice search version; not showing hints.");
212            return true;
213        }
214    }
215
216    public boolean allowWebSearchShortcuts() {
217        return true;
218    }
219
220    /**
221     * @return true if user searches should always be based at google.com, false
222     *     otherwise.
223     */
224    @Override
225    public boolean shouldUseGoogleCom() {
226        // Note that this preserves the old behaviour of using google.com
227        // for searches, with the gl= parameter set.
228        return getSearchPreferences().getBoolean(USE_GOOGLE_COM_PREF, true);
229    }
230
231    @Override
232    public void setUseGoogleCom(boolean useGoogleCom) {
233        storeBoolean(USE_GOOGLE_COM_PREF, useGoogleCom);
234    }
235
236    @Override
237    public long getSearchBaseDomainApplyTime() {
238        return getSearchPreferences().getLong(SEARCH_BASE_DOMAIN_APPLY_TIME, -1);
239    }
240
241    @Override
242    public String getSearchBaseDomain() {
243        // Note that the only time this will return null is on the first run
244        // of the app, or when settings have been cleared. Callers should
245        // ideally check that getSearchBaseDomainApplyTime() is not -1 before
246        // calling this function.
247        return getSearchPreferences().getString(SEARCH_BASE_DOMAIN_PREF, null);
248    }
249
250    @Override
251    public void setSearchBaseDomain(String searchBaseUrl) {
252        Editor sharedPrefEditor = getSearchPreferences().edit();
253        sharedPrefEditor.putString(SEARCH_BASE_DOMAIN_PREF, searchBaseUrl);
254        sharedPrefEditor.putLong(SEARCH_BASE_DOMAIN_APPLY_TIME, System.currentTimeMillis());
255
256        SharedPreferencesCompat.apply(sharedPrefEditor);
257    }
258}
259