1/*
2 * Copyright (C) 2013 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.inputmethod.latin.settings;
18
19import android.content.Context;
20import android.content.SharedPreferences;
21import android.content.pm.ApplicationInfo;
22import android.content.res.Configuration;
23import android.content.res.Resources;
24import android.os.Build;
25import android.preference.PreferenceManager;
26import android.util.Log;
27
28import com.android.inputmethod.compat.BuildCompatUtils;
29import com.android.inputmethod.latin.AudioAndHapticFeedbackManager;
30import com.android.inputmethod.latin.InputAttributes;
31import com.android.inputmethod.latin.R;
32import com.android.inputmethod.latin.common.StringUtils;
33import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils;
34import com.android.inputmethod.latin.utils.ResourceUtils;
35import com.android.inputmethod.latin.utils.RunInLocale;
36import com.android.inputmethod.latin.utils.StatsUtils;
37
38import java.util.Collections;
39import java.util.Locale;
40import java.util.Set;
41import java.util.concurrent.locks.ReentrantLock;
42
43import javax.annotation.Nonnull;
44
45public final class Settings implements SharedPreferences.OnSharedPreferenceChangeListener {
46    private static final String TAG = Settings.class.getSimpleName();
47    // Settings screens
48    public static final String SCREEN_ACCOUNTS = "screen_accounts";
49    public static final String SCREEN_THEME = "screen_theme";
50    public static final String SCREEN_DEBUG = "screen_debug";
51    // In the same order as xml/prefs.xml
52    public static final String PREF_AUTO_CAP = "auto_cap";
53    public static final String PREF_VIBRATE_ON = "vibrate_on";
54    public static final String PREF_SOUND_ON = "sound_on";
55    public static final String PREF_POPUP_ON = "popup_on";
56    // PREF_VOICE_MODE_OBSOLETE is obsolete. Use PREF_VOICE_INPUT_KEY instead.
57    public static final String PREF_VOICE_MODE_OBSOLETE = "voice_mode";
58    public static final String PREF_VOICE_INPUT_KEY = "pref_voice_input_key";
59    public static final String PREF_EDIT_PERSONAL_DICTIONARY = "edit_personal_dictionary";
60    public static final String PREF_CONFIGURE_DICTIONARIES_KEY = "configure_dictionaries_key";
61    // PREF_AUTO_CORRECTION_THRESHOLD_OBSOLETE is obsolete. Use PREF_AUTO_CORRECTION instead.
62    public static final String PREF_AUTO_CORRECTION_THRESHOLD_OBSOLETE =
63            "auto_correction_threshold";
64    public static final String PREF_AUTO_CORRECTION = "pref_key_auto_correction";
65    // PREF_SHOW_SUGGESTIONS_SETTING_OBSOLETE is obsolete. Use PREF_SHOW_SUGGESTIONS instead.
66    public static final String PREF_SHOW_SUGGESTIONS_SETTING_OBSOLETE = "show_suggestions_setting";
67    public static final String PREF_SHOW_SUGGESTIONS = "show_suggestions";
68    public static final String PREF_KEY_USE_CONTACTS_DICT = "pref_key_use_contacts_dict";
69    public static final String PREF_KEY_USE_PERSONALIZED_DICTS = "pref_key_use_personalized_dicts";
70    public static final String PREF_KEY_USE_DOUBLE_SPACE_PERIOD =
71            "pref_key_use_double_space_period";
72    public static final String PREF_BLOCK_POTENTIALLY_OFFENSIVE =
73            "pref_key_block_potentially_offensive";
74    public static final boolean ENABLE_SHOW_LANGUAGE_SWITCH_KEY_SETTINGS =
75            BuildCompatUtils.EFFECTIVE_SDK_INT <= Build.VERSION_CODES.KITKAT;
76    public static final boolean SHOULD_SHOW_LXX_SUGGESTION_UI =
77            BuildCompatUtils.EFFECTIVE_SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
78    public static final String PREF_SHOW_LANGUAGE_SWITCH_KEY =
79            "pref_show_language_switch_key";
80    public static final String PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST =
81            "pref_include_other_imes_in_language_switch_list";
82    public static final String PREF_CUSTOM_INPUT_STYLES = "custom_input_styles";
83    public static final String PREF_ENABLE_SPLIT_KEYBOARD = "pref_split_keyboard";
84    // TODO: consolidate key preview dismiss delay with the key preview animation parameters.
85    public static final String PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY =
86            "pref_key_preview_popup_dismiss_delay";
87    public static final String PREF_BIGRAM_PREDICTIONS = "next_word_prediction";
88    public static final String PREF_GESTURE_INPUT = "gesture_input";
89    public static final String PREF_VIBRATION_DURATION_SETTINGS =
90            "pref_vibration_duration_settings";
91    public static final String PREF_KEYPRESS_SOUND_VOLUME = "pref_keypress_sound_volume";
92    public static final String PREF_KEY_LONGPRESS_TIMEOUT = "pref_key_longpress_timeout";
93    public static final String PREF_ENABLE_EMOJI_ALT_PHYSICAL_KEY =
94            "pref_enable_emoji_alt_physical_key";
95    public static final String PREF_GESTURE_PREVIEW_TRAIL = "pref_gesture_preview_trail";
96    public static final String PREF_GESTURE_FLOATING_PREVIEW_TEXT =
97            "pref_gesture_floating_preview_text";
98    public static final String PREF_SHOW_SETUP_WIZARD_ICON = "pref_show_setup_wizard_icon";
99
100    public static final String PREF_KEY_IS_INTERNAL = "pref_key_is_internal";
101
102    public static final String PREF_ENABLE_METRICS_LOGGING = "pref_enable_metrics_logging";
103    // This preference key is deprecated. Use {@link #PREF_SHOW_LANGUAGE_SWITCH_KEY} instead.
104    // This is being used only for the backward compatibility.
105    private static final String PREF_SUPPRESS_LANGUAGE_SWITCH_KEY =
106            "pref_suppress_language_switch_key";
107
108    private static final String PREF_LAST_USED_PERSONALIZATION_TOKEN =
109            "pref_last_used_personalization_token";
110    private static final String PREF_LAST_PERSONALIZATION_DICT_WIPED_TIME =
111            "pref_last_used_personalization_dict_wiped_time";
112    private static final String PREF_CORPUS_HANDLES_FOR_PERSONALIZATION =
113            "pref_corpus_handles_for_personalization";
114
115    // Emoji
116    public static final String PREF_EMOJI_RECENT_KEYS = "emoji_recent_keys";
117    public static final String PREF_EMOJI_CATEGORY_LAST_TYPED_ID = "emoji_category_last_typed_id";
118    public static final String PREF_LAST_SHOWN_EMOJI_CATEGORY_ID = "last_shown_emoji_category_id";
119
120    private static final float UNDEFINED_PREFERENCE_VALUE_FLOAT = -1.0f;
121    private static final int UNDEFINED_PREFERENCE_VALUE_INT = -1;
122
123    private Context mContext;
124    private Resources mRes;
125    private SharedPreferences mPrefs;
126    private SettingsValues mSettingsValues;
127    private final ReentrantLock mSettingsValuesLock = new ReentrantLock();
128
129    private static final Settings sInstance = new Settings();
130
131    public static Settings getInstance() {
132        return sInstance;
133    }
134
135    public static void init(final Context context) {
136        sInstance.onCreate(context);
137    }
138
139    private Settings() {
140        // Intentional empty constructor for singleton.
141    }
142
143    private void onCreate(final Context context) {
144        mContext = context;
145        mRes = context.getResources();
146        mPrefs = PreferenceManager.getDefaultSharedPreferences(context);
147        mPrefs.registerOnSharedPreferenceChangeListener(this);
148        upgradeAutocorrectionSettings(mPrefs, mRes);
149    }
150
151    public void onDestroy() {
152        mPrefs.unregisterOnSharedPreferenceChangeListener(this);
153    }
154
155    @Override
156    public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
157        mSettingsValuesLock.lock();
158        try {
159            if (mSettingsValues == null) {
160                // TODO: Introduce a static function to register this class and ensure that
161                // loadSettings must be called before "onSharedPreferenceChanged" is called.
162                Log.w(TAG, "onSharedPreferenceChanged called before loadSettings.");
163                return;
164            }
165            loadSettings(mContext, mSettingsValues.mLocale, mSettingsValues.mInputAttributes);
166            StatsUtils.onLoadSettings(mSettingsValues);
167        } finally {
168            mSettingsValuesLock.unlock();
169        }
170    }
171
172    public void loadSettings(final Context context, final Locale locale,
173            @Nonnull final InputAttributes inputAttributes) {
174        mSettingsValuesLock.lock();
175        mContext = context;
176        try {
177            final SharedPreferences prefs = mPrefs;
178            final RunInLocale<SettingsValues> job = new RunInLocale<SettingsValues>() {
179                @Override
180                protected SettingsValues job(final Resources res) {
181                    return new SettingsValues(context, prefs, res, inputAttributes);
182                }
183            };
184            mSettingsValues = job.runInLocale(mRes, locale);
185        } finally {
186            mSettingsValuesLock.unlock();
187        }
188    }
189
190    // TODO: Remove this method and add proxy method to SettingsValues.
191    public SettingsValues getCurrent() {
192        return mSettingsValues;
193    }
194
195    public boolean isInternal() {
196        return mSettingsValues.mIsInternal;
197    }
198
199    public static int readScreenMetrics(final Resources res) {
200        return res.getInteger(R.integer.config_screen_metrics);
201    }
202
203    // Accessed from the settings interface, hence public
204    public static boolean readKeypressSoundEnabled(final SharedPreferences prefs,
205            final Resources res) {
206        return prefs.getBoolean(PREF_SOUND_ON,
207                res.getBoolean(R.bool.config_default_sound_enabled));
208    }
209
210    public static boolean readVibrationEnabled(final SharedPreferences prefs,
211            final Resources res) {
212        final boolean hasVibrator = AudioAndHapticFeedbackManager.getInstance().hasVibrator();
213        return hasVibrator && prefs.getBoolean(PREF_VIBRATE_ON,
214                res.getBoolean(R.bool.config_default_vibration_enabled));
215    }
216
217    public static boolean readAutoCorrectEnabled(final SharedPreferences prefs,
218            final Resources res) {
219        return prefs.getBoolean(PREF_AUTO_CORRECTION, true);
220    }
221
222    public static float readPlausibilityThreshold(final Resources res) {
223        return Float.parseFloat(res.getString(R.string.plausibility_threshold));
224    }
225
226    public static boolean readBlockPotentiallyOffensive(final SharedPreferences prefs,
227            final Resources res) {
228        return prefs.getBoolean(PREF_BLOCK_POTENTIALLY_OFFENSIVE,
229                res.getBoolean(R.bool.config_block_potentially_offensive));
230    }
231
232    public static boolean readFromBuildConfigIfGestureInputEnabled(final Resources res) {
233        return res.getBoolean(R.bool.config_gesture_input_enabled_by_build_config);
234    }
235
236    public static boolean readGestureInputEnabled(final SharedPreferences prefs,
237            final Resources res) {
238        return readFromBuildConfigIfGestureInputEnabled(res)
239                && prefs.getBoolean(PREF_GESTURE_INPUT, true);
240    }
241
242    public static boolean readFromBuildConfigIfToShowKeyPreviewPopupOption(final Resources res) {
243        return res.getBoolean(R.bool.config_enable_show_key_preview_popup_option);
244    }
245
246    public static boolean readKeyPreviewPopupEnabled(final SharedPreferences prefs,
247            final Resources res) {
248        final boolean defaultKeyPreviewPopup = res.getBoolean(
249                R.bool.config_default_key_preview_popup);
250        if (!readFromBuildConfigIfToShowKeyPreviewPopupOption(res)) {
251            return defaultKeyPreviewPopup;
252        }
253        return prefs.getBoolean(PREF_POPUP_ON, defaultKeyPreviewPopup);
254    }
255
256    public static int readKeyPreviewPopupDismissDelay(final SharedPreferences prefs,
257            final Resources res) {
258        return Integer.parseInt(prefs.getString(PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY,
259                Integer.toString(res.getInteger(
260                        R.integer.config_key_preview_linger_timeout))));
261    }
262
263    public static boolean readShowsLanguageSwitchKey(final SharedPreferences prefs) {
264        if (prefs.contains(PREF_SUPPRESS_LANGUAGE_SWITCH_KEY)) {
265            final boolean suppressLanguageSwitchKey = prefs.getBoolean(
266                    PREF_SUPPRESS_LANGUAGE_SWITCH_KEY, false);
267            final SharedPreferences.Editor editor = prefs.edit();
268            editor.remove(PREF_SUPPRESS_LANGUAGE_SWITCH_KEY);
269            editor.putBoolean(PREF_SHOW_LANGUAGE_SWITCH_KEY, !suppressLanguageSwitchKey);
270            editor.apply();
271        }
272        return prefs.getBoolean(PREF_SHOW_LANGUAGE_SWITCH_KEY, true);
273    }
274
275    public static String readPrefAdditionalSubtypes(final SharedPreferences prefs,
276            final Resources res) {
277        final String predefinedPrefSubtypes = AdditionalSubtypeUtils.createPrefSubtypes(
278                res.getStringArray(R.array.predefined_subtypes));
279        return prefs.getString(PREF_CUSTOM_INPUT_STYLES, predefinedPrefSubtypes);
280    }
281
282    public static void writePrefAdditionalSubtypes(final SharedPreferences prefs,
283            final String prefSubtypes) {
284        prefs.edit().putString(PREF_CUSTOM_INPUT_STYLES, prefSubtypes).apply();
285    }
286
287    public static float readKeypressSoundVolume(final SharedPreferences prefs,
288            final Resources res) {
289        final float volume = prefs.getFloat(
290                PREF_KEYPRESS_SOUND_VOLUME, UNDEFINED_PREFERENCE_VALUE_FLOAT);
291        return (volume != UNDEFINED_PREFERENCE_VALUE_FLOAT) ? volume
292                : readDefaultKeypressSoundVolume(res);
293    }
294
295    // Default keypress sound volume for unknown devices.
296    // The negative value means system default.
297    private static final String DEFAULT_KEYPRESS_SOUND_VOLUME = Float.toString(-1.0f);
298
299    public static float readDefaultKeypressSoundVolume(final Resources res) {
300        return Float.parseFloat(ResourceUtils.getDeviceOverrideValue(res,
301                R.array.keypress_volumes, DEFAULT_KEYPRESS_SOUND_VOLUME));
302    }
303
304    public static int readKeyLongpressTimeout(final SharedPreferences prefs,
305            final Resources res) {
306        final int milliseconds = prefs.getInt(
307                PREF_KEY_LONGPRESS_TIMEOUT, UNDEFINED_PREFERENCE_VALUE_INT);
308        return (milliseconds != UNDEFINED_PREFERENCE_VALUE_INT) ? milliseconds
309                : readDefaultKeyLongpressTimeout(res);
310    }
311
312    public static int readDefaultKeyLongpressTimeout(final Resources res) {
313        return res.getInteger(R.integer.config_default_longpress_key_timeout);
314    }
315
316    public static int readKeypressVibrationDuration(final SharedPreferences prefs,
317            final Resources res) {
318        final int milliseconds = prefs.getInt(
319                PREF_VIBRATION_DURATION_SETTINGS, UNDEFINED_PREFERENCE_VALUE_INT);
320        return (milliseconds != UNDEFINED_PREFERENCE_VALUE_INT) ? milliseconds
321                : readDefaultKeypressVibrationDuration(res);
322    }
323
324    // Default keypress vibration duration for unknown devices.
325    // The negative value means system default.
326    private static final String DEFAULT_KEYPRESS_VIBRATION_DURATION = Integer.toString(-1);
327
328    public static int readDefaultKeypressVibrationDuration(final Resources res) {
329        return Integer.parseInt(ResourceUtils.getDeviceOverrideValue(res,
330                R.array.keypress_vibration_durations, DEFAULT_KEYPRESS_VIBRATION_DURATION));
331    }
332
333    public static float readKeyPreviewAnimationScale(final SharedPreferences prefs,
334            final String prefKey, final float defaultValue) {
335        final float fraction = prefs.getFloat(prefKey, UNDEFINED_PREFERENCE_VALUE_FLOAT);
336        return (fraction != UNDEFINED_PREFERENCE_VALUE_FLOAT) ? fraction : defaultValue;
337    }
338
339    public static int readKeyPreviewAnimationDuration(final SharedPreferences prefs,
340            final String prefKey, final int defaultValue) {
341        final int milliseconds = prefs.getInt(prefKey, UNDEFINED_PREFERENCE_VALUE_INT);
342        return (milliseconds != UNDEFINED_PREFERENCE_VALUE_INT) ? milliseconds : defaultValue;
343    }
344
345    public static float readKeyboardHeight(final SharedPreferences prefs,
346            final float defaultValue) {
347        final float percentage = prefs.getFloat(
348                DebugSettings.PREF_KEYBOARD_HEIGHT_SCALE, UNDEFINED_PREFERENCE_VALUE_FLOAT);
349        return (percentage != UNDEFINED_PREFERENCE_VALUE_FLOAT) ? percentage : defaultValue;
350    }
351
352    public static boolean readUseFullscreenMode(final Resources res) {
353        return res.getBoolean(R.bool.config_use_fullscreen_mode);
354    }
355
356    public static boolean readShowSetupWizardIcon(final SharedPreferences prefs,
357            final Context context) {
358        if (!prefs.contains(PREF_SHOW_SETUP_WIZARD_ICON)) {
359            final ApplicationInfo appInfo = context.getApplicationInfo();
360            final boolean isApplicationInSystemImage =
361                    (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
362            // Default value
363            return !isApplicationInSystemImage;
364        }
365        return prefs.getBoolean(PREF_SHOW_SETUP_WIZARD_ICON, false);
366    }
367
368    public static boolean readHasHardwareKeyboard(final Configuration conf) {
369        // The standard way of finding out whether we have a hardware keyboard. This code is taken
370        // from InputMethodService#onEvaluateInputShown, which canonically determines this.
371        // In a nutshell, we have a keyboard if the configuration says the type of hardware keyboard
372        // is NOKEYS and if it's not hidden (e.g. folded inside the device).
373        return conf.keyboard != Configuration.KEYBOARD_NOKEYS
374                && conf.hardKeyboardHidden != Configuration.HARDKEYBOARDHIDDEN_YES;
375    }
376
377    public static boolean isInternal(final SharedPreferences prefs) {
378        return prefs.getBoolean(PREF_KEY_IS_INTERNAL, false);
379    }
380
381    public void writeLastUsedPersonalizationToken(byte[] token) {
382        if (token == null) {
383            mPrefs.edit().remove(PREF_LAST_USED_PERSONALIZATION_TOKEN).apply();
384        } else {
385            final String tokenStr = StringUtils.byteArrayToHexString(token);
386            mPrefs.edit().putString(PREF_LAST_USED_PERSONALIZATION_TOKEN, tokenStr).apply();
387        }
388    }
389
390    public byte[] readLastUsedPersonalizationToken() {
391        final String tokenStr = mPrefs.getString(PREF_LAST_USED_PERSONALIZATION_TOKEN, null);
392        return StringUtils.hexStringToByteArray(tokenStr);
393    }
394
395    public void writeLastPersonalizationDictWipedTime(final long timestamp) {
396        mPrefs.edit().putLong(PREF_LAST_PERSONALIZATION_DICT_WIPED_TIME, timestamp).apply();
397    }
398
399    public long readLastPersonalizationDictGeneratedTime() {
400        return mPrefs.getLong(PREF_LAST_PERSONALIZATION_DICT_WIPED_TIME, 0);
401    }
402
403    public void writeCorpusHandlesForPersonalization(final Set<String> corpusHandles) {
404        mPrefs.edit().putStringSet(PREF_CORPUS_HANDLES_FOR_PERSONALIZATION, corpusHandles).apply();
405    }
406
407    public Set<String> readCorpusHandlesForPersonalization() {
408        final Set<String> emptySet = Collections.emptySet();
409        return mPrefs.getStringSet(PREF_CORPUS_HANDLES_FOR_PERSONALIZATION, emptySet);
410    }
411
412    public static void writeEmojiRecentKeys(final SharedPreferences prefs, String str) {
413        prefs.edit().putString(PREF_EMOJI_RECENT_KEYS, str).apply();
414    }
415
416    public static String readEmojiRecentKeys(final SharedPreferences prefs) {
417        return prefs.getString(PREF_EMOJI_RECENT_KEYS, "");
418    }
419
420    public static void writeLastTypedEmojiCategoryPageId(
421            final SharedPreferences prefs, final int categoryId, final int categoryPageId) {
422        final String key = PREF_EMOJI_CATEGORY_LAST_TYPED_ID + categoryId;
423        prefs.edit().putInt(key, categoryPageId).apply();
424    }
425
426    public static int readLastTypedEmojiCategoryPageId(
427            final SharedPreferences prefs, final int categoryId) {
428        final String key = PREF_EMOJI_CATEGORY_LAST_TYPED_ID + categoryId;
429        return prefs.getInt(key, 0);
430    }
431
432    public static void writeLastShownEmojiCategoryId(
433            final SharedPreferences prefs, final int categoryId) {
434        prefs.edit().putInt(PREF_LAST_SHOWN_EMOJI_CATEGORY_ID, categoryId).apply();
435    }
436
437    public static int readLastShownEmojiCategoryId(
438            final SharedPreferences prefs, final int defValue) {
439        return prefs.getInt(PREF_LAST_SHOWN_EMOJI_CATEGORY_ID, defValue);
440    }
441
442    private void upgradeAutocorrectionSettings(final SharedPreferences prefs, final Resources res) {
443        final String thresholdSetting =
444                prefs.getString(PREF_AUTO_CORRECTION_THRESHOLD_OBSOLETE, null);
445        if (thresholdSetting != null) {
446            SharedPreferences.Editor editor = prefs.edit();
447            editor.remove(PREF_AUTO_CORRECTION_THRESHOLD_OBSOLETE);
448            final String autoCorrectionOff =
449                    res.getString(R.string.auto_correction_threshold_mode_index_off);
450            if (thresholdSetting.equals(autoCorrectionOff)) {
451                editor.putBoolean(PREF_AUTO_CORRECTION, false);
452            } else {
453                editor.putBoolean(PREF_AUTO_CORRECTION, true);
454            }
455            editor.commit();
456        }
457    }
458}
459