aboutsummaryrefslogtreecommitdiffstats
path: root/java/src/org/kelar/inputmethod/latin/settings
diff options
context:
space:
mode:
authorAmin Bandali <bandali@kelar.org>2024-12-16 21:45:41 -0500
committerAmin Bandali <bandali@kelar.org>2025-01-11 14:17:35 -0500
commite9a0e66716dab4dd3184d009d8920de1961efdfa (patch)
tree02dcc096643d74645bf28459c2834c3d4a2ad7f2 /java/src/org/kelar/inputmethod/latin/settings
parentfb3b9360d70596d7e921de8bf7d3ca99564a077e (diff)
downloadlatinime-e9a0e66716dab4dd3184d009d8920de1961efdfa.tar.gz
latinime-e9a0e66716dab4dd3184d009d8920de1961efdfa.tar.xz
latinime-e9a0e66716dab4dd3184d009d8920de1961efdfa.zip
Rename to Kelar Keyboard (org.kelar.inputmethod.latin)
Diffstat (limited to 'java/src/org/kelar/inputmethod/latin/settings')
-rw-r--r--java/src/org/kelar/inputmethod/latin/settings/AccountsSettingsFragment.java508
-rw-r--r--java/src/org/kelar/inputmethod/latin/settings/AdditionalFeaturesSettingUtils.java57
-rw-r--r--java/src/org/kelar/inputmethod/latin/settings/AdvancedSettingsFragment.java262
-rw-r--r--java/src/org/kelar/inputmethod/latin/settings/AppearanceSettingsFragment.java46
-rw-r--r--java/src/org/kelar/inputmethod/latin/settings/CorrectionSettingsFragment.java152
-rw-r--r--java/src/org/kelar/inputmethod/latin/settings/CustomInputStylePreference.java341
-rw-r--r--java/src/org/kelar/inputmethod/latin/settings/CustomInputStyleSettingsFragment.java318
-rw-r--r--java/src/org/kelar/inputmethod/latin/settings/DebugSettings.java53
-rw-r--r--java/src/org/kelar/inputmethod/latin/settings/DebugSettingsFragment.java288
-rw-r--r--java/src/org/kelar/inputmethod/latin/settings/GestureSettingsFragment.java38
-rw-r--r--java/src/org/kelar/inputmethod/latin/settings/LocalSettingsConstants.java61
-rw-r--r--java/src/org/kelar/inputmethod/latin/settings/PreferencesSettingsFragment.java104
-rw-r--r--java/src/org/kelar/inputmethod/latin/settings/RadioButtonPreference.java97
-rw-r--r--java/src/org/kelar/inputmethod/latin/settings/SeekBarDialogPreference.java147
-rw-r--r--java/src/org/kelar/inputmethod/latin/settings/Settings.java458
-rw-r--r--java/src/org/kelar/inputmethod/latin/settings/SettingsActivity.java87
-rw-r--r--java/src/org/kelar/inputmethod/latin/settings/SettingsFragment.java101
-rw-r--r--java/src/org/kelar/inputmethod/latin/settings/SettingsValues.java453
-rw-r--r--java/src/org/kelar/inputmethod/latin/settings/SettingsValuesForSuggestion.java25
-rw-r--r--java/src/org/kelar/inputmethod/latin/settings/SpacingAndPunctuations.java155
-rw-r--r--java/src/org/kelar/inputmethod/latin/settings/SubScreenFragment.java134
-rw-r--r--java/src/org/kelar/inputmethod/latin/settings/TestFragmentActivity.java55
-rw-r--r--java/src/org/kelar/inputmethod/latin/settings/ThemeSettingsFragment.java112
-rw-r--r--java/src/org/kelar/inputmethod/latin/settings/TwoStatePreferenceHelper.java82
24 files changed, 4134 insertions, 0 deletions
diff --git a/java/src/org/kelar/inputmethod/latin/settings/AccountsSettingsFragment.java b/java/src/org/kelar/inputmethod/latin/settings/AccountsSettingsFragment.java
new file mode 100644
index 000000000..a361ad32f
--- /dev/null
+++ b/java/src/org/kelar/inputmethod/latin/settings/AccountsSettingsFragment.java
@@ -0,0 +1,508 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.kelar.inputmethod.latin.settings;
+
+import static org.kelar.inputmethod.latin.settings.LocalSettingsConstants.PREF_ACCOUNT_NAME;
+import static org.kelar.inputmethod.latin.settings.LocalSettingsConstants.PREF_ENABLE_CLOUD_SYNC;
+
+import android.Manifest;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnShowListener;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.TwoStatePreference;
+import android.text.TextUtils;
+import android.text.method.LinkMovementMethod;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import org.kelar.inputmethod.annotations.UsedForTesting;
+import org.kelar.inputmethod.latin.R;
+import org.kelar.inputmethod.latin.accounts.AccountStateChangedListener;
+import org.kelar.inputmethod.latin.accounts.LoginAccountUtils;
+import org.kelar.inputmethod.latin.define.ProductionFlags;
+import org.kelar.inputmethod.latin.permissions.PermissionsUtil;
+import org.kelar.inputmethod.latin.utils.ManagedProfileUtils;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.annotation.Nullable;
+
+/**
+ * "Accounts & Privacy" settings sub screen.
+ *
+ * This settings sub screen handles the following preferences:
+ * <li> Account selection/management for IME </li>
+ * <li> Sync preferences </li>
+ * <li> Privacy preferences </li>
+ */
+public final class AccountsSettingsFragment extends SubScreenFragment {
+ private static final String PREF_ENABLE_SYNC_NOW = "pref_enable_cloud_sync";
+ private static final String PREF_SYNC_NOW = "pref_sync_now";
+ private static final String PREF_CLEAR_SYNC_DATA = "pref_clear_sync_data";
+
+ static final String PREF_ACCCOUNT_SWITCHER = "account_switcher";
+
+ /**
+ * Onclick listener for sync now pref.
+ */
+ private final Preference.OnPreferenceClickListener mSyncNowListener =
+ new SyncNowListener();
+ /**
+ * Onclick listener for delete sync pref.
+ */
+ private final Preference.OnPreferenceClickListener mDeleteSyncDataListener =
+ new DeleteSyncDataListener();
+
+ /**
+ * Onclick listener for enable sync pref.
+ */
+ private final Preference.OnPreferenceClickListener mEnableSyncClickListener =
+ new EnableSyncClickListener();
+
+ /**
+ * Enable sync checkbox pref.
+ */
+ private TwoStatePreference mEnableSyncPreference;
+
+ /**
+ * Enable sync checkbox pref.
+ */
+ private Preference mSyncNowPreference;
+
+ /**
+ * Clear sync data pref.
+ */
+ private Preference mClearSyncDataPreference;
+
+ /**
+ * Account switcher preference.
+ */
+ private Preference mAccountSwitcher;
+
+ /**
+ * Stores if we are currently detecting a managed profile.
+ */
+ private AtomicBoolean mManagedProfileBeingDetected = new AtomicBoolean(true);
+
+ /**
+ * Stores if we have successfully detected if the device has a managed profile.
+ */
+ private AtomicBoolean mHasManagedProfile = new AtomicBoolean(false);
+
+ @Override
+ public void onCreate(final Bundle icicle) {
+ super.onCreate(icicle);
+ addPreferencesFromResource(R.xml.prefs_screen_accounts);
+
+ mAccountSwitcher = findPreference(PREF_ACCCOUNT_SWITCHER);
+ mEnableSyncPreference = (TwoStatePreference) findPreference(PREF_ENABLE_SYNC_NOW);
+ mSyncNowPreference = findPreference(PREF_SYNC_NOW);
+ mClearSyncDataPreference = findPreference(PREF_CLEAR_SYNC_DATA);
+
+ if (ProductionFlags.IS_METRICS_LOGGING_SUPPORTED) {
+ final Preference enableMetricsLogging =
+ findPreference(Settings.PREF_ENABLE_METRICS_LOGGING);
+ final Resources res = getResources();
+ if (enableMetricsLogging != null) {
+ final String enableMetricsLoggingTitle = res.getString(
+ R.string.enable_metrics_logging, getApplicationName());
+ enableMetricsLogging.setTitle(enableMetricsLoggingTitle);
+ }
+ } else {
+ removePreference(Settings.PREF_ENABLE_METRICS_LOGGING);
+ }
+
+ if (!ProductionFlags.ENABLE_USER_HISTORY_DICTIONARY_SYNC) {
+ removeSyncPreferences();
+ } else {
+ // Disable by default till we are sure we can enable this.
+ disableSyncPreferences();
+ new ManagedProfileCheckerTask(this).execute();
+ }
+ }
+
+ /**
+ * Task to check work profile. If found, it removes the sync prefs. If not,
+ * it enables them.
+ */
+ private static class ManagedProfileCheckerTask extends AsyncTask<Void, Void, Boolean> {
+ private final AccountsSettingsFragment mFragment;
+
+ private ManagedProfileCheckerTask(final AccountsSettingsFragment fragment) {
+ mFragment = fragment;
+ }
+
+ @Override
+ protected void onPreExecute() {
+ mFragment.mManagedProfileBeingDetected.set(true);
+ }
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ return ManagedProfileUtils.getInstance().hasWorkProfile(mFragment.getActivity());
+ }
+
+ @Override
+ protected void onPostExecute(final Boolean hasWorkProfile) {
+ mFragment.mHasManagedProfile.set(hasWorkProfile);
+ mFragment.mManagedProfileBeingDetected.set(false);
+ mFragment.refreshSyncSettingsUI();
+ }
+ }
+
+ private void enableSyncPreferences(final String[] accountsForLogin,
+ final String currentAccountName) {
+ if (!ProductionFlags.ENABLE_USER_HISTORY_DICTIONARY_SYNC) {
+ return;
+ }
+ mAccountSwitcher.setEnabled(true);
+
+ mEnableSyncPreference.setEnabled(true);
+ mEnableSyncPreference.setOnPreferenceClickListener(mEnableSyncClickListener);
+
+ mSyncNowPreference.setEnabled(true);
+ mSyncNowPreference.setOnPreferenceClickListener(mSyncNowListener);
+
+ mClearSyncDataPreference.setEnabled(true);
+ mClearSyncDataPreference.setOnPreferenceClickListener(mDeleteSyncDataListener);
+
+ if (currentAccountName != null) {
+ mAccountSwitcher.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(final Preference preference) {
+ if (accountsForLogin.length > 0) {
+ // TODO: Add addition of account.
+ createAccountPicker(accountsForLogin, getSignedInAccountName(),
+ new AccountChangedListener(null)).show();
+ }
+ return true;
+ }
+ });
+ }
+ }
+
+ /**
+ * Two reasons for disable - work profile or no accounts on device.
+ */
+ private void disableSyncPreferences() {
+ if (!ProductionFlags.ENABLE_USER_HISTORY_DICTIONARY_SYNC) {
+ return;
+ }
+
+ mAccountSwitcher.setEnabled(false);
+ mEnableSyncPreference.setEnabled(false);
+ mSyncNowPreference.setEnabled(false);
+ mClearSyncDataPreference.setEnabled(false);
+ }
+
+ /**
+ * Called only when ProductionFlag is turned off.
+ */
+ private void removeSyncPreferences() {
+ removePreference(PREF_ACCCOUNT_SWITCHER);
+ removePreference(PREF_ENABLE_CLOUD_SYNC);
+ removePreference(PREF_SYNC_NOW);
+ removePreference(PREF_CLEAR_SYNC_DATA);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ refreshSyncSettingsUI();
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
+ if (TextUtils.equals(key, PREF_ACCOUNT_NAME)) {
+ refreshSyncSettingsUI();
+ } else if (TextUtils.equals(key, PREF_ENABLE_CLOUD_SYNC)) {
+ mEnableSyncPreference = (TwoStatePreference) findPreference(PREF_ENABLE_SYNC_NOW);
+ final boolean syncEnabled = prefs.getBoolean(PREF_ENABLE_CLOUD_SYNC, false);
+ if (isSyncEnabled()) {
+ mEnableSyncPreference.setSummary(getString(R.string.cloud_sync_summary));
+ } else {
+ mEnableSyncPreference.setSummary(getString(R.string.cloud_sync_summary_disabled));
+ }
+ AccountStateChangedListener.onSyncPreferenceChanged(getSignedInAccountName(),
+ syncEnabled);
+ }
+ }
+
+ /**
+ * Checks different states like whether account is present or managed profile is present
+ * and sets the sync settings accordingly.
+ */
+ private void refreshSyncSettingsUI() {
+ if (!ProductionFlags.ENABLE_USER_HISTORY_DICTIONARY_SYNC) {
+ return;
+ }
+ boolean hasAccountsPermission = PermissionsUtil.checkAllPermissionsGranted(
+ getActivity(), Manifest.permission.READ_CONTACTS);
+
+ final String[] accountsForLogin = hasAccountsPermission ?
+ LoginAccountUtils.getAccountsForLogin(getActivity()) : new String[0];
+ final String currentAccount = hasAccountsPermission ? getSignedInAccountName() : null;
+
+ if (hasAccountsPermission && !mManagedProfileBeingDetected.get() &&
+ !mHasManagedProfile.get() && accountsForLogin.length > 0) {
+ // Sync can be used by user; enable all preferences.
+ enableSyncPreferences(accountsForLogin, currentAccount);
+ } else {
+ // Sync cannot be used by user; disable all preferences.
+ disableSyncPreferences();
+ }
+ refreshSyncSettingsMessaging(hasAccountsPermission, mManagedProfileBeingDetected.get(),
+ mHasManagedProfile.get(), accountsForLogin.length > 0,
+ currentAccount);
+ }
+
+ /**
+ * @param hasAccountsPermission whether the app has the permission to read accounts.
+ * @param managedProfileBeingDetected whether we are in process of determining work profile.
+ * @param hasManagedProfile whether the device has work profile.
+ * @param hasAccountsForLogin whether the device has enough accounts for login.
+ * @param currentAccount the account currently selected in the application.
+ */
+ private void refreshSyncSettingsMessaging(boolean hasAccountsPermission,
+ boolean managedProfileBeingDetected,
+ boolean hasManagedProfile,
+ boolean hasAccountsForLogin,
+ String currentAccount) {
+ if (!ProductionFlags.ENABLE_USER_HISTORY_DICTIONARY_SYNC) {
+ return;
+ }
+
+ if (!hasAccountsPermission) {
+ mEnableSyncPreference.setChecked(false);
+ mEnableSyncPreference.setSummary(getString(R.string.cloud_sync_summary_disabled));
+ mAccountSwitcher.setSummary("");
+ return;
+ } else if (managedProfileBeingDetected) {
+ // If we are determining eligiblity, we show empty summaries.
+ // Once we have some deterministic result, we set summaries based on different results.
+ mEnableSyncPreference.setSummary("");
+ mAccountSwitcher.setSummary("");
+ } else if (hasManagedProfile) {
+ mEnableSyncPreference.setSummary(
+ getString(R.string.cloud_sync_summary_disabled_work_profile));
+ } else if (!hasAccountsForLogin) {
+ mEnableSyncPreference.setSummary(getString(R.string.add_account_to_enable_sync));
+ } else if (isSyncEnabled()) {
+ mEnableSyncPreference.setSummary(getString(R.string.cloud_sync_summary));
+ } else {
+ mEnableSyncPreference.setSummary(getString(R.string.cloud_sync_summary_disabled));
+ }
+
+ // Set some interdependent settings.
+ // No account automatically turns off sync.
+ if (!managedProfileBeingDetected && !hasManagedProfile) {
+ if (currentAccount != null) {
+ mAccountSwitcher.setSummary(getString(R.string.account_selected, currentAccount));
+ } else {
+ mEnableSyncPreference.setChecked(false);
+ mAccountSwitcher.setSummary(getString(R.string.no_accounts_selected));
+ }
+ }
+ }
+
+ @Nullable
+ String getSignedInAccountName() {
+ return getSharedPreferences().getString(LocalSettingsConstants.PREF_ACCOUNT_NAME, null);
+ }
+
+ boolean isSyncEnabled() {
+ return getSharedPreferences().getBoolean(PREF_ENABLE_CLOUD_SYNC, false);
+ }
+
+ /**
+ * Creates an account picker dialog showing the given accounts in a list and selecting
+ * the selected account by default. The list of accounts must not be null/empty.
+ *
+ * Package-private for testing.
+ *
+ * @param accounts list of accounts on the device.
+ * @param selectedAccount currently selected account
+ * @param positiveButtonClickListener listener that gets called when positive button is
+ * clicked
+ */
+ @UsedForTesting
+ AlertDialog createAccountPicker(final String[] accounts,
+ final String selectedAccount,
+ final DialogInterface.OnClickListener positiveButtonClickListener) {
+ if (accounts == null || accounts.length == 0) {
+ throw new IllegalArgumentException("List of accounts must not be empty");
+ }
+
+ // See if the currently selected account is in the list.
+ // If it is, the entry is selected, and a sign-out button is provided.
+ // If it isn't, select the 0th account by default which will get picked up
+ // if the user presses OK.
+ int index = 0;
+ boolean isSignedIn = false;
+ for (int i = 0; i < accounts.length; i++) {
+ if (TextUtils.equals(accounts[i], selectedAccount)) {
+ index = i;
+ isSignedIn = true;
+ break;
+ }
+ }
+ final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
+ .setTitle(R.string.account_select_title)
+ .setSingleChoiceItems(accounts, index, null)
+ .setPositiveButton(R.string.account_select_ok, positiveButtonClickListener)
+ .setNegativeButton(R.string.account_select_cancel, null);
+ if (isSignedIn) {
+ builder.setNeutralButton(R.string.account_select_sign_out, positiveButtonClickListener);
+ }
+ return builder.create();
+ }
+
+ /**
+ * Listener for a account selection changes from the picker.
+ * Persists/removes the account to/from shared preferences and sets up sync if required.
+ */
+ class AccountChangedListener implements DialogInterface.OnClickListener {
+ /**
+ * Represents preference that should be changed based on account chosen.
+ */
+ private TwoStatePreference mDependentPreference;
+
+ AccountChangedListener(final TwoStatePreference dependentPreference) {
+ mDependentPreference = dependentPreference;
+ }
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ final String oldAccount = getSignedInAccountName();
+ switch (which) {
+ case DialogInterface.BUTTON_POSITIVE: // Signed in
+ final ListView lv = ((AlertDialog)dialog).getListView();
+ final String newAccount =
+ (String) lv.getItemAtPosition(lv.getCheckedItemPosition());
+ getSharedPreferences()
+ .edit()
+ .putString(PREF_ACCOUNT_NAME, newAccount)
+ .apply();
+ AccountStateChangedListener.onAccountSignedIn(oldAccount, newAccount);
+ if (mDependentPreference != null) {
+ mDependentPreference.setChecked(true);
+ }
+ break;
+ case DialogInterface.BUTTON_NEUTRAL: // Signed out
+ AccountStateChangedListener.onAccountSignedOut(oldAccount);
+ getSharedPreferences()
+ .edit()
+ .remove(PREF_ACCOUNT_NAME)
+ .apply();
+ break;
+ }
+ }
+ }
+
+ /**
+ * Listener that initiates the process of sync in the background.
+ */
+ class SyncNowListener implements Preference.OnPreferenceClickListener {
+ @Override
+ public boolean onPreferenceClick(final Preference preference) {
+ AccountStateChangedListener.forceSync(getSignedInAccountName());
+ return true;
+ }
+ }
+
+ /**
+ * Listener that initiates the process of deleting user's data from the cloud.
+ */
+ class DeleteSyncDataListener implements Preference.OnPreferenceClickListener {
+ @Override
+ public boolean onPreferenceClick(final Preference preference) {
+ final AlertDialog confirmationDialog = new AlertDialog.Builder(getActivity())
+ .setTitle(R.string.clear_sync_data_title)
+ .setMessage(R.string.clear_sync_data_confirmation)
+ .setPositiveButton(R.string.clear_sync_data_ok,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ AccountStateChangedListener.forceDelete(
+ getSignedInAccountName());
+ }
+ }
+ })
+ .setNegativeButton(R.string.cloud_sync_cancel, null /* OnClickListener */)
+ .create();
+ confirmationDialog.show();
+ return true;
+ }
+ }
+
+ /**
+ * Listens to events when user clicks on "Enable sync" feature.
+ */
+ class EnableSyncClickListener implements OnShowListener, Preference.OnPreferenceClickListener {
+ // TODO(cvnguyen): Write tests.
+ @Override
+ public boolean onPreferenceClick(final Preference preference) {
+ final TwoStatePreference syncPreference = (TwoStatePreference) preference;
+ if (syncPreference.isChecked()) {
+ // Uncheck for now.
+ syncPreference.setChecked(false);
+
+ // Show opt-in.
+ final AlertDialog optInDialog = new AlertDialog.Builder(getActivity())
+ .setTitle(R.string.cloud_sync_title)
+ .setMessage(R.string.cloud_sync_opt_in_text)
+ .setPositiveButton(R.string.account_select_ok,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(final DialogInterface dialog,
+ final int which) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ final Context context = getActivity();
+ final String[] accountsForLogin =
+ LoginAccountUtils.getAccountsForLogin(context);
+ createAccountPicker(accountsForLogin,
+ getSignedInAccountName(),
+ new AccountChangedListener(syncPreference))
+ .show();
+ }
+ }
+ })
+ .setNegativeButton(R.string.cloud_sync_cancel, null)
+ .create();
+ optInDialog.setOnShowListener(this);
+ optInDialog.show();
+ }
+ return true;
+ }
+
+ @Override
+ public void onShow(DialogInterface dialog) {
+ TextView messageView = (TextView) ((AlertDialog) dialog).findViewById(
+ android.R.id.message);
+ if (messageView != null) {
+ messageView.setMovementMethod(LinkMovementMethod.getInstance());
+ }
+ }
+ }
+}
diff --git a/java/src/org/kelar/inputmethod/latin/settings/AdditionalFeaturesSettingUtils.java b/java/src/org/kelar/inputmethod/latin/settings/AdditionalFeaturesSettingUtils.java
new file mode 100644
index 000000000..95e589c75
--- /dev/null
+++ b/java/src/org/kelar/inputmethod/latin/settings/AdditionalFeaturesSettingUtils.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.kelar.inputmethod.latin.settings;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceFragment;
+import android.view.inputmethod.InputMethodSubtype;
+
+import org.kelar.inputmethod.latin.RichInputMethodSubtype;
+import org.kelar.inputmethod.latin.RichInputMethodManager;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Utility class for managing additional features settings.
+ */
+@SuppressWarnings("unused")
+public class AdditionalFeaturesSettingUtils {
+ public static final int ADDITIONAL_FEATURES_SETTINGS_SIZE = 0;
+
+ private AdditionalFeaturesSettingUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static void addAdditionalFeaturesPreferences(
+ final Context context, final PreferenceFragment settingsFragment) {
+ // do nothing.
+ }
+
+ public static void readAdditionalFeaturesPreferencesIntoArray(final Context context,
+ final SharedPreferences prefs, final int[] additionalFeaturesPreferences) {
+ // do nothing.
+ }
+
+ @Nonnull
+ public static RichInputMethodSubtype createRichInputMethodSubtype(
+ @Nonnull final RichInputMethodManager imm,
+ @Nonnull final InputMethodSubtype subtype,
+ final Context context) {
+ return new RichInputMethodSubtype(subtype);
+ }
+}
diff --git a/java/src/org/kelar/inputmethod/latin/settings/AdvancedSettingsFragment.java b/java/src/org/kelar/inputmethod/latin/settings/AdvancedSettingsFragment.java
new file mode 100644
index 000000000..9f3df399e
--- /dev/null
+++ b/java/src/org/kelar/inputmethod/latin/settings/AdvancedSettingsFragment.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.kelar.inputmethod.latin.settings;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.preference.ListPreference;
+
+import org.kelar.inputmethod.latin.AudioAndHapticFeedbackManager;
+import org.kelar.inputmethod.latin.R;
+import org.kelar.inputmethod.latin.SystemBroadcastReceiver;
+
+/**
+ * "Advanced" settings sub screen.
+ *
+ * This settings sub screen handles the following advanced preferences.
+ * - Key popup dismiss delay
+ * - Keypress vibration duration
+ * - Keypress sound volume
+ * - Show app icon
+ * - Improve keyboard
+ * - Debug settings
+ */
+public final class AdvancedSettingsFragment extends SubScreenFragment {
+ @Override
+ public void onCreate(final Bundle icicle) {
+ super.onCreate(icicle);
+ addPreferencesFromResource(R.xml.prefs_screen_advanced);
+
+ final Resources res = getResources();
+ final Context context = getActivity();
+
+ // When we are called from the Settings application but we are not already running, some
+ // singleton and utility classes may not have been initialized. We have to call
+ // initialization method of these classes here. See {@link LatinIME#onCreate()}.
+ AudioAndHapticFeedbackManager.init(context);
+
+ final SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
+
+ if (!Settings.isInternal(prefs)) {
+ removePreference(Settings.SCREEN_DEBUG);
+ }
+
+ if (!AudioAndHapticFeedbackManager.getInstance().hasVibrator()) {
+ removePreference(Settings.PREF_VIBRATION_DURATION_SETTINGS);
+ }
+
+ // TODO: consolidate key preview dismiss delay with the key preview animation parameters.
+ if (!Settings.readFromBuildConfigIfToShowKeyPreviewPopupOption(res)) {
+ removePreference(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY);
+ } else {
+ // TODO: Cleanup this setup.
+ final ListPreference keyPreviewPopupDismissDelay =
+ (ListPreference) findPreference(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY);
+ final String popupDismissDelayDefaultValue = Integer.toString(res.getInteger(
+ R.integer.config_key_preview_linger_timeout));
+ keyPreviewPopupDismissDelay.setEntries(new String[] {
+ res.getString(R.string.key_preview_popup_dismiss_no_delay),
+ res.getString(R.string.key_preview_popup_dismiss_default_delay),
+ });
+ keyPreviewPopupDismissDelay.setEntryValues(new String[] {
+ "0",
+ popupDismissDelayDefaultValue
+ });
+ if (null == keyPreviewPopupDismissDelay.getValue()) {
+ keyPreviewPopupDismissDelay.setValue(popupDismissDelayDefaultValue);
+ }
+ keyPreviewPopupDismissDelay.setEnabled(
+ Settings.readKeyPreviewPopupEnabled(prefs, res));
+ }
+
+ setupKeypressVibrationDurationSettings();
+ setupKeypressSoundVolumeSettings();
+ setupKeyLongpressTimeoutSettings();
+ refreshEnablingsOfKeypressSoundAndVibrationSettings();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ final SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
+ updateListPreferenceSummaryToCurrentValue(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY);
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
+ final Resources res = getResources();
+ if (key.equals(Settings.PREF_POPUP_ON)) {
+ setPreferenceEnabled(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY,
+ Settings.readKeyPreviewPopupEnabled(prefs, res));
+ } else if (key.equals(Settings.PREF_SHOW_SETUP_WIZARD_ICON)) {
+ SystemBroadcastReceiver.toggleAppIcon(getActivity());
+ }
+ updateListPreferenceSummaryToCurrentValue(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY);
+ refreshEnablingsOfKeypressSoundAndVibrationSettings();
+ }
+
+ private void refreshEnablingsOfKeypressSoundAndVibrationSettings() {
+ final SharedPreferences prefs = getSharedPreferences();
+ final Resources res = getResources();
+ setPreferenceEnabled(Settings.PREF_VIBRATION_DURATION_SETTINGS,
+ Settings.readVibrationEnabled(prefs, res));
+ setPreferenceEnabled(Settings.PREF_KEYPRESS_SOUND_VOLUME,
+ Settings.readKeypressSoundEnabled(prefs, res));
+ }
+
+ private void setupKeypressVibrationDurationSettings() {
+ final SeekBarDialogPreference pref = (SeekBarDialogPreference)findPreference(
+ Settings.PREF_VIBRATION_DURATION_SETTINGS);
+ if (pref == null) {
+ return;
+ }
+ final SharedPreferences prefs = getSharedPreferences();
+ final Resources res = getResources();
+ pref.setInterface(new SeekBarDialogPreference.ValueProxy() {
+ @Override
+ public void writeValue(final int value, final String key) {
+ prefs.edit().putInt(key, value).apply();
+ }
+
+ @Override
+ public void writeDefaultValue(final String key) {
+ prefs.edit().remove(key).apply();
+ }
+
+ @Override
+ public int readValue(final String key) {
+ return Settings.readKeypressVibrationDuration(prefs, res);
+ }
+
+ @Override
+ public int readDefaultValue(final String key) {
+ return Settings.readDefaultKeypressVibrationDuration(res);
+ }
+
+ @Override
+ public void feedbackValue(final int value) {
+ AudioAndHapticFeedbackManager.getInstance().vibrate(value);
+ }
+
+ @Override
+ public String getValueText(final int value) {
+ if (value < 0) {
+ return res.getString(R.string.settings_system_default);
+ }
+ return res.getString(R.string.abbreviation_unit_milliseconds, value);
+ }
+ });
+ }
+
+ private void setupKeypressSoundVolumeSettings() {
+ final SeekBarDialogPreference pref = (SeekBarDialogPreference)findPreference(
+ Settings.PREF_KEYPRESS_SOUND_VOLUME);
+ if (pref == null) {
+ return;
+ }
+ final SharedPreferences prefs = getSharedPreferences();
+ final Resources res = getResources();
+ final AudioManager am = (AudioManager)getActivity().getSystemService(Context.AUDIO_SERVICE);
+ pref.setInterface(new SeekBarDialogPreference.ValueProxy() {
+ private static final float PERCENTAGE_FLOAT = 100.0f;
+
+ private float getValueFromPercentage(final int percentage) {
+ return percentage / PERCENTAGE_FLOAT;
+ }
+
+ private int getPercentageFromValue(final float floatValue) {
+ return (int)(floatValue * PERCENTAGE_FLOAT);
+ }
+
+ @Override
+ public void writeValue(final int value, final String key) {
+ prefs.edit().putFloat(key, getValueFromPercentage(value)).apply();
+ }
+
+ @Override
+ public void writeDefaultValue(final String key) {
+ prefs.edit().remove(key).apply();
+ }
+
+ @Override
+ public int readValue(final String key) {
+ return getPercentageFromValue(Settings.readKeypressSoundVolume(prefs, res));
+ }
+
+ @Override
+ public int readDefaultValue(final String key) {
+ return getPercentageFromValue(Settings.readDefaultKeypressSoundVolume(res));
+ }
+
+ @Override
+ public String getValueText(final int value) {
+ if (value < 0) {
+ return res.getString(R.string.settings_system_default);
+ }
+ return Integer.toString(value);
+ }
+
+ @Override
+ public void feedbackValue(final int value) {
+ am.playSoundEffect(
+ AudioManager.FX_KEYPRESS_STANDARD, getValueFromPercentage(value));
+ }
+ });
+ }
+
+ private void setupKeyLongpressTimeoutSettings() {
+ final SharedPreferences prefs = getSharedPreferences();
+ final Resources res = getResources();
+ final SeekBarDialogPreference pref = (SeekBarDialogPreference)findPreference(
+ Settings.PREF_KEY_LONGPRESS_TIMEOUT);
+ if (pref == null) {
+ return;
+ }
+ pref.setInterface(new SeekBarDialogPreference.ValueProxy() {
+ @Override
+ public void writeValue(final int value, final String key) {
+ prefs.edit().putInt(key, value).apply();
+ }
+
+ @Override
+ public void writeDefaultValue(final String key) {
+ prefs.edit().remove(key).apply();
+ }
+
+ @Override
+ public int readValue(final String key) {
+ return Settings.readKeyLongpressTimeout(prefs, res);
+ }
+
+ @Override
+ public int readDefaultValue(final String key) {
+ return Settings.readDefaultKeyLongpressTimeout(res);
+ }
+
+ @Override
+ public String getValueText(final int value) {
+ return res.getString(R.string.abbreviation_unit_milliseconds, value);
+ }
+
+ @Override
+ public void feedbackValue(final int value) {}
+ });
+ }
+}
diff --git a/java/src/org/kelar/inputmethod/latin/settings/AppearanceSettingsFragment.java b/java/src/org/kelar/inputmethod/latin/settings/AppearanceSettingsFragment.java
new file mode 100644
index 000000000..a294f1a6d
--- /dev/null
+++ b/java/src/org/kelar/inputmethod/latin/settings/AppearanceSettingsFragment.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.kelar.inputmethod.latin.settings;
+
+import android.os.Bundle;
+
+import org.kelar.inputmethod.latin.R;
+import org.kelar.inputmethod.latin.common.Constants;
+import org.kelar.inputmethod.latin.define.ProductionFlags;
+
+/**
+ * "Appearance" settings sub screen.
+ */
+public final class AppearanceSettingsFragment extends SubScreenFragment {
+ @Override
+ public void onCreate(final Bundle icicle) {
+ super.onCreate(icicle);
+ addPreferencesFromResource(R.xml.prefs_screen_appearance);
+ if (!ProductionFlags.IS_SPLIT_KEYBOARD_SUPPORTED ||
+ Constants.isPhone(Settings.readScreenMetrics(getResources()))) {
+ removePreference(Settings.PREF_ENABLE_SPLIT_KEYBOARD);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ CustomInputStyleSettingsFragment.updateCustomInputStylesSummary(
+ findPreference(Settings.PREF_CUSTOM_INPUT_STYLES));
+ ThemeSettingsFragment.updateKeyboardThemeSummary(findPreference(Settings.SCREEN_THEME));
+ }
+}
diff --git a/java/src/org/kelar/inputmethod/latin/settings/CorrectionSettingsFragment.java b/java/src/org/kelar/inputmethod/latin/settings/CorrectionSettingsFragment.java
new file mode 100644
index 000000000..0594ce5d1
--- /dev/null
+++ b/java/src/org/kelar/inputmethod/latin/settings/CorrectionSettingsFragment.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.kelar.inputmethod.latin.settings;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.SwitchPreference;
+import android.text.TextUtils;
+
+import org.kelar.inputmethod.dictionarypack.DictionarySettingsActivity;
+import org.kelar.inputmethod.latin.R;
+import org.kelar.inputmethod.latin.permissions.PermissionsManager;
+import org.kelar.inputmethod.latin.permissions.PermissionsUtil;
+import org.kelar.inputmethod.latin.userdictionary.UserDictionaryList;
+import org.kelar.inputmethod.latin.userdictionary.UserDictionarySettings;
+
+import java.util.TreeSet;
+
+/**
+ * "Text correction" settings sub screen.
+ *
+ * This settings sub screen handles the following text correction preferences.
+ * - Personal dictionary
+ * - Add-on dictionaries
+ * - Block offensive words
+ * - Auto-correction
+ * - Show correction suggestions
+ * - Personalized suggestions
+ * - Suggest Contact names
+ * - Next-word suggestions
+ */
+public final class CorrectionSettingsFragment extends SubScreenFragment
+ implements SharedPreferences.OnSharedPreferenceChangeListener,
+ PermissionsManager.PermissionsResultCallback {
+
+ private static final boolean DBG_USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS = false;
+ private static final boolean USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS =
+ DBG_USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS
+ || Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2;
+
+ private SwitchPreference mUseContactsPreference;
+
+ @Override
+ public void onCreate(final Bundle icicle) {
+ super.onCreate(icicle);
+ addPreferencesFromResource(R.xml.prefs_screen_correction);
+
+ final Context context = getActivity();
+ final PackageManager pm = context.getPackageManager();
+
+ final Preference dictionaryLink = findPreference(Settings.PREF_CONFIGURE_DICTIONARIES_KEY);
+ final Intent intent = dictionaryLink.getIntent();
+ intent.setClassName(context.getPackageName(), DictionarySettingsActivity.class.getName());
+ final int number = pm.queryIntentActivities(intent, 0).size();
+ if (0 >= number) {
+ removePreference(Settings.PREF_CONFIGURE_DICTIONARIES_KEY);
+ }
+
+ final Preference editPersonalDictionary =
+ findPreference(Settings.PREF_EDIT_PERSONAL_DICTIONARY);
+ final Intent editPersonalDictionaryIntent = editPersonalDictionary.getIntent();
+ final ResolveInfo ri = USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS ? null
+ : pm.resolveActivity(
+ editPersonalDictionaryIntent, PackageManager.MATCH_DEFAULT_ONLY);
+ if (ri == null) {
+ overwriteUserDictionaryPreference(editPersonalDictionary);
+ }
+
+ mUseContactsPreference = (SwitchPreference) findPreference(Settings.PREF_KEY_USE_CONTACTS_DICT);
+ turnOffUseContactsIfNoPermission();
+ }
+
+ private void overwriteUserDictionaryPreference(final Preference userDictionaryPreference) {
+ final Activity activity = getActivity();
+ final TreeSet<String> localeList = UserDictionaryList.getUserDictionaryLocalesSet(activity);
+ if (null == localeList) {
+ // The locale list is null if and only if the user dictionary service is
+ // not present or disabled. In this case we need to remove the preference.
+ getPreferenceScreen().removePreference(userDictionaryPreference);
+ } else if (localeList.size() <= 1) {
+ userDictionaryPreference.setFragment(UserDictionarySettings.class.getName());
+ // If the size of localeList is 0, we don't set the locale parameter in the
+ // extras. This will be interpreted by the UserDictionarySettings class as
+ // meaning "the current locale".
+ // Note that with the current code for UserDictionaryList#getUserDictionaryLocalesSet()
+ // the locale list always has at least one element, since it always includes the current
+ // locale explicitly. @see UserDictionaryList.getUserDictionaryLocalesSet().
+ if (localeList.size() == 1) {
+ final String locale = (String)localeList.toArray()[0];
+ userDictionaryPreference.getExtras().putString("locale", locale);
+ }
+ } else {
+ userDictionaryPreference.setFragment(UserDictionaryList.class.getName());
+ }
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) {
+ if (!TextUtils.equals(key, Settings.PREF_KEY_USE_CONTACTS_DICT)) {
+ return;
+ }
+ if (!sharedPreferences.getBoolean(key, false)) {
+ // don't care if the preference is turned off.
+ return;
+ }
+
+ // Check for permissions.
+ if (PermissionsUtil.checkAllPermissionsGranted(
+ getActivity() /* context */, Manifest.permission.READ_CONTACTS)) {
+ return; // all permissions granted, no need to request permissions.
+ }
+
+ PermissionsManager.get(getActivity() /* context */).requestPermissions(
+ this /* PermissionsResultCallback */,
+ getActivity() /* activity */,
+ Manifest.permission.READ_CONTACTS);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(boolean allGranted) {
+ turnOffUseContactsIfNoPermission();
+ }
+
+ private void turnOffUseContactsIfNoPermission() {
+ if (!PermissionsUtil.checkAllPermissionsGranted(
+ getActivity(), Manifest.permission.READ_CONTACTS)) {
+ mUseContactsPreference.setChecked(false);
+ }
+ }
+}
diff --git a/java/src/org/kelar/inputmethod/latin/settings/CustomInputStylePreference.java b/java/src/org/kelar/inputmethod/latin/settings/CustomInputStylePreference.java
new file mode 100644
index 000000000..0f4cd0da3
--- /dev/null
+++ b/java/src/org/kelar/inputmethod/latin/settings/CustomInputStylePreference.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.kelar.inputmethod.latin.settings;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.preference.DialogPreference;
+import android.preference.Preference;
+import android.util.Log;
+import android.view.View;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodSubtype;
+import android.widget.ArrayAdapter;
+import android.widget.Spinner;
+import android.widget.SpinnerAdapter;
+
+import org.kelar.inputmethod.compat.InputMethodSubtypeCompatUtils;
+import org.kelar.inputmethod.compat.ViewCompatUtils;
+import org.kelar.inputmethod.latin.R;
+import org.kelar.inputmethod.latin.RichInputMethodManager;
+import org.kelar.inputmethod.latin.utils.AdditionalSubtypeUtils;
+import org.kelar.inputmethod.latin.utils.SubtypeLocaleUtils;
+
+import java.util.TreeSet;
+
+final class CustomInputStylePreference extends DialogPreference
+ implements DialogInterface.OnCancelListener {
+ private static final boolean DEBUG_SUBTYPE_ID = false;
+
+ interface Listener {
+ public void onRemoveCustomInputStyle(CustomInputStylePreference stylePref);
+ public void onSaveCustomInputStyle(CustomInputStylePreference stylePref);
+ public void onAddCustomInputStyle(CustomInputStylePreference stylePref);
+ public SubtypeLocaleAdapter getSubtypeLocaleAdapter();
+ public KeyboardLayoutSetAdapter getKeyboardLayoutSetAdapter();
+ }
+
+ private static final String KEY_PREFIX = "subtype_pref_";
+ private static final String KEY_NEW_SUBTYPE = KEY_PREFIX + "new";
+
+ private InputMethodSubtype mSubtype;
+ private InputMethodSubtype mPreviousSubtype;
+
+ private final Listener mProxy;
+ private Spinner mSubtypeLocaleSpinner;
+ private Spinner mKeyboardLayoutSetSpinner;
+
+ public static CustomInputStylePreference newIncompleteSubtypePreference(
+ final Context context, final Listener proxy) {
+ return new CustomInputStylePreference(context, null, proxy);
+ }
+
+ public CustomInputStylePreference(final Context context, final InputMethodSubtype subtype,
+ final Listener proxy) {
+ super(context, null);
+ setDialogLayoutResource(R.layout.additional_subtype_dialog);
+ setPersistent(false);
+ mProxy = proxy;
+ setSubtype(subtype);
+ }
+
+ public void show() {
+ showDialog(null);
+ }
+
+ public final boolean isIncomplete() {
+ return mSubtype == null;
+ }
+
+ public InputMethodSubtype getSubtype() {
+ return mSubtype;
+ }
+
+ public void setSubtype(final InputMethodSubtype subtype) {
+ mPreviousSubtype = mSubtype;
+ mSubtype = subtype;
+ if (isIncomplete()) {
+ setTitle(null);
+ setDialogTitle(R.string.add_style);
+ setKey(KEY_NEW_SUBTYPE);
+ } else {
+ final String displayName =
+ SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype);
+ setTitle(displayName);
+ setDialogTitle(displayName);
+ setKey(KEY_PREFIX + subtype.getLocale() + "_"
+ + SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype));
+ }
+ }
+
+ public void revert() {
+ setSubtype(mPreviousSubtype);
+ }
+
+ public boolean hasBeenModified() {
+ return mSubtype != null && !mSubtype.equals(mPreviousSubtype);
+ }
+
+ @Override
+ protected View onCreateDialogView() {
+ final View v = super.onCreateDialogView();
+ mSubtypeLocaleSpinner = (Spinner) v.findViewById(R.id.subtype_locale_spinner);
+ mSubtypeLocaleSpinner.setAdapter(mProxy.getSubtypeLocaleAdapter());
+ mKeyboardLayoutSetSpinner = (Spinner) v.findViewById(R.id.keyboard_layout_set_spinner);
+ mKeyboardLayoutSetSpinner.setAdapter(mProxy.getKeyboardLayoutSetAdapter());
+ // All keyboard layout names are in the Latin script and thus left to right. That means
+ // the view would align them to the left even if the system locale is RTL, but that
+ // would look strange. To fix this, we align them to the view's start, which will be
+ // natural for any direction.
+ ViewCompatUtils.setTextAlignment(
+ mKeyboardLayoutSetSpinner, ViewCompatUtils.TEXT_ALIGNMENT_VIEW_START);
+ return v;
+ }
+
+ @Override
+ protected void onPrepareDialogBuilder(final AlertDialog.Builder builder) {
+ builder.setCancelable(true).setOnCancelListener(this);
+ if (isIncomplete()) {
+ builder.setPositiveButton(R.string.add, this)
+ .setNegativeButton(android.R.string.cancel, this);
+ } else {
+ builder.setPositiveButton(R.string.save, this)
+ .setNeutralButton(android.R.string.cancel, this)
+ .setNegativeButton(R.string.remove, this);
+ final SubtypeLocaleItem localeItem = new SubtypeLocaleItem(mSubtype);
+ final KeyboardLayoutSetItem layoutItem = new KeyboardLayoutSetItem(mSubtype);
+ setSpinnerPosition(mSubtypeLocaleSpinner, localeItem);
+ setSpinnerPosition(mKeyboardLayoutSetSpinner, layoutItem);
+ }
+ }
+
+ private static void setSpinnerPosition(final Spinner spinner, final Object itemToSelect) {
+ final SpinnerAdapter adapter = spinner.getAdapter();
+ final int count = adapter.getCount();
+ for (int i = 0; i < count; i++) {
+ final Object item = spinner.getItemAtPosition(i);
+ if (item.equals(itemToSelect)) {
+ spinner.setSelection(i);
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onCancel(final DialogInterface dialog) {
+ if (isIncomplete()) {
+ mProxy.onRemoveCustomInputStyle(this);
+ }
+ }
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ super.onClick(dialog, which);
+ switch (which) {
+ case DialogInterface.BUTTON_POSITIVE:
+ final boolean isEditing = !isIncomplete();
+ final SubtypeLocaleItem locale =
+ (SubtypeLocaleItem) mSubtypeLocaleSpinner.getSelectedItem();
+ final KeyboardLayoutSetItem layout =
+ (KeyboardLayoutSetItem) mKeyboardLayoutSetSpinner.getSelectedItem();
+ final InputMethodSubtype subtype =
+ AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+ locale.mLocaleString, layout.mLayoutName);
+ setSubtype(subtype);
+ notifyChanged();
+ if (isEditing) {
+ mProxy.onSaveCustomInputStyle(this);
+ } else {
+ mProxy.onAddCustomInputStyle(this);
+ }
+ break;
+ case DialogInterface.BUTTON_NEUTRAL:
+ // Nothing to do
+ break;
+ case DialogInterface.BUTTON_NEGATIVE:
+ mProxy.onRemoveCustomInputStyle(this);
+ break;
+ }
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ final Parcelable superState = super.onSaveInstanceState();
+ final Dialog dialog = getDialog();
+ if (dialog == null || !dialog.isShowing()) {
+ return superState;
+ }
+
+ final SavedState myState = new SavedState(superState);
+ myState.mSubtype = mSubtype;
+ return myState;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(final Parcelable state) {
+ if (!(state instanceof SavedState)) {
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ final SavedState myState = (SavedState) state;
+ super.onRestoreInstanceState(myState.getSuperState());
+ setSubtype(myState.mSubtype);
+ }
+
+ static final class SavedState extends Preference.BaseSavedState {
+ InputMethodSubtype mSubtype;
+
+ public SavedState(final Parcelable superState) {
+ super(superState);
+ }
+
+ @Override
+ public void writeToParcel(final Parcel dest, final int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeParcelable(mSubtype, 0);
+ }
+
+ public SavedState(final Parcel source) {
+ super(source);
+ mSubtype = (InputMethodSubtype)source.readParcelable(null);
+ }
+
+ @SuppressWarnings("hiding")
+ public static final Parcelable.Creator<SavedState> CREATOR =
+ new Parcelable.Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(final Parcel source) {
+ return new SavedState(source);
+ }
+
+ @Override
+ public SavedState[] newArray(final int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ static final class SubtypeLocaleItem implements Comparable<SubtypeLocaleItem> {
+ public final String mLocaleString;
+ private final String mDisplayName;
+
+ public SubtypeLocaleItem(final InputMethodSubtype subtype) {
+ mLocaleString = subtype.getLocale();
+ mDisplayName = SubtypeLocaleUtils.getSubtypeLocaleDisplayNameInSystemLocale(
+ mLocaleString);
+ }
+
+ // {@link ArrayAdapter<T>} that hosts the instance of this class needs {@link #toString()}
+ // to get display name.
+ @Override
+ public String toString() {
+ return mDisplayName;
+ }
+
+ @Override
+ public int compareTo(final SubtypeLocaleItem o) {
+ return mLocaleString.compareTo(o.mLocaleString);
+ }
+ }
+
+ static final class SubtypeLocaleAdapter extends ArrayAdapter<SubtypeLocaleItem> {
+ private static final String TAG_SUBTYPE = SubtypeLocaleAdapter.class.getSimpleName();
+
+ public SubtypeLocaleAdapter(final Context context) {
+ super(context, android.R.layout.simple_spinner_item);
+ setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+
+ final TreeSet<SubtypeLocaleItem> items = new TreeSet<>();
+ final InputMethodInfo imi = RichInputMethodManager.getInstance()
+ .getInputMethodInfoOfThisIme();
+ final int count = imi.getSubtypeCount();
+ for (int i = 0; i < count; i++) {
+ final InputMethodSubtype subtype = imi.getSubtypeAt(i);
+ if (DEBUG_SUBTYPE_ID) {
+ Log.d(TAG_SUBTYPE, String.format("%-6s 0x%08x %11d %s",
+ subtype.getLocale(), subtype.hashCode(), subtype.hashCode(),
+ SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype)));
+ }
+ if (InputMethodSubtypeCompatUtils.isAsciiCapable(subtype)) {
+ items.add(new SubtypeLocaleItem(subtype));
+ }
+ }
+ // TODO: Should filter out already existing combinations of locale and layout.
+ addAll(items);
+ }
+ }
+
+ static final class KeyboardLayoutSetItem {
+ public final String mLayoutName;
+ private final String mDisplayName;
+
+ public KeyboardLayoutSetItem(final InputMethodSubtype subtype) {
+ mLayoutName = SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype);
+ mDisplayName = SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(subtype);
+ }
+
+ // {@link ArrayAdapter<T>} that hosts the instance of this class needs {@link #toString()}
+ // to get display name.
+ @Override
+ public String toString() {
+ return mDisplayName;
+ }
+ }
+
+ static final class KeyboardLayoutSetAdapter extends ArrayAdapter<KeyboardLayoutSetItem> {
+ public KeyboardLayoutSetAdapter(final Context context) {
+ super(context, android.R.layout.simple_spinner_item);
+ setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+
+ final String[] predefinedKeyboardLayoutSet = context.getResources().getStringArray(
+ R.array.predefined_layouts);
+ // TODO: Should filter out already existing combinations of locale and layout.
+ for (final String layout : predefinedKeyboardLayoutSet) {
+ // This is a placeholder for a subtype with NO_LANGUAGE, only for display.
+ final InputMethodSubtype subtype =
+ AdditionalSubtypeUtils.createDummyAdditionalSubtype(
+ SubtypeLocaleUtils.NO_LANGUAGE, layout);
+ add(new KeyboardLayoutSetItem(subtype));
+ }
+ }
+ }
+}
diff --git a/java/src/org/kelar/inputmethod/latin/settings/CustomInputStyleSettingsFragment.java b/java/src/org/kelar/inputmethod/latin/settings/CustomInputStyleSettingsFragment.java
new file mode 100644
index 000000000..2e83719f2
--- /dev/null
+++ b/java/src/org/kelar/inputmethod/latin/settings/CustomInputStyleSettingsFragment.java
@@ -0,0 +1,318 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.kelar.inputmethod.latin.settings;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceGroup;
+import androidx.core.view.ViewCompat;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodSubtype;
+import android.widget.Toast;
+
+import org.kelar.inputmethod.latin.R;
+import org.kelar.inputmethod.latin.RichInputMethodManager;
+import org.kelar.inputmethod.latin.utils.AdditionalSubtypeUtils;
+import org.kelar.inputmethod.latin.utils.DialogUtils;
+import org.kelar.inputmethod.latin.utils.IntentUtils;
+import org.kelar.inputmethod.latin.utils.SubtypeLocaleUtils;
+
+import java.util.ArrayList;
+
+public final class CustomInputStyleSettingsFragment extends PreferenceFragment
+ implements CustomInputStylePreference.Listener {
+ private static final String TAG = CustomInputStyleSettingsFragment.class.getSimpleName();
+ // Note: We would like to turn this debug flag true in order to see what input styles are
+ // defined in a bug-report.
+ private static final boolean DEBUG_CUSTOM_INPUT_STYLES = true;
+
+ private RichInputMethodManager mRichImm;
+ private SharedPreferences mPrefs;
+ private CustomInputStylePreference.SubtypeLocaleAdapter mSubtypeLocaleAdapter;
+ private CustomInputStylePreference.KeyboardLayoutSetAdapter mKeyboardLayoutSetAdapter;
+
+ private boolean mIsAddingNewSubtype;
+ private AlertDialog mSubtypeEnablerNotificationDialog;
+ private String mSubtypePreferenceKeyForSubtypeEnabler;
+
+ private static final String KEY_IS_ADDING_NEW_SUBTYPE = "is_adding_new_subtype";
+ private static final String KEY_IS_SUBTYPE_ENABLER_NOTIFICATION_DIALOG_OPEN =
+ "is_subtype_enabler_notification_dialog_open";
+ private static final String KEY_SUBTYPE_FOR_SUBTYPE_ENABLER = "subtype_for_subtype_enabler";
+
+ public CustomInputStyleSettingsFragment() {
+ // Empty constructor for fragment generation.
+ }
+
+ static void updateCustomInputStylesSummary(final Preference pref) {
+ // When we are called from the Settings application but we are not already running, some
+ // singleton and utility classes may not have been initialized. We have to call
+ // initialization method of these classes here. See {@link LatinIME#onCreate()}.
+ SubtypeLocaleUtils.init(pref.getContext());
+
+ final Resources res = pref.getContext().getResources();
+ final SharedPreferences prefs = pref.getSharedPreferences();
+ final String prefSubtype = Settings.readPrefAdditionalSubtypes(prefs, res);
+ final InputMethodSubtype[] subtypes =
+ AdditionalSubtypeUtils.createAdditionalSubtypesArray(prefSubtype);
+ final ArrayList<String> subtypeNames = new ArrayList<>();
+ for (final InputMethodSubtype subtype : subtypes) {
+ subtypeNames.add(SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype));
+ }
+ // TODO: A delimiter of custom input styles should be localized.
+ pref.setSummary(TextUtils.join(", ", subtypeNames));
+ }
+
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mPrefs = getPreferenceManager().getSharedPreferences();
+ RichInputMethodManager.init(getActivity());
+ mRichImm = RichInputMethodManager.getInstance();
+ addPreferencesFromResource(R.xml.additional_subtype_settings);
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ final View view = super.onCreateView(inflater, container, savedInstanceState);
+ // For correct display in RTL locales, we need to set the layout direction of the
+ // fragment's top view.
+ ViewCompat.setLayoutDirection(view, ViewCompat.LAYOUT_DIRECTION_LOCALE);
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ final Context context = getActivity();
+ mSubtypeLocaleAdapter = new CustomInputStylePreference.SubtypeLocaleAdapter(context);
+ mKeyboardLayoutSetAdapter =
+ new CustomInputStylePreference.KeyboardLayoutSetAdapter(context);
+
+ final String prefSubtypes =
+ Settings.readPrefAdditionalSubtypes(mPrefs, getResources());
+ if (DEBUG_CUSTOM_INPUT_STYLES) {
+ Log.i(TAG, "Load custom input styles: " + prefSubtypes);
+ }
+ setPrefSubtypes(prefSubtypes, context);
+
+ mIsAddingNewSubtype = (savedInstanceState != null)
+ && savedInstanceState.containsKey(KEY_IS_ADDING_NEW_SUBTYPE);
+ if (mIsAddingNewSubtype) {
+ getPreferenceScreen().addPreference(
+ CustomInputStylePreference.newIncompleteSubtypePreference(context, this));
+ }
+
+ super.onActivityCreated(savedInstanceState);
+
+ if (savedInstanceState != null && savedInstanceState.containsKey(
+ KEY_IS_SUBTYPE_ENABLER_NOTIFICATION_DIALOG_OPEN)) {
+ mSubtypePreferenceKeyForSubtypeEnabler = savedInstanceState.getString(
+ KEY_SUBTYPE_FOR_SUBTYPE_ENABLER);
+ mSubtypeEnablerNotificationDialog = createDialog();
+ mSubtypeEnablerNotificationDialog.show();
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(final Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if (mIsAddingNewSubtype) {
+ outState.putBoolean(KEY_IS_ADDING_NEW_SUBTYPE, true);
+ }
+ if (mSubtypeEnablerNotificationDialog != null
+ && mSubtypeEnablerNotificationDialog.isShowing()) {
+ outState.putBoolean(KEY_IS_SUBTYPE_ENABLER_NOTIFICATION_DIALOG_OPEN, true);
+ outState.putString(
+ KEY_SUBTYPE_FOR_SUBTYPE_ENABLER, mSubtypePreferenceKeyForSubtypeEnabler);
+ }
+ }
+
+ @Override
+ public void onRemoveCustomInputStyle(final CustomInputStylePreference stylePref) {
+ mIsAddingNewSubtype = false;
+ final PreferenceGroup group = getPreferenceScreen();
+ group.removePreference(stylePref);
+ mRichImm.setAdditionalInputMethodSubtypes(getSubtypes());
+ }
+
+ @Override
+ public void onSaveCustomInputStyle(final CustomInputStylePreference stylePref) {
+ final InputMethodSubtype subtype = stylePref.getSubtype();
+ if (!stylePref.hasBeenModified()) {
+ return;
+ }
+ if (findDuplicatedSubtype(subtype) == null) {
+ mRichImm.setAdditionalInputMethodSubtypes(getSubtypes());
+ return;
+ }
+
+ // Saved subtype is duplicated.
+ final PreferenceGroup group = getPreferenceScreen();
+ group.removePreference(stylePref);
+ stylePref.revert();
+ group.addPreference(stylePref);
+ showSubtypeAlreadyExistsToast(subtype);
+ }
+
+ @Override
+ public void onAddCustomInputStyle(final CustomInputStylePreference stylePref) {
+ mIsAddingNewSubtype = false;
+ final InputMethodSubtype subtype = stylePref.getSubtype();
+ if (findDuplicatedSubtype(subtype) == null) {
+ mRichImm.setAdditionalInputMethodSubtypes(getSubtypes());
+ mSubtypePreferenceKeyForSubtypeEnabler = stylePref.getKey();
+ mSubtypeEnablerNotificationDialog = createDialog();
+ mSubtypeEnablerNotificationDialog.show();
+ return;
+ }
+
+ // Newly added subtype is duplicated.
+ final PreferenceGroup group = getPreferenceScreen();
+ group.removePreference(stylePref);
+ showSubtypeAlreadyExistsToast(subtype);
+ }
+
+ @Override
+ public CustomInputStylePreference.SubtypeLocaleAdapter getSubtypeLocaleAdapter() {
+ return mSubtypeLocaleAdapter;
+ }
+
+ @Override
+ public CustomInputStylePreference.KeyboardLayoutSetAdapter getKeyboardLayoutSetAdapter() {
+ return mKeyboardLayoutSetAdapter;
+ }
+
+ private void showSubtypeAlreadyExistsToast(final InputMethodSubtype subtype) {
+ final Context context = getActivity();
+ final Resources res = context.getResources();
+ final String message = res.getString(R.string.custom_input_style_already_exists,
+ SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype));
+ Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
+ }
+
+ private InputMethodSubtype findDuplicatedSubtype(final InputMethodSubtype subtype) {
+ final String localeString = subtype.getLocale();
+ final String keyboardLayoutSetName = SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype);
+ return mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
+ localeString, keyboardLayoutSetName);
+ }
+
+ private AlertDialog createDialog() {
+ final String imeId = mRichImm.getInputMethodIdOfThisIme();
+ final AlertDialog.Builder builder = new AlertDialog.Builder(
+ DialogUtils.getPlatformDialogThemeContext(getActivity()));
+ builder.setTitle(R.string.custom_input_styles_title)
+ .setMessage(R.string.custom_input_style_note_message)
+ .setNegativeButton(R.string.not_now, null)
+ .setPositiveButton(R.string.enable, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ final Intent intent = IntentUtils.getInputLanguageSelectionIntent(
+ imeId,
+ Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
+ | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ // TODO: Add newly adding subtype to extra value of the intent as a hint
+ // for the input language selection activity.
+ // intent.putExtra("newlyAddedSubtype", subtypePref.getSubtype());
+ startActivity(intent);
+ }
+ });
+
+ return builder.create();
+ }
+
+ private void setPrefSubtypes(final String prefSubtypes, final Context context) {
+ final PreferenceGroup group = getPreferenceScreen();
+ group.removeAll();
+ final InputMethodSubtype[] subtypesArray =
+ AdditionalSubtypeUtils.createAdditionalSubtypesArray(prefSubtypes);
+ for (final InputMethodSubtype subtype : subtypesArray) {
+ final CustomInputStylePreference pref =
+ new CustomInputStylePreference(context, subtype, this);
+ group.addPreference(pref);
+ }
+ }
+
+ private InputMethodSubtype[] getSubtypes() {
+ final PreferenceGroup group = getPreferenceScreen();
+ final ArrayList<InputMethodSubtype> subtypes = new ArrayList<>();
+ final int count = group.getPreferenceCount();
+ for (int i = 0; i < count; i++) {
+ final Preference pref = group.getPreference(i);
+ if (pref instanceof CustomInputStylePreference) {
+ final CustomInputStylePreference subtypePref = (CustomInputStylePreference)pref;
+ // We should not save newly adding subtype to preference because it is incomplete.
+ if (subtypePref.isIncomplete()) continue;
+ subtypes.add(subtypePref.getSubtype());
+ }
+ }
+ return subtypes.toArray(new InputMethodSubtype[subtypes.size()]);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ final String oldSubtypes = Settings.readPrefAdditionalSubtypes(mPrefs, getResources());
+ final InputMethodSubtype[] subtypes = getSubtypes();
+ final String prefSubtypes = AdditionalSubtypeUtils.createPrefSubtypes(subtypes);
+ if (DEBUG_CUSTOM_INPUT_STYLES) {
+ Log.i(TAG, "Save custom input styles: " + prefSubtypes);
+ }
+ if (prefSubtypes.equals(oldSubtypes)) {
+ return;
+ }
+ Settings.writePrefAdditionalSubtypes(mPrefs, prefSubtypes);
+ mRichImm.setAdditionalInputMethodSubtypes(subtypes);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
+ inflater.inflate(R.menu.add_style, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ final int itemId = item.getItemId();
+ if (itemId == R.id.action_add_style) {
+ final CustomInputStylePreference newSubtype =
+ CustomInputStylePreference.newIncompleteSubtypePreference(getActivity(), this);
+ getPreferenceScreen().addPreference(newSubtype);
+ newSubtype.show();
+ mIsAddingNewSubtype = true;
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/java/src/org/kelar/inputmethod/latin/settings/DebugSettings.java b/java/src/org/kelar/inputmethod/latin/settings/DebugSettings.java
new file mode 100644
index 000000000..6f26a00b7
--- /dev/null
+++ b/java/src/org/kelar/inputmethod/latin/settings/DebugSettings.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.kelar.inputmethod.latin.settings;
+
+/**
+ * Debug settings for the application.
+ *
+ * Note: Even though these settings are stored in the default shared preferences file,
+ * they shouldn't be restored across devices.
+ * If a new key is added here, it should also be blacklisted for restore in
+ * {@link LocalSettingsConstants}.
+ */
+public final class DebugSettings {
+ public static final String PREF_DEBUG_MODE = "debug_mode";
+ public static final String PREF_FORCE_NON_DISTINCT_MULTITOUCH = "force_non_distinct_multitouch";
+ public static final String PREF_HAS_CUSTOM_KEY_PREVIEW_ANIMATION_PARAMS =
+ "pref_has_custom_key_preview_animation_params";
+ public static final String PREF_RESIZE_KEYBOARD = "pref_resize_keyboard";
+ public static final String PREF_KEYBOARD_HEIGHT_SCALE = "pref_keyboard_height_scale";
+ public static final String PREF_KEY_PREVIEW_DISMISS_DURATION =
+ "pref_key_preview_dismiss_duration";
+ public static final String PREF_KEY_PREVIEW_DISMISS_END_X_SCALE =
+ "pref_key_preview_dismiss_end_x_scale";
+ public static final String PREF_KEY_PREVIEW_DISMISS_END_Y_SCALE =
+ "pref_key_preview_dismiss_end_y_scale";
+ public static final String PREF_KEY_PREVIEW_SHOW_UP_DURATION =
+ "pref_key_preview_show_up_duration";
+ public static final String PREF_KEY_PREVIEW_SHOW_UP_START_X_SCALE =
+ "pref_key_preview_show_up_start_x_scale";
+ public static final String PREF_KEY_PREVIEW_SHOW_UP_START_Y_SCALE =
+ "pref_key_preview_show_up_start_y_scale";
+ public static final String PREF_SHOULD_SHOW_LXX_SUGGESTION_UI =
+ "pref_should_show_lxx_suggestion_ui";
+ public static final String PREF_SLIDING_KEY_INPUT_PREVIEW = "pref_sliding_key_input_preview";
+
+ private DebugSettings() {
+ // This class is not publicly instantiable.
+ }
+}
diff --git a/java/src/org/kelar/inputmethod/latin/settings/DebugSettingsFragment.java b/java/src/org/kelar/inputmethod/latin/settings/DebugSettingsFragment.java
new file mode 100644
index 000000000..5cecb8155
--- /dev/null
+++ b/java/src/org/kelar/inputmethod/latin/settings/DebugSettingsFragment.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.kelar.inputmethod.latin.settings;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.os.Process;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.PreferenceGroup;
+import android.preference.TwoStatePreference;
+
+import org.kelar.inputmethod.latin.DictionaryDumpBroadcastReceiver;
+import org.kelar.inputmethod.latin.DictionaryFacilitatorImpl;
+import org.kelar.inputmethod.latin.R;
+import org.kelar.inputmethod.latin.utils.ApplicationUtils;
+import org.kelar.inputmethod.latin.utils.ResourceUtils;
+
+import java.util.Locale;
+
+/**
+ * "Debug mode" settings sub screen.
+ *
+ * This settings sub screen handles a several preference options for debugging.
+ */
+public final class DebugSettingsFragment extends SubScreenFragment
+ implements OnPreferenceClickListener {
+ private static final String PREF_KEY_DUMP_DICTS = "pref_key_dump_dictionaries";
+ private static final String PREF_KEY_DUMP_DICT_PREFIX = "pref_key_dump_dictionaries";
+
+ private boolean mServiceNeedsRestart = false;
+ private TwoStatePreference mDebugMode;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ addPreferencesFromResource(R.xml.prefs_screen_debug);
+
+ if (!Settings.SHOULD_SHOW_LXX_SUGGESTION_UI) {
+ removePreference(DebugSettings.PREF_SHOULD_SHOW_LXX_SUGGESTION_UI);
+ }
+
+ final PreferenceGroup dictDumpPreferenceGroup =
+ (PreferenceGroup)findPreference(PREF_KEY_DUMP_DICTS);
+ for (final String dictName : DictionaryFacilitatorImpl.DICT_TYPE_TO_CLASS.keySet()) {
+ final Preference pref = new DictDumpPreference(getActivity(), dictName);
+ pref.setOnPreferenceClickListener(this);
+ dictDumpPreferenceGroup.addPreference(pref);
+ }
+ final Resources res = getResources();
+ setupKeyPreviewAnimationDuration(DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_DURATION,
+ res.getInteger(R.integer.config_key_preview_show_up_duration));
+ setupKeyPreviewAnimationDuration(DebugSettings.PREF_KEY_PREVIEW_DISMISS_DURATION,
+ res.getInteger(R.integer.config_key_preview_dismiss_duration));
+ final float defaultKeyPreviewShowUpStartScale = ResourceUtils.getFloatFromFraction(
+ res, R.fraction.config_key_preview_show_up_start_scale);
+ final float defaultKeyPreviewDismissEndScale = ResourceUtils.getFloatFromFraction(
+ res, R.fraction.config_key_preview_dismiss_end_scale);
+ setupKeyPreviewAnimationScale(DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_START_X_SCALE,
+ defaultKeyPreviewShowUpStartScale);
+ setupKeyPreviewAnimationScale(DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_START_Y_SCALE,
+ defaultKeyPreviewShowUpStartScale);
+ setupKeyPreviewAnimationScale(DebugSettings.PREF_KEY_PREVIEW_DISMISS_END_X_SCALE,
+ defaultKeyPreviewDismissEndScale);
+ setupKeyPreviewAnimationScale(DebugSettings.PREF_KEY_PREVIEW_DISMISS_END_Y_SCALE,
+ defaultKeyPreviewDismissEndScale);
+ setupKeyboardHeight(
+ DebugSettings.PREF_KEYBOARD_HEIGHT_SCALE, SettingsValues.DEFAULT_SIZE_SCALE);
+
+ mServiceNeedsRestart = false;
+ mDebugMode = (TwoStatePreference) findPreference(DebugSettings.PREF_DEBUG_MODE);
+ updateDebugMode();
+ }
+
+ private static class DictDumpPreference extends Preference {
+ public final String mDictName;
+
+ public DictDumpPreference(final Context context, final String dictName) {
+ super(context);
+ setKey(PREF_KEY_DUMP_DICT_PREFIX + dictName);
+ setTitle("Dump " + dictName + " dictionary");
+ mDictName = dictName;
+ }
+ }
+
+ @Override
+ public boolean onPreferenceClick(final Preference pref) {
+ final Context context = getActivity();
+ if (pref instanceof DictDumpPreference) {
+ final DictDumpPreference dictDumpPref = (DictDumpPreference)pref;
+ final String dictName = dictDumpPref.mDictName;
+ final Intent intent = new Intent(
+ DictionaryDumpBroadcastReceiver.DICTIONARY_DUMP_INTENT_ACTION);
+ intent.putExtra(DictionaryDumpBroadcastReceiver.DICTIONARY_NAME_KEY, dictName);
+ context.sendBroadcast(intent);
+ return true;
+ }
+ return true;
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ if (mServiceNeedsRestart) {
+ Process.killProcess(Process.myPid());
+ }
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
+ if (key.equals(DebugSettings.PREF_DEBUG_MODE) && mDebugMode != null) {
+ mDebugMode.setChecked(prefs.getBoolean(DebugSettings.PREF_DEBUG_MODE, false));
+ updateDebugMode();
+ mServiceNeedsRestart = true;
+ return;
+ }
+ if (key.equals(DebugSettings.PREF_FORCE_NON_DISTINCT_MULTITOUCH)) {
+ mServiceNeedsRestart = true;
+ return;
+ }
+ }
+
+ private void updateDebugMode() {
+ boolean isDebugMode = mDebugMode.isChecked();
+ final String version = getString(
+ R.string.version_text, ApplicationUtils.getVersionName(getActivity()));
+ if (!isDebugMode) {
+ mDebugMode.setTitle(version);
+ mDebugMode.setSummary(null);
+ } else {
+ mDebugMode.setTitle(getString(R.string.prefs_debug_mode));
+ mDebugMode.setSummary(version);
+ }
+ }
+
+ private void setupKeyPreviewAnimationScale(final String prefKey, final float defaultValue) {
+ final SharedPreferences prefs = getSharedPreferences();
+ final Resources res = getResources();
+ final SeekBarDialogPreference pref = (SeekBarDialogPreference)findPreference(prefKey);
+ if (pref == null) {
+ return;
+ }
+ pref.setInterface(new SeekBarDialogPreference.ValueProxy() {
+ private static final float PERCENTAGE_FLOAT = 100.0f;
+
+ private float getValueFromPercentage(final int percentage) {
+ return percentage / PERCENTAGE_FLOAT;
+ }
+
+ private int getPercentageFromValue(final float floatValue) {
+ return (int)(floatValue * PERCENTAGE_FLOAT);
+ }
+
+ @Override
+ public void writeValue(final int value, final String key) {
+ prefs.edit().putFloat(key, getValueFromPercentage(value)).apply();
+ }
+
+ @Override
+ public void writeDefaultValue(final String key) {
+ prefs.edit().remove(key).apply();
+ }
+
+ @Override
+ public int readValue(final String key) {
+ return getPercentageFromValue(
+ Settings.readKeyPreviewAnimationScale(prefs, key, defaultValue));
+ }
+
+ @Override
+ public int readDefaultValue(final String key) {
+ return getPercentageFromValue(defaultValue);
+ }
+
+ @Override
+ public String getValueText(final int value) {
+ if (value < 0) {
+ return res.getString(R.string.settings_system_default);
+ }
+ return String.format(Locale.ROOT, "%d%%", value);
+ }
+
+ @Override
+ public void feedbackValue(final int value) {}
+ });
+ }
+
+ private void setupKeyPreviewAnimationDuration(final String prefKey, final int defaultValue) {
+ final SharedPreferences prefs = getSharedPreferences();
+ final Resources res = getResources();
+ final SeekBarDialogPreference pref = (SeekBarDialogPreference)findPreference(prefKey);
+ if (pref == null) {
+ return;
+ }
+ pref.setInterface(new SeekBarDialogPreference.ValueProxy() {
+ @Override
+ public void writeValue(final int value, final String key) {
+ prefs.edit().putInt(key, value).apply();
+ }
+
+ @Override
+ public void writeDefaultValue(final String key) {
+ prefs.edit().remove(key).apply();
+ }
+
+ @Override
+ public int readValue(final String key) {
+ return Settings.readKeyPreviewAnimationDuration(prefs, key, defaultValue);
+ }
+
+ @Override
+ public int readDefaultValue(final String key) {
+ return defaultValue;
+ }
+
+ @Override
+ public String getValueText(final int value) {
+ return res.getString(R.string.abbreviation_unit_milliseconds, value);
+ }
+
+ @Override
+ public void feedbackValue(final int value) {}
+ });
+ }
+
+ private void setupKeyboardHeight(final String prefKey, final float defaultValue) {
+ final SharedPreferences prefs = getSharedPreferences();
+ final SeekBarDialogPreference pref = (SeekBarDialogPreference)findPreference(prefKey);
+ if (pref == null) {
+ return;
+ }
+ pref.setInterface(new SeekBarDialogPreference.ValueProxy() {
+ private static final float PERCENTAGE_FLOAT = 100.0f;
+ private float getValueFromPercentage(final int percentage) {
+ return percentage / PERCENTAGE_FLOAT;
+ }
+
+ private int getPercentageFromValue(final float floatValue) {
+ return (int)(floatValue * PERCENTAGE_FLOAT);
+ }
+
+ @Override
+ public void writeValue(final int value, final String key) {
+ prefs.edit().putFloat(key, getValueFromPercentage(value)).apply();
+ }
+
+ @Override
+ public void writeDefaultValue(final String key) {
+ prefs.edit().remove(key).apply();
+ }
+
+ @Override
+ public int readValue(final String key) {
+ return getPercentageFromValue(Settings.readKeyboardHeight(prefs, defaultValue));
+ }
+
+ @Override
+ public int readDefaultValue(final String key) {
+ return getPercentageFromValue(defaultValue);
+ }
+
+ @Override
+ public String getValueText(final int value) {
+ return String.format(Locale.ROOT, "%d%%", value);
+ }
+
+ @Override
+ public void feedbackValue(final int value) {}
+ });
+ }
+}
diff --git a/java/src/org/kelar/inputmethod/latin/settings/GestureSettingsFragment.java b/java/src/org/kelar/inputmethod/latin/settings/GestureSettingsFragment.java
new file mode 100644
index 000000000..f26392185
--- /dev/null
+++ b/java/src/org/kelar/inputmethod/latin/settings/GestureSettingsFragment.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.kelar.inputmethod.latin.settings;
+
+import android.os.Bundle;
+
+import org.kelar.inputmethod.latin.R;
+
+/**
+ * "Gesture typing preferences" settings sub screen.
+ *
+ * This settings sub screen handles the following gesture typing preferences.
+ * - Enable gesture typing
+ * - Dynamic floating preview
+ * - Show gesture trail
+ * - Phrase gesture
+ */
+public final class GestureSettingsFragment extends SubScreenFragment {
+ @Override
+ public void onCreate(final Bundle icicle) {
+ super.onCreate(icicle);
+ addPreferencesFromResource(R.xml.prefs_screen_gesture);
+ }
+}
diff --git a/java/src/org/kelar/inputmethod/latin/settings/LocalSettingsConstants.java b/java/src/org/kelar/inputmethod/latin/settings/LocalSettingsConstants.java
new file mode 100644
index 000000000..74551724f
--- /dev/null
+++ b/java/src/org/kelar/inputmethod/latin/settings/LocalSettingsConstants.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.kelar.inputmethod.latin.settings;
+
+/**
+ * Collection of device specific preference constants.
+ */
+public class LocalSettingsConstants {
+ // Preference file for storing preferences that are tied to a device
+ // and are not backed up.
+ public static final String PREFS_FILE = "local_prefs";
+
+ // Preference key for the current account.
+ // Do not restore.
+ public static final String PREF_ACCOUNT_NAME = "pref_account_name";
+ // Preference key for enabling cloud sync feature.
+ // Do not restore.
+ public static final String PREF_ENABLE_CLOUD_SYNC = "pref_enable_cloud_sync";
+
+ // List of preference keys to skip from being restored by backup agent.
+ // These preferences are tied to a device and hence should not be restored.
+ // e.g. account name.
+ // Ideally they could have been kept in a separate file that wasn't backed up
+ // however the preference UI currently only deals with the default
+ // shared preferences which makes it non-trivial to move these out to
+ // a different shared preferences file.
+ public static final String[] PREFS_TO_SKIP_RESTORING = new String[] {
+ PREF_ACCOUNT_NAME,
+ PREF_ENABLE_CLOUD_SYNC,
+ // The debug settings are not restored on a new device.
+ // If a feature relies on these, it should ensure that the defaults are
+ // correctly set for it to work on a new device.
+ DebugSettings.PREF_DEBUG_MODE,
+ DebugSettings.PREF_FORCE_NON_DISTINCT_MULTITOUCH,
+ DebugSettings.PREF_HAS_CUSTOM_KEY_PREVIEW_ANIMATION_PARAMS,
+ DebugSettings.PREF_KEYBOARD_HEIGHT_SCALE,
+ DebugSettings.PREF_KEY_PREVIEW_DISMISS_DURATION,
+ DebugSettings.PREF_KEY_PREVIEW_DISMISS_END_X_SCALE,
+ DebugSettings.PREF_KEY_PREVIEW_DISMISS_END_Y_SCALE,
+ DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_DURATION,
+ DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_START_X_SCALE,
+ DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_START_Y_SCALE,
+ DebugSettings.PREF_RESIZE_KEYBOARD,
+ DebugSettings.PREF_SHOULD_SHOW_LXX_SUGGESTION_UI,
+ DebugSettings.PREF_SLIDING_KEY_INPUT_PREVIEW
+ };
+}
diff --git a/java/src/org/kelar/inputmethod/latin/settings/PreferencesSettingsFragment.java b/java/src/org/kelar/inputmethod/latin/settings/PreferencesSettingsFragment.java
new file mode 100644
index 000000000..3103a7a7f
--- /dev/null
+++ b/java/src/org/kelar/inputmethod/latin/settings/PreferencesSettingsFragment.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.kelar.inputmethod.latin.settings;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.Preference;
+
+import org.kelar.inputmethod.latin.AudioAndHapticFeedbackManager;
+import org.kelar.inputmethod.latin.R;
+import org.kelar.inputmethod.latin.RichInputMethodManager;
+
+/**
+ * "Preferences" settings sub screen.
+ *
+ * This settings sub screen handles the following input preferences.
+ * - Auto-capitalization
+ * - Double-space period
+ * - Vibrate on keypress
+ * - Sound on keypress
+ * - Popup on keypress
+ * - Voice input key
+ */
+public final class PreferencesSettingsFragment extends SubScreenFragment {
+
+ private static final boolean VOICE_IME_ENABLED =
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
+
+ @Override
+ public void onCreate(final Bundle icicle) {
+ super.onCreate(icicle);
+ addPreferencesFromResource(R.xml.prefs_screen_preferences);
+
+ final Resources res = getResources();
+ final Context context = getActivity();
+
+ // When we are called from the Settings application but we are not already running, some
+ // singleton and utility classes may not have been initialized. We have to call
+ // initialization method of these classes here. See {@link LatinIME#onCreate()}.
+ RichInputMethodManager.init(context);
+
+ final boolean showVoiceKeyOption = res.getBoolean(
+ R.bool.config_enable_show_voice_key_option);
+ if (!showVoiceKeyOption) {
+ removePreference(Settings.PREF_VOICE_INPUT_KEY);
+ }
+ if (!AudioAndHapticFeedbackManager.getInstance().hasVibrator()) {
+ removePreference(Settings.PREF_VIBRATE_ON);
+ }
+ if (!Settings.readFromBuildConfigIfToShowKeyPreviewPopupOption(res)) {
+ removePreference(Settings.PREF_POPUP_ON);
+ }
+
+ refreshEnablingsOfKeypressSoundAndVibrationSettings();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ final Preference voiceInputKeyOption = findPreference(Settings.PREF_VOICE_INPUT_KEY);
+ if (voiceInputKeyOption != null) {
+ RichInputMethodManager.getInstance().refreshSubtypeCaches();
+ voiceInputKeyOption.setEnabled(VOICE_IME_ENABLED);
+ voiceInputKeyOption.setSummary(VOICE_IME_ENABLED
+ ? null : getText(R.string.voice_input_disabled_summary));
+ }
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
+ final Resources res = getResources();
+ if (key.equals(Settings.PREF_POPUP_ON)) {
+ setPreferenceEnabled(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY,
+ Settings.readKeyPreviewPopupEnabled(prefs, res));
+ }
+ refreshEnablingsOfKeypressSoundAndVibrationSettings();
+ }
+
+ private void refreshEnablingsOfKeypressSoundAndVibrationSettings() {
+ final SharedPreferences prefs = getSharedPreferences();
+ final Resources res = getResources();
+ setPreferenceEnabled(Settings.PREF_VIBRATION_DURATION_SETTINGS,
+ Settings.readVibrationEnabled(prefs, res));
+ setPreferenceEnabled(Settings.PREF_KEYPRESS_SOUND_VOLUME,
+ Settings.readKeypressSoundEnabled(prefs, res));
+ }
+}
diff --git a/java/src/org/kelar/inputmethod/latin/settings/RadioButtonPreference.java b/java/src/org/kelar/inputmethod/latin/settings/RadioButtonPreference.java
new file mode 100644
index 000000000..0993cfe29
--- /dev/null
+++ b/java/src/org/kelar/inputmethod/latin/settings/RadioButtonPreference.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.kelar.inputmethod.latin.settings;
+
+import android.content.Context;
+import android.preference.Preference;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.RadioButton;
+
+import org.kelar.inputmethod.latin.R;
+
+/**
+ * Radio Button preference
+ */
+public class RadioButtonPreference extends Preference {
+ interface OnRadioButtonClickedListener {
+ /**
+ * Called when this preference needs to be saved its state.
+ *
+ * @param preference This preference.
+ */
+ public void onRadioButtonClicked(RadioButtonPreference preference);
+ }
+
+ private boolean mIsSelected;
+ private RadioButton mRadioButton;
+ private OnRadioButtonClickedListener mListener;
+ private final View.OnClickListener mClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(final View v) {
+ callListenerOnRadioButtonClicked();
+ }
+ };
+
+ public RadioButtonPreference(final Context context) {
+ this(context, null);
+ }
+
+ public RadioButtonPreference(final Context context, final AttributeSet attrs) {
+ this(context, attrs, android.R.attr.preferenceStyle);
+ }
+
+ public RadioButtonPreference(final Context context, final AttributeSet attrs,
+ final int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ setWidgetLayoutResource(R.layout.radio_button_preference_widget);
+ }
+
+ public void setOnRadioButtonClickedListener(final OnRadioButtonClickedListener listener) {
+ mListener = listener;
+ }
+
+ void callListenerOnRadioButtonClicked() {
+ if (mListener != null) {
+ mListener.onRadioButtonClicked(this);
+ }
+ }
+
+ @Override
+ protected void onBindView(final View view) {
+ super.onBindView(view);
+ mRadioButton = (RadioButton)view.findViewById(R.id.radio_button);
+ mRadioButton.setChecked(mIsSelected);
+ mRadioButton.setOnClickListener(mClickListener);
+ view.setOnClickListener(mClickListener);
+ }
+
+ public boolean isSelected() {
+ return mIsSelected;
+ }
+
+ public void setSelected(final boolean selected) {
+ if (selected == mIsSelected) {
+ return;
+ }
+ mIsSelected = selected;
+ if (mRadioButton != null) {
+ mRadioButton.setChecked(selected);
+ }
+ notifyChanged();
+ }
+}
diff --git a/java/src/org/kelar/inputmethod/latin/settings/SeekBarDialogPreference.java b/java/src/org/kelar/inputmethod/latin/settings/SeekBarDialogPreference.java
new file mode 100644
index 000000000..a5437cf13
--- /dev/null
+++ b/java/src/org/kelar/inputmethod/latin/settings/SeekBarDialogPreference.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.kelar.inputmethod.latin.settings;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.TypedArray;
+import android.preference.DialogPreference;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import org.kelar.inputmethod.latin.R;
+
+public final class SeekBarDialogPreference extends DialogPreference
+ implements SeekBar.OnSeekBarChangeListener {
+ public interface ValueProxy {
+ public int readValue(final String key);
+ public int readDefaultValue(final String key);
+ public void writeValue(final int value, final String key);
+ public void writeDefaultValue(final String key);
+ public String getValueText(final int value);
+ public void feedbackValue(final int value);
+ }
+
+ private final int mMaxValue;
+ private final int mMinValue;
+ private final int mStepValue;
+
+ private TextView mValueView;
+ private SeekBar mSeekBar;
+
+ private ValueProxy mValueProxy;
+
+ public SeekBarDialogPreference(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.SeekBarDialogPreference, 0, 0);
+ mMaxValue = a.getInt(R.styleable.SeekBarDialogPreference_maxValue, 0);
+ mMinValue = a.getInt(R.styleable.SeekBarDialogPreference_minValue, 0);
+ mStepValue = a.getInt(R.styleable.SeekBarDialogPreference_stepValue, 0);
+ a.recycle();
+ setDialogLayoutResource(R.layout.seek_bar_dialog);
+ }
+
+ public void setInterface(final ValueProxy proxy) {
+ mValueProxy = proxy;
+ final int value = mValueProxy.readValue(getKey());
+ setSummary(mValueProxy.getValueText(value));
+ }
+
+ @Override
+ protected View onCreateDialogView() {
+ final View view = super.onCreateDialogView();
+ mSeekBar = (SeekBar)view.findViewById(R.id.seek_bar_dialog_bar);
+ mSeekBar.setMax(mMaxValue - mMinValue);
+ mSeekBar.setOnSeekBarChangeListener(this);
+ mValueView = (TextView)view.findViewById(R.id.seek_bar_dialog_value);
+ return view;
+ }
+
+ private int getProgressFromValue(final int value) {
+ return value - mMinValue;
+ }
+
+ private int getValueFromProgress(final int progress) {
+ return progress + mMinValue;
+ }
+
+ private int clipValue(final int value) {
+ final int clippedValue = Math.min(mMaxValue, Math.max(mMinValue, value));
+ if (mStepValue <= 1) {
+ return clippedValue;
+ }
+ return clippedValue - (clippedValue % mStepValue);
+ }
+
+ private int getClippedValueFromProgress(final int progress) {
+ return clipValue(getValueFromProgress(progress));
+ }
+
+ @Override
+ protected void onBindDialogView(final View view) {
+ final int value = mValueProxy.readValue(getKey());
+ mValueView.setText(mValueProxy.getValueText(value));
+ mSeekBar.setProgress(getProgressFromValue(clipValue(value)));
+ }
+
+ @Override
+ protected void onPrepareDialogBuilder(final AlertDialog.Builder builder) {
+ builder.setPositiveButton(android.R.string.ok, this)
+ .setNegativeButton(android.R.string.cancel, this)
+ .setNeutralButton(R.string.button_default, this);
+ }
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ super.onClick(dialog, which);
+ final String key = getKey();
+ if (which == DialogInterface.BUTTON_NEUTRAL) {
+ final int value = mValueProxy.readDefaultValue(key);
+ setSummary(mValueProxy.getValueText(value));
+ mValueProxy.writeDefaultValue(key);
+ return;
+ }
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ final int value = getClippedValueFromProgress(mSeekBar.getProgress());
+ setSummary(mValueProxy.getValueText(value));
+ mValueProxy.writeValue(value, key);
+ return;
+ }
+ }
+
+ @Override
+ public void onProgressChanged(final SeekBar seekBar, final int progress,
+ final boolean fromUser) {
+ final int value = getClippedValueFromProgress(progress);
+ mValueView.setText(mValueProxy.getValueText(value));
+ if (!fromUser) {
+ mSeekBar.setProgress(getProgressFromValue(value));
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(final SeekBar seekBar) {}
+
+ @Override
+ public void onStopTrackingTouch(final SeekBar seekBar) {
+ mValueProxy.feedbackValue(getClippedValueFromProgress(seekBar.getProgress()));
+ }
+}
diff --git a/java/src/org/kelar/inputmethod/latin/settings/Settings.java b/java/src/org/kelar/inputmethod/latin/settings/Settings.java
new file mode 100644
index 000000000..c16caddb2
--- /dev/null
+++ b/java/src/org/kelar/inputmethod/latin/settings/Settings.java
@@ -0,0 +1,458 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.kelar.inputmethod.latin.settings;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.Build;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+import org.kelar.inputmethod.compat.BuildCompatUtils;
+import org.kelar.inputmethod.latin.AudioAndHapticFeedbackManager;
+import org.kelar.inputmethod.latin.InputAttributes;
+import org.kelar.inputmethod.latin.R;
+import org.kelar.inputmethod.latin.common.StringUtils;
+import org.kelar.inputmethod.latin.utils.AdditionalSubtypeUtils;
+import org.kelar.inputmethod.latin.utils.ResourceUtils;
+import org.kelar.inputmethod.latin.utils.RunInLocale;
+import org.kelar.inputmethod.latin.utils.StatsUtils;
+
+import java.util.Collections;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.annotation.Nonnull;
+
+public final class Settings implements SharedPreferences.OnSharedPreferenceChangeListener {
+ private static final String TAG = Settings.class.getSimpleName();
+ // Settings screens
+ public static final String SCREEN_ACCOUNTS = "screen_accounts";
+ public static final String SCREEN_THEME = "screen_theme";
+ public static final String SCREEN_DEBUG = "screen_debug";
+ // In the same order as xml/prefs.xml
+ public static final String PREF_AUTO_CAP = "auto_cap";
+ public static final String PREF_VIBRATE_ON = "vibrate_on";
+ public static final String PREF_SOUND_ON = "sound_on";
+ public static final String PREF_POPUP_ON = "popup_on";
+ // PREF_VOICE_MODE_OBSOLETE is obsolete. Use PREF_VOICE_INPUT_KEY instead.
+ public static final String PREF_VOICE_MODE_OBSOLETE = "voice_mode";
+ public static final String PREF_VOICE_INPUT_KEY = "pref_voice_input_key";
+ public static final String PREF_EDIT_PERSONAL_DICTIONARY = "edit_personal_dictionary";
+ public static final String PREF_CONFIGURE_DICTIONARIES_KEY = "configure_dictionaries_key";
+ // PREF_AUTO_CORRECTION_THRESHOLD_OBSOLETE is obsolete. Use PREF_AUTO_CORRECTION instead.
+ public static final String PREF_AUTO_CORRECTION_THRESHOLD_OBSOLETE =
+ "auto_correction_threshold";
+ public static final String PREF_AUTO_CORRECTION = "pref_key_auto_correction";
+ // PREF_SHOW_SUGGESTIONS_SETTING_OBSOLETE is obsolete. Use PREF_SHOW_SUGGESTIONS instead.
+ public static final String PREF_SHOW_SUGGESTIONS_SETTING_OBSOLETE = "show_suggestions_setting";
+ public static final String PREF_SHOW_SUGGESTIONS = "show_suggestions";
+ public static final String PREF_KEY_USE_CONTACTS_DICT = "pref_key_use_contacts_dict";
+ public static final String PREF_KEY_USE_PERSONALIZED_DICTS = "pref_key_use_personalized_dicts";
+ public static final String PREF_KEY_USE_DOUBLE_SPACE_PERIOD =
+ "pref_key_use_double_space_period";
+ public static final String PREF_BLOCK_POTENTIALLY_OFFENSIVE =
+ "pref_key_block_potentially_offensive";
+ public static final boolean ENABLE_SHOW_LANGUAGE_SWITCH_KEY_SETTINGS =
+ BuildCompatUtils.EFFECTIVE_SDK_INT <= Build.VERSION_CODES.KITKAT;
+ public static final boolean SHOULD_SHOW_LXX_SUGGESTION_UI =
+ BuildCompatUtils.EFFECTIVE_SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
+ public static final String PREF_SHOW_LANGUAGE_SWITCH_KEY =
+ "pref_show_language_switch_key";
+ public static final String PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST =
+ "pref_include_other_imes_in_language_switch_list";
+ public static final String PREF_CUSTOM_INPUT_STYLES = "custom_input_styles";
+ public static final String PREF_ENABLE_SPLIT_KEYBOARD = "pref_split_keyboard";
+ // TODO: consolidate key preview dismiss delay with the key preview animation parameters.
+ public static final String PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY =
+ "pref_key_preview_popup_dismiss_delay";
+ public static final String PREF_BIGRAM_PREDICTIONS = "next_word_prediction";
+ public static final String PREF_GESTURE_INPUT = "gesture_input";
+ public static final String PREF_VIBRATION_DURATION_SETTINGS =
+ "pref_vibration_duration_settings";
+ public static final String PREF_KEYPRESS_SOUND_VOLUME = "pref_keypress_sound_volume";
+ public static final String PREF_KEY_LONGPRESS_TIMEOUT = "pref_key_longpress_timeout";
+ public static final String PREF_ENABLE_EMOJI_ALT_PHYSICAL_KEY =
+ "pref_enable_emoji_alt_physical_key";
+ public static final String PREF_GESTURE_PREVIEW_TRAIL = "pref_gesture_preview_trail";
+ public static final String PREF_GESTURE_FLOATING_PREVIEW_TEXT =
+ "pref_gesture_floating_preview_text";
+ public static final String PREF_SHOW_SETUP_WIZARD_ICON = "pref_show_setup_wizard_icon";
+
+ public static final String PREF_KEY_IS_INTERNAL = "pref_key_is_internal";
+
+ public static final String PREF_ENABLE_METRICS_LOGGING = "pref_enable_metrics_logging";
+ // This preference key is deprecated. Use {@link #PREF_SHOW_LANGUAGE_SWITCH_KEY} instead.
+ // This is being used only for the backward compatibility.
+ private static final String PREF_SUPPRESS_LANGUAGE_SWITCH_KEY =
+ "pref_suppress_language_switch_key";
+
+ private static final String PREF_LAST_USED_PERSONALIZATION_TOKEN =
+ "pref_last_used_personalization_token";
+ private static final String PREF_LAST_PERSONALIZATION_DICT_WIPED_TIME =
+ "pref_last_used_personalization_dict_wiped_time";
+ private static final String PREF_CORPUS_HANDLES_FOR_PERSONALIZATION =
+ "pref_corpus_handles_for_personalization";
+
+ // Emoji
+ public static final String PREF_EMOJI_RECENT_KEYS = "emoji_recent_keys";
+ public static final String PREF_EMOJI_CATEGORY_LAST_TYPED_ID = "emoji_category_last_typed_id";
+ public static final String PREF_LAST_SHOWN_EMOJI_CATEGORY_ID = "last_shown_emoji_category_id";
+
+ private static final float UNDEFINED_PREFERENCE_VALUE_FLOAT = -1.0f;
+ private static final int UNDEFINED_PREFERENCE_VALUE_INT = -1;
+
+ private Context mContext;
+ private Resources mRes;
+ private SharedPreferences mPrefs;
+ private SettingsValues mSettingsValues;
+ private final ReentrantLock mSettingsValuesLock = new ReentrantLock();
+
+ private static final Settings sInstance = new Settings();
+
+ public static Settings getInstance() {
+ return sInstance;
+ }
+
+ public static void init(final Context context) {
+ sInstance.onCreate(context);
+ }
+
+ private Settings() {
+ // Intentional empty constructor for singleton.
+ }
+
+ private void onCreate(final Context context) {
+ mContext = context;
+ mRes = context.getResources();
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(context);
+ mPrefs.registerOnSharedPreferenceChangeListener(this);
+ upgradeAutocorrectionSettings(mPrefs, mRes);
+ }
+
+ public void onDestroy() {
+ mPrefs.unregisterOnSharedPreferenceChangeListener(this);
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
+ mSettingsValuesLock.lock();
+ try {
+ if (mSettingsValues == null) {
+ // TODO: Introduce a static function to register this class and ensure that
+ // loadSettings must be called before "onSharedPreferenceChanged" is called.
+ Log.w(TAG, "onSharedPreferenceChanged called before loadSettings.");
+ return;
+ }
+ loadSettings(mContext, mSettingsValues.mLocale, mSettingsValues.mInputAttributes);
+ StatsUtils.onLoadSettings(mSettingsValues);
+ } finally {
+ mSettingsValuesLock.unlock();
+ }
+ }
+
+ public void loadSettings(final Context context, final Locale locale,
+ @Nonnull final InputAttributes inputAttributes) {
+ mSettingsValuesLock.lock();
+ mContext = context;
+ try {
+ final SharedPreferences prefs = mPrefs;
+ final RunInLocale<SettingsValues> job = new RunInLocale<SettingsValues>() {
+ @Override
+ protected SettingsValues job(final Resources res) {
+ return new SettingsValues(context, prefs, res, inputAttributes);
+ }
+ };
+ mSettingsValues = job.runInLocale(mRes, locale);
+ } finally {
+ mSettingsValuesLock.unlock();
+ }
+ }
+
+ // TODO: Remove this method and add proxy method to SettingsValues.
+ public SettingsValues getCurrent() {
+ return mSettingsValues;
+ }
+
+ public boolean isInternal() {
+ return mSettingsValues.mIsInternal;
+ }
+
+ public static int readScreenMetrics(final Resources res) {
+ return res.getInteger(R.integer.config_screen_metrics);
+ }
+
+ // Accessed from the settings interface, hence public
+ public static boolean readKeypressSoundEnabled(final SharedPreferences prefs,
+ final Resources res) {
+ return prefs.getBoolean(PREF_SOUND_ON,
+ res.getBoolean(R.bool.config_default_sound_enabled));
+ }
+
+ public static boolean readVibrationEnabled(final SharedPreferences prefs,
+ final Resources res) {
+ final boolean hasVibrator = AudioAndHapticFeedbackManager.getInstance().hasVibrator();
+ return hasVibrator && prefs.getBoolean(PREF_VIBRATE_ON,
+ res.getBoolean(R.bool.config_default_vibration_enabled));
+ }
+
+ public static boolean readAutoCorrectEnabled(final SharedPreferences prefs,
+ final Resources res) {
+ return prefs.getBoolean(PREF_AUTO_CORRECTION, true);
+ }
+
+ public static float readPlausibilityThreshold(final Resources res) {
+ return Float.parseFloat(res.getString(R.string.plausibility_threshold));
+ }
+
+ public static boolean readBlockPotentiallyOffensive(final SharedPreferences prefs,
+ final Resources res) {
+ return prefs.getBoolean(PREF_BLOCK_POTENTIALLY_OFFENSIVE,
+ res.getBoolean(R.bool.config_block_potentially_offensive));
+ }
+
+ public static boolean readFromBuildConfigIfGestureInputEnabled(final Resources res) {
+ return res.getBoolean(R.bool.config_gesture_input_enabled_by_build_config);
+ }
+
+ public static boolean readGestureInputEnabled(final SharedPreferences prefs,
+ final Resources res) {
+ return readFromBuildConfigIfGestureInputEnabled(res)
+ && prefs.getBoolean(PREF_GESTURE_INPUT, true);
+ }
+
+ public static boolean readFromBuildConfigIfToShowKeyPreviewPopupOption(final Resources res) {
+ return res.getBoolean(R.bool.config_enable_show_key_preview_popup_option);
+ }
+
+ public static boolean readKeyPreviewPopupEnabled(final SharedPreferences prefs,
+ final Resources res) {
+ final boolean defaultKeyPreviewPopup = res.getBoolean(
+ R.bool.config_default_key_preview_popup);
+ if (!readFromBuildConfigIfToShowKeyPreviewPopupOption(res)) {
+ return defaultKeyPreviewPopup;
+ }
+ return prefs.getBoolean(PREF_POPUP_ON, defaultKeyPreviewPopup);
+ }
+
+ public static int readKeyPreviewPopupDismissDelay(final SharedPreferences prefs,
+ final Resources res) {
+ return Integer.parseInt(prefs.getString(PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY,
+ Integer.toString(res.getInteger(
+ R.integer.config_key_preview_linger_timeout))));
+ }
+
+ public static boolean readShowsLanguageSwitchKey(final SharedPreferences prefs) {
+ if (prefs.contains(PREF_SUPPRESS_LANGUAGE_SWITCH_KEY)) {
+ final boolean suppressLanguageSwitchKey = prefs.getBoolean(
+ PREF_SUPPRESS_LANGUAGE_SWITCH_KEY, false);
+ final SharedPreferences.Editor editor = prefs.edit();
+ editor.remove(PREF_SUPPRESS_LANGUAGE_SWITCH_KEY);
+ editor.putBoolean(PREF_SHOW_LANGUAGE_SWITCH_KEY, !suppressLanguageSwitchKey);
+ editor.apply();
+ }
+ return prefs.getBoolean(PREF_SHOW_LANGUAGE_SWITCH_KEY, true);
+ }
+
+ public static String readPrefAdditionalSubtypes(final SharedPreferences prefs,
+ final Resources res) {
+ final String predefinedPrefSubtypes = AdditionalSubtypeUtils.createPrefSubtypes(
+ res.getStringArray(R.array.predefined_subtypes));
+ return prefs.getString(PREF_CUSTOM_INPUT_STYLES, predefinedPrefSubtypes);
+ }
+
+ public static void writePrefAdditionalSubtypes(final SharedPreferences prefs,
+ final String prefSubtypes) {
+ prefs.edit().putString(PREF_CUSTOM_INPUT_STYLES, prefSubtypes).apply();
+ }
+
+ public static float readKeypressSoundVolume(final SharedPreferences prefs,
+ final Resources res) {
+ final float volume = prefs.getFloat(
+ PREF_KEYPRESS_SOUND_VOLUME, UNDEFINED_PREFERENCE_VALUE_FLOAT);
+ return (volume != UNDEFINED_PREFERENCE_VALUE_FLOAT) ? volume
+ : readDefaultKeypressSoundVolume(res);
+ }
+
+ // Default keypress sound volume for unknown devices.
+ // The negative value means system default.
+ private static final String DEFAULT_KEYPRESS_SOUND_VOLUME = Float.toString(-1.0f);
+
+ public static float readDefaultKeypressSoundVolume(final Resources res) {
+ return Float.parseFloat(ResourceUtils.getDeviceOverrideValue(res,
+ R.array.keypress_volumes, DEFAULT_KEYPRESS_SOUND_VOLUME));
+ }
+
+ public static int readKeyLongpressTimeout(final SharedPreferences prefs,
+ final Resources res) {
+ final int milliseconds = prefs.getInt(
+ PREF_KEY_LONGPRESS_TIMEOUT, UNDEFINED_PREFERENCE_VALUE_INT);
+ return (milliseconds != UNDEFINED_PREFERENCE_VALUE_INT) ? milliseconds
+ : readDefaultKeyLongpressTimeout(res);
+ }
+
+ public static int readDefaultKeyLongpressTimeout(final Resources res) {
+ return res.getInteger(R.integer.config_default_longpress_key_timeout);
+ }
+
+ public static int readKeypressVibrationDuration(final SharedPreferences prefs,
+ final Resources res) {
+ final int milliseconds = prefs.getInt(
+ PREF_VIBRATION_DURATION_SETTINGS, UNDEFINED_PREFERENCE_VALUE_INT);
+ return (milliseconds != UNDEFINED_PREFERENCE_VALUE_INT) ? milliseconds
+ : readDefaultKeypressVibrationDuration(res);
+ }
+
+ // Default keypress vibration duration for unknown devices.
+ // The negative value means system default.
+ private static final String DEFAULT_KEYPRESS_VIBRATION_DURATION = Integer.toString(-1);
+
+ public static int readDefaultKeypressVibrationDuration(final Resources res) {
+ return Integer.parseInt(ResourceUtils.getDeviceOverrideValue(res,
+ R.array.keypress_vibration_durations, DEFAULT_KEYPRESS_VIBRATION_DURATION));
+ }
+
+ public static float readKeyPreviewAnimationScale(final SharedPreferences prefs,
+ final String prefKey, final float defaultValue) {
+ final float fraction = prefs.getFloat(prefKey, UNDEFINED_PREFERENCE_VALUE_FLOAT);
+ return (fraction != UNDEFINED_PREFERENCE_VALUE_FLOAT) ? fraction : defaultValue;
+ }
+
+ public static int readKeyPreviewAnimationDuration(final SharedPreferences prefs,
+ final String prefKey, final int defaultValue) {
+ final int milliseconds = prefs.getInt(prefKey, UNDEFINED_PREFERENCE_VALUE_INT);
+ return (milliseconds != UNDEFINED_PREFERENCE_VALUE_INT) ? milliseconds : defaultValue;
+ }
+
+ public static float readKeyboardHeight(final SharedPreferences prefs,
+ final float defaultValue) {
+ final float percentage = prefs.getFloat(
+ DebugSettings.PREF_KEYBOARD_HEIGHT_SCALE, UNDEFINED_PREFERENCE_VALUE_FLOAT);
+ return (percentage != UNDEFINED_PREFERENCE_VALUE_FLOAT) ? percentage : defaultValue;
+ }
+
+ public static boolean readUseFullscreenMode(final Resources res) {
+ return res.getBoolean(R.bool.config_use_fullscreen_mode);
+ }
+
+ public static boolean readShowSetupWizardIcon(final SharedPreferences prefs,
+ final Context context) {
+ if (!prefs.contains(PREF_SHOW_SETUP_WIZARD_ICON)) {
+ final ApplicationInfo appInfo = context.getApplicationInfo();
+ final boolean isApplicationInSystemImage =
+ (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
+ // Default value
+ return !isApplicationInSystemImage;
+ }
+ return prefs.getBoolean(PREF_SHOW_SETUP_WIZARD_ICON, false);
+ }
+
+ public static boolean readHasHardwareKeyboard(final Configuration conf) {
+ // The standard way of finding out whether we have a hardware keyboard. This code is taken
+ // from InputMethodService#onEvaluateInputShown, which canonically determines this.
+ // In a nutshell, we have a keyboard if the configuration says the type of hardware keyboard
+ // is NOKEYS and if it's not hidden (e.g. folded inside the device).
+ return conf.keyboard != Configuration.KEYBOARD_NOKEYS
+ && conf.hardKeyboardHidden != Configuration.HARDKEYBOARDHIDDEN_YES;
+ }
+
+ public static boolean isInternal(final SharedPreferences prefs) {
+ return prefs.getBoolean(PREF_KEY_IS_INTERNAL, false);
+ }
+
+ public void writeLastUsedPersonalizationToken(byte[] token) {
+ if (token == null) {
+ mPrefs.edit().remove(PREF_LAST_USED_PERSONALIZATION_TOKEN).apply();
+ } else {
+ final String tokenStr = StringUtils.byteArrayToHexString(token);
+ mPrefs.edit().putString(PREF_LAST_USED_PERSONALIZATION_TOKEN, tokenStr).apply();
+ }
+ }
+
+ public byte[] readLastUsedPersonalizationToken() {
+ final String tokenStr = mPrefs.getString(PREF_LAST_USED_PERSONALIZATION_TOKEN, null);
+ return StringUtils.hexStringToByteArray(tokenStr);
+ }
+
+ public void writeLastPersonalizationDictWipedTime(final long timestamp) {
+ mPrefs.edit().putLong(PREF_LAST_PERSONALIZATION_DICT_WIPED_TIME, timestamp).apply();
+ }
+
+ public long readLastPersonalizationDictGeneratedTime() {
+ return mPrefs.getLong(PREF_LAST_PERSONALIZATION_DICT_WIPED_TIME, 0);
+ }
+
+ public void writeCorpusHandlesForPersonalization(final Set<String> corpusHandles) {
+ mPrefs.edit().putStringSet(PREF_CORPUS_HANDLES_FOR_PERSONALIZATION, corpusHandles).apply();
+ }
+
+ public Set<String> readCorpusHandlesForPersonalization() {
+ final Set<String> emptySet = Collections.emptySet();
+ return mPrefs.getStringSet(PREF_CORPUS_HANDLES_FOR_PERSONALIZATION, emptySet);
+ }
+
+ public static void writeEmojiRecentKeys(final SharedPreferences prefs, String str) {
+ prefs.edit().putString(PREF_EMOJI_RECENT_KEYS, str).apply();
+ }
+
+ public static String readEmojiRecentKeys(final SharedPreferences prefs) {
+ return prefs.getString(PREF_EMOJI_RECENT_KEYS, "");
+ }
+
+ public static void writeLastTypedEmojiCategoryPageId(
+ final SharedPreferences prefs, final int categoryId, final int categoryPageId) {
+ final String key = PREF_EMOJI_CATEGORY_LAST_TYPED_ID + categoryId;
+ prefs.edit().putInt(key, categoryPageId).apply();
+ }
+
+ public static int readLastTypedEmojiCategoryPageId(
+ final SharedPreferences prefs, final int categoryId) {
+ final String key = PREF_EMOJI_CATEGORY_LAST_TYPED_ID + categoryId;
+ return prefs.getInt(key, 0);
+ }
+
+ public static void writeLastShownEmojiCategoryId(
+ final SharedPreferences prefs, final int categoryId) {
+ prefs.edit().putInt(PREF_LAST_SHOWN_EMOJI_CATEGORY_ID, categoryId).apply();
+ }
+
+ public static int readLastShownEmojiCategoryId(
+ final SharedPreferences prefs, final int defValue) {
+ return prefs.getInt(PREF_LAST_SHOWN_EMOJI_CATEGORY_ID, defValue);
+ }
+
+ private void upgradeAutocorrectionSettings(final SharedPreferences prefs, final Resources res) {
+ final String thresholdSetting =
+ prefs.getString(PREF_AUTO_CORRECTION_THRESHOLD_OBSOLETE, null);
+ if (thresholdSetting != null) {
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.remove(PREF_AUTO_CORRECTION_THRESHOLD_OBSOLETE);
+ final String autoCorrectionOff =
+ res.getString(R.string.auto_correction_threshold_mode_index_off);
+ if (thresholdSetting.equals(autoCorrectionOff)) {
+ editor.putBoolean(PREF_AUTO_CORRECTION, false);
+ } else {
+ editor.putBoolean(PREF_AUTO_CORRECTION, true);
+ }
+ editor.commit();
+ }
+ }
+}
diff --git a/java/src/org/kelar/inputmethod/latin/settings/SettingsActivity.java b/java/src/org/kelar/inputmethod/latin/settings/SettingsActivity.java
new file mode 100644
index 000000000..a11cf47e6
--- /dev/null
+++ b/java/src/org/kelar/inputmethod/latin/settings/SettingsActivity.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.kelar.inputmethod.latin.settings;
+
+import org.kelar.inputmethod.latin.permissions.PermissionsManager;
+import org.kelar.inputmethod.latin.utils.FragmentUtils;
+import org.kelar.inputmethod.latin.utils.StatsUtils;
+
+import android.app.ActionBar;
+import android.content.Intent;
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+import androidx.core.app.ActivityCompat;
+import android.view.MenuItem;
+
+public final class SettingsActivity extends PreferenceActivity
+ implements ActivityCompat.OnRequestPermissionsResultCallback {
+ private static final String DEFAULT_FRAGMENT = SettingsFragment.class.getName();
+
+ public static final String EXTRA_SHOW_HOME_AS_UP = "show_home_as_up";
+ public static final String EXTRA_ENTRY_KEY = "entry";
+ public static final String EXTRA_ENTRY_VALUE_LONG_PRESS_COMMA = "long_press_comma";
+ public static final String EXTRA_ENTRY_VALUE_APP_ICON = "app_icon";
+ public static final String EXTRA_ENTRY_VALUE_NOTICE_DIALOG = "important_notice";
+ public static final String EXTRA_ENTRY_VALUE_SYSTEM_SETTINGS = "system_settings";
+
+ private boolean mShowHomeAsUp;
+
+ @Override
+ protected void onCreate(final Bundle savedState) {
+ super.onCreate(savedState);
+ final ActionBar actionBar = getActionBar();
+ final Intent intent = getIntent();
+ if (actionBar != null) {
+ mShowHomeAsUp = intent.getBooleanExtra(EXTRA_SHOW_HOME_AS_UP, true);
+ actionBar.setDisplayHomeAsUpEnabled(mShowHomeAsUp);
+ actionBar.setHomeButtonEnabled(mShowHomeAsUp);
+ }
+ StatsUtils.onSettingsActivity(
+ intent.hasExtra(EXTRA_ENTRY_KEY) ? intent.getStringExtra(EXTRA_ENTRY_KEY)
+ : EXTRA_ENTRY_VALUE_SYSTEM_SETTINGS);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ if (mShowHomeAsUp && item.getItemId() == android.R.id.home) {
+ finish();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public Intent getIntent() {
+ final Intent intent = super.getIntent();
+ final String fragment = intent.getStringExtra(EXTRA_SHOW_FRAGMENT);
+ if (fragment == null) {
+ intent.putExtra(EXTRA_SHOW_FRAGMENT, DEFAULT_FRAGMENT);
+ }
+ intent.putExtra(EXTRA_NO_HEADERS, true);
+ return intent;
+ }
+
+ @Override
+ public boolean isValidFragment(final String fragmentName) {
+ return FragmentUtils.isValidFragment(fragmentName);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ PermissionsManager.get(this).onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+}
diff --git a/java/src/org/kelar/inputmethod/latin/settings/SettingsFragment.java b/java/src/org/kelar/inputmethod/latin/settings/SettingsFragment.java
new file mode 100644
index 000000000..b61d418f6
--- /dev/null
+++ b/java/src/org/kelar/inputmethod/latin/settings/SettingsFragment.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.kelar.inputmethod.latin.settings;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceScreen;
+import android.provider.Settings.Secure;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+
+import org.kelar.inputmethod.latin.R;
+import org.kelar.inputmethod.latin.define.ProductionFlags;
+import org.kelar.inputmethod.latin.utils.ApplicationUtils;
+import org.kelar.inputmethod.latin.utils.FeedbackUtils;
+import org.kelar.inputmethodcommon.InputMethodSettingsFragment;
+
+public final class SettingsFragment extends InputMethodSettingsFragment {
+ // We don't care about menu grouping.
+ private static final int NO_MENU_GROUP = Menu.NONE;
+ // The first menu item id and order.
+ private static final int MENU_ABOUT = Menu.FIRST;
+ // The second menu item id and order.
+ private static final int MENU_HELP_AND_FEEDBACK = Menu.FIRST + 1;
+
+ @Override
+ public void onCreate(final Bundle icicle) {
+ super.onCreate(icicle);
+ setHasOptionsMenu(true);
+ setInputMethodSettingsCategoryTitle(R.string.language_selection_title);
+ setSubtypeEnablerTitle(R.string.select_language);
+ addPreferencesFromResource(R.xml.prefs);
+ final PreferenceScreen preferenceScreen = getPreferenceScreen();
+ preferenceScreen.setTitle(
+ ApplicationUtils.getActivityTitleResId(getActivity(), SettingsActivity.class));
+ if (!ProductionFlags.ENABLE_ACCOUNT_SIGN_IN) {
+ final Preference accountsPreference = findPreference(Settings.SCREEN_ACCOUNTS);
+ preferenceScreen.removePreference(accountsPreference);
+ }
+ }
+
+ @Override
+ public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
+ if (FeedbackUtils.isHelpAndFeedbackFormSupported()) {
+ menu.add(NO_MENU_GROUP, MENU_HELP_AND_FEEDBACK /* itemId */,
+ MENU_HELP_AND_FEEDBACK /* order */, R.string.help_and_feedback);
+ }
+ final int aboutResId = FeedbackUtils.getAboutKeyboardTitleResId();
+ if (aboutResId != 0) {
+ menu.add(NO_MENU_GROUP, MENU_ABOUT /* itemId */, MENU_ABOUT /* order */, aboutResId);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ final Activity activity = getActivity();
+ if (!isUserSetupComplete(activity)) {
+ // If setup is not complete, it's not safe to launch Help or other activities
+ // because they might go to the Play Store. See b/19866981.
+ return true;
+ }
+ final int itemId = item.getItemId();
+ if (itemId == MENU_HELP_AND_FEEDBACK) {
+ FeedbackUtils.showHelpAndFeedbackForm(activity);
+ return true;
+ }
+ if (itemId == MENU_ABOUT) {
+ final Intent aboutIntent = FeedbackUtils.getAboutKeyboardIntent(activity);
+ if (aboutIntent != null) {
+ startActivity(aboutIntent);
+ return true;
+ }
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private static boolean isUserSetupComplete(final Activity activity) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ return true;
+ }
+ return Secure.getInt(activity.getContentResolver(), "user_setup_complete", 0) != 0;
+ }
+}
diff --git a/java/src/org/kelar/inputmethod/latin/settings/SettingsValues.java b/java/src/org/kelar/inputmethod/latin/settings/SettingsValues.java
new file mode 100644
index 000000000..f54a70361
--- /dev/null
+++ b/java/src/org/kelar/inputmethod/latin/settings/SettingsValues.java
@@ -0,0 +1,453 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.kelar.inputmethod.latin.settings;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.Build;
+import android.util.Log;
+import android.view.inputmethod.EditorInfo;
+
+import org.kelar.inputmethod.compat.AppWorkaroundsUtils;
+import org.kelar.inputmethod.latin.InputAttributes;
+import org.kelar.inputmethod.latin.R;
+import org.kelar.inputmethod.latin.RichInputMethodManager;
+import org.kelar.inputmethod.latin.utils.AsyncResultHolder;
+import org.kelar.inputmethod.latin.utils.ResourceUtils;
+import org.kelar.inputmethod.latin.utils.TargetPackageInfoGetterTask;
+import org.kelar.inputmethod.latin.utils.RunInLocale;
+
+import java.util.Arrays;
+import java.util.Locale;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * When you call the constructor of this class, you may want to change the current system locale by
+ * using {@link RunInLocale}.
+ */
+// Non-final for testing via mock library.
+public class SettingsValues {
+ private static final String TAG = SettingsValues.class.getSimpleName();
+ // "floatMaxValue" and "floatNegativeInfinity" are special marker strings for
+ // Float.NEGATIVE_INFINITE and Float.MAX_VALUE. Currently used for auto-correction settings.
+ private static final String FLOAT_MAX_VALUE_MARKER_STRING = "floatMaxValue";
+ private static final String FLOAT_NEGATIVE_INFINITY_MARKER_STRING = "floatNegativeInfinity";
+ private static final int TIMEOUT_TO_GET_TARGET_PACKAGE = 5; // seconds
+ public static final float DEFAULT_SIZE_SCALE = 1.0f; // 100%
+
+ // From resources:
+ public final SpacingAndPunctuations mSpacingAndPunctuations;
+ public final int mDelayInMillisecondsToUpdateOldSuggestions;
+ public final long mDoubleSpacePeriodTimeout;
+ // From configuration:
+ public final Locale mLocale;
+ public final boolean mHasHardwareKeyboard;
+ public final int mDisplayOrientation;
+ // From preferences, in the same order as xml/prefs.xml:
+ public final boolean mAutoCap;
+ public final boolean mVibrateOn;
+ public final boolean mSoundOn;
+ public final boolean mKeyPreviewPopupOn;
+ public final boolean mShowsVoiceInputKey;
+ public final boolean mIncludesOtherImesInLanguageSwitchList;
+ public final boolean mShowsLanguageSwitchKey;
+ public final boolean mUseContactsDict;
+ public final boolean mUsePersonalizedDicts;
+ public final boolean mUseDoubleSpacePeriod;
+ public final boolean mBlockPotentiallyOffensive;
+ // Use bigrams to predict the next word when there is no input for it yet
+ public final boolean mBigramPredictionEnabled;
+ public final boolean mGestureInputEnabled;
+ public final boolean mGestureTrailEnabled;
+ public final boolean mGestureFloatingPreviewTextEnabled;
+ public final boolean mSlidingKeyInputPreviewEnabled;
+ public final int mKeyLongpressTimeout;
+ public final boolean mEnableEmojiAltPhysicalKey;
+ public final boolean mShowAppIcon;
+ public final boolean mIsShowAppIconSettingInPreferences;
+ public final boolean mCloudSyncEnabled;
+ public final boolean mEnableMetricsLogging;
+ public final boolean mShouldShowLxxSuggestionUi;
+ // Use split layout for keyboard.
+ public final boolean mIsSplitKeyboardEnabled;
+ public final int mScreenMetrics;
+
+ // From the input box
+ @Nonnull
+ public final InputAttributes mInputAttributes;
+
+ // Deduced settings
+ public final int mKeypressVibrationDuration;
+ public final float mKeypressSoundVolume;
+ public final int mKeyPreviewPopupDismissDelay;
+ private final boolean mAutoCorrectEnabled;
+ public final float mAutoCorrectionThreshold;
+ public final float mPlausibilityThreshold;
+ public final boolean mAutoCorrectionEnabledPerUserSettings;
+ private final boolean mSuggestionsEnabledPerUserSettings;
+ private final AsyncResultHolder<AppWorkaroundsUtils> mAppWorkarounds;
+
+ // Debug settings
+ public final boolean mIsInternal;
+ public final boolean mHasCustomKeyPreviewAnimationParams;
+ public final boolean mHasKeyboardResize;
+ public final float mKeyboardHeightScale;
+ public final int mKeyPreviewShowUpDuration;
+ public final int mKeyPreviewDismissDuration;
+ public final float mKeyPreviewShowUpStartXScale;
+ public final float mKeyPreviewShowUpStartYScale;
+ public final float mKeyPreviewDismissEndXScale;
+ public final float mKeyPreviewDismissEndYScale;
+
+ @Nullable public final String mAccount;
+
+ public SettingsValues(final Context context, final SharedPreferences prefs, final Resources res,
+ @Nonnull final InputAttributes inputAttributes) {
+ mLocale = res.getConfiguration().locale;
+ // Get the resources
+ mDelayInMillisecondsToUpdateOldSuggestions =
+ res.getInteger(R.integer.config_delay_in_milliseconds_to_update_old_suggestions);
+ mSpacingAndPunctuations = new SpacingAndPunctuations(res);
+
+ // Store the input attributes
+ mInputAttributes = inputAttributes;
+
+ // Get the settings preferences
+ mAutoCap = prefs.getBoolean(Settings.PREF_AUTO_CAP, true);
+ mVibrateOn = Settings.readVibrationEnabled(prefs, res);
+ mSoundOn = Settings.readKeypressSoundEnabled(prefs, res);
+ mKeyPreviewPopupOn = Settings.readKeyPreviewPopupEnabled(prefs, res);
+ mSlidingKeyInputPreviewEnabled = prefs.getBoolean(
+ DebugSettings.PREF_SLIDING_KEY_INPUT_PREVIEW, true);
+ mShowsVoiceInputKey = needsToShowVoiceInputKey(prefs, res)
+ && mInputAttributes.mShouldShowVoiceInputKey
+ && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
+ mIncludesOtherImesInLanguageSwitchList = Settings.ENABLE_SHOW_LANGUAGE_SWITCH_KEY_SETTINGS
+ ? prefs.getBoolean(Settings.PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST, false)
+ : true /* forcibly */;
+ mShowsLanguageSwitchKey = Settings.ENABLE_SHOW_LANGUAGE_SWITCH_KEY_SETTINGS
+ ? Settings.readShowsLanguageSwitchKey(prefs) : true /* forcibly */;
+ mUseContactsDict = prefs.getBoolean(Settings.PREF_KEY_USE_CONTACTS_DICT, true);
+ mUsePersonalizedDicts = prefs.getBoolean(Settings.PREF_KEY_USE_PERSONALIZED_DICTS, true);
+ mUseDoubleSpacePeriod = prefs.getBoolean(Settings.PREF_KEY_USE_DOUBLE_SPACE_PERIOD, true)
+ && inputAttributes.mIsGeneralTextInput;
+ mBlockPotentiallyOffensive = Settings.readBlockPotentiallyOffensive(prefs, res);
+ mAutoCorrectEnabled = Settings.readAutoCorrectEnabled(prefs, res);
+ final String autoCorrectionThresholdRawValue = mAutoCorrectEnabled
+ ? res.getString(R.string.auto_correction_threshold_mode_index_modest)
+ : res.getString(R.string.auto_correction_threshold_mode_index_off);
+ mBigramPredictionEnabled = readBigramPredictionEnabled(prefs, res);
+ mDoubleSpacePeriodTimeout = res.getInteger(R.integer.config_double_space_period_timeout);
+ mHasHardwareKeyboard = Settings.readHasHardwareKeyboard(res.getConfiguration());
+ mEnableMetricsLogging = prefs.getBoolean(Settings.PREF_ENABLE_METRICS_LOGGING, true);
+ mIsSplitKeyboardEnabled = prefs.getBoolean(Settings.PREF_ENABLE_SPLIT_KEYBOARD, false);
+ mScreenMetrics = Settings.readScreenMetrics(res);
+
+ mShouldShowLxxSuggestionUi = Settings.SHOULD_SHOW_LXX_SUGGESTION_UI
+ && prefs.getBoolean(DebugSettings.PREF_SHOULD_SHOW_LXX_SUGGESTION_UI, true);
+ // Compute other readable settings
+ mKeyLongpressTimeout = Settings.readKeyLongpressTimeout(prefs, res);
+ mKeypressVibrationDuration = Settings.readKeypressVibrationDuration(prefs, res);
+ mKeypressSoundVolume = Settings.readKeypressSoundVolume(prefs, res);
+ mKeyPreviewPopupDismissDelay = Settings.readKeyPreviewPopupDismissDelay(prefs, res);
+ mEnableEmojiAltPhysicalKey = prefs.getBoolean(
+ Settings.PREF_ENABLE_EMOJI_ALT_PHYSICAL_KEY, true);
+ mShowAppIcon = Settings.readShowSetupWizardIcon(prefs, context);
+ mIsShowAppIconSettingInPreferences = prefs.contains(Settings.PREF_SHOW_SETUP_WIZARD_ICON);
+ mAutoCorrectionThreshold = readAutoCorrectionThreshold(res,
+ autoCorrectionThresholdRawValue);
+ mPlausibilityThreshold = Settings.readPlausibilityThreshold(res);
+ mGestureInputEnabled = Settings.readGestureInputEnabled(prefs, res);
+ mGestureTrailEnabled = prefs.getBoolean(Settings.PREF_GESTURE_PREVIEW_TRAIL, true);
+ mCloudSyncEnabled = prefs.getBoolean(LocalSettingsConstants.PREF_ENABLE_CLOUD_SYNC, false);
+ mAccount = prefs.getString(LocalSettingsConstants.PREF_ACCOUNT_NAME,
+ null /* default */);
+ mGestureFloatingPreviewTextEnabled = !mInputAttributes.mDisableGestureFloatingPreviewText
+ && prefs.getBoolean(Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT, true);
+ mAutoCorrectionEnabledPerUserSettings = mAutoCorrectEnabled
+ && !mInputAttributes.mInputTypeNoAutoCorrect;
+ mSuggestionsEnabledPerUserSettings = readSuggestionsEnabled(prefs);
+ mIsInternal = Settings.isInternal(prefs);
+ mHasCustomKeyPreviewAnimationParams = prefs.getBoolean(
+ DebugSettings.PREF_HAS_CUSTOM_KEY_PREVIEW_ANIMATION_PARAMS, false);
+ mHasKeyboardResize = prefs.getBoolean(DebugSettings.PREF_RESIZE_KEYBOARD, false);
+ mKeyboardHeightScale = Settings.readKeyboardHeight(prefs, DEFAULT_SIZE_SCALE);
+ mKeyPreviewShowUpDuration = Settings.readKeyPreviewAnimationDuration(
+ prefs, DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_DURATION,
+ res.getInteger(R.integer.config_key_preview_show_up_duration));
+ mKeyPreviewDismissDuration = Settings.readKeyPreviewAnimationDuration(
+ prefs, DebugSettings.PREF_KEY_PREVIEW_DISMISS_DURATION,
+ res.getInteger(R.integer.config_key_preview_dismiss_duration));
+ final float defaultKeyPreviewShowUpStartScale = ResourceUtils.getFloatFromFraction(
+ res, R.fraction.config_key_preview_show_up_start_scale);
+ final float defaultKeyPreviewDismissEndScale = ResourceUtils.getFloatFromFraction(
+ res, R.fraction.config_key_preview_dismiss_end_scale);
+ mKeyPreviewShowUpStartXScale = Settings.readKeyPreviewAnimationScale(
+ prefs, DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_START_X_SCALE,
+ defaultKeyPreviewShowUpStartScale);
+ mKeyPreviewShowUpStartYScale = Settings.readKeyPreviewAnimationScale(
+ prefs, DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_START_Y_SCALE,
+ defaultKeyPreviewShowUpStartScale);
+ mKeyPreviewDismissEndXScale = Settings.readKeyPreviewAnimationScale(
+ prefs, DebugSettings.PREF_KEY_PREVIEW_DISMISS_END_X_SCALE,
+ defaultKeyPreviewDismissEndScale);
+ mKeyPreviewDismissEndYScale = Settings.readKeyPreviewAnimationScale(
+ prefs, DebugSettings.PREF_KEY_PREVIEW_DISMISS_END_Y_SCALE,
+ defaultKeyPreviewDismissEndScale);
+ mDisplayOrientation = res.getConfiguration().orientation;
+ mAppWorkarounds = new AsyncResultHolder<>("AppWorkarounds");
+ final PackageInfo packageInfo = TargetPackageInfoGetterTask.getCachedPackageInfo(
+ mInputAttributes.mTargetApplicationPackageName);
+ if (null != packageInfo) {
+ mAppWorkarounds.set(new AppWorkaroundsUtils(packageInfo));
+ } else {
+ new TargetPackageInfoGetterTask(context, mAppWorkarounds)
+ .execute(mInputAttributes.mTargetApplicationPackageName);
+ }
+ }
+
+ public boolean isMetricsLoggingEnabled() {
+ return mEnableMetricsLogging;
+ }
+
+ public boolean isApplicationSpecifiedCompletionsOn() {
+ return mInputAttributes.mApplicationSpecifiedCompletionOn;
+ }
+
+ public boolean needsToLookupSuggestions() {
+ return mInputAttributes.mShouldShowSuggestions
+ && (mAutoCorrectionEnabledPerUserSettings || isSuggestionsEnabledPerUserSettings());
+ }
+
+ public boolean isSuggestionsEnabledPerUserSettings() {
+ return mSuggestionsEnabledPerUserSettings;
+ }
+
+ public boolean isPersonalizationEnabled() {
+ return mUsePersonalizedDicts;
+ }
+
+ public boolean isWordSeparator(final int code) {
+ return mSpacingAndPunctuations.isWordSeparator(code);
+ }
+
+ public boolean isWordConnector(final int code) {
+ return mSpacingAndPunctuations.isWordConnector(code);
+ }
+
+ public boolean isWordCodePoint(final int code) {
+ return Character.isLetter(code) || isWordConnector(code)
+ || Character.COMBINING_SPACING_MARK == Character.getType(code);
+ }
+
+ public boolean isUsuallyPrecededBySpace(final int code) {
+ return mSpacingAndPunctuations.isUsuallyPrecededBySpace(code);
+ }
+
+ public boolean isUsuallyFollowedBySpace(final int code) {
+ return mSpacingAndPunctuations.isUsuallyFollowedBySpace(code);
+ }
+
+ public boolean shouldInsertSpacesAutomatically() {
+ return mInputAttributes.mShouldInsertSpacesAutomatically;
+ }
+
+ public boolean isLanguageSwitchKeyEnabled() {
+ if (!mShowsLanguageSwitchKey) {
+ return false;
+ }
+ final RichInputMethodManager imm = RichInputMethodManager.getInstance();
+ if (mIncludesOtherImesInLanguageSwitchList) {
+ return imm.hasMultipleEnabledIMEsOrSubtypes(false /* include aux subtypes */);
+ }
+ return imm.hasMultipleEnabledSubtypesInThisIme(false /* include aux subtypes */);
+ }
+
+ public boolean isSameInputType(final EditorInfo editorInfo) {
+ return mInputAttributes.isSameInputType(editorInfo);
+ }
+
+ public boolean hasSameOrientation(final Configuration configuration) {
+ return mDisplayOrientation == configuration.orientation;
+ }
+
+ public boolean isBeforeJellyBean() {
+ final AppWorkaroundsUtils appWorkaroundUtils
+ = mAppWorkarounds.get(null, TIMEOUT_TO_GET_TARGET_PACKAGE);
+ return null == appWorkaroundUtils ? false : appWorkaroundUtils.isBeforeJellyBean();
+ }
+
+ public boolean isBrokenByRecorrection() {
+ final AppWorkaroundsUtils appWorkaroundUtils
+ = mAppWorkarounds.get(null, TIMEOUT_TO_GET_TARGET_PACKAGE);
+ return null == appWorkaroundUtils ? false : appWorkaroundUtils.isBrokenByRecorrection();
+ }
+
+ private static final String SUGGESTIONS_VISIBILITY_HIDE_VALUE_OBSOLETE = "2";
+
+ private static boolean readSuggestionsEnabled(final SharedPreferences prefs) {
+ if (prefs.contains(Settings.PREF_SHOW_SUGGESTIONS_SETTING_OBSOLETE)) {
+ final boolean alwaysHide = SUGGESTIONS_VISIBILITY_HIDE_VALUE_OBSOLETE.equals(
+ prefs.getString(Settings.PREF_SHOW_SUGGESTIONS_SETTING_OBSOLETE, null));
+ prefs.edit()
+ .remove(Settings.PREF_SHOW_SUGGESTIONS_SETTING_OBSOLETE)
+ .putBoolean(Settings.PREF_SHOW_SUGGESTIONS, !alwaysHide)
+ .apply();
+ }
+ return prefs.getBoolean(Settings.PREF_SHOW_SUGGESTIONS, true);
+ }
+
+ private static boolean readBigramPredictionEnabled(final SharedPreferences prefs,
+ final Resources res) {
+ return prefs.getBoolean(Settings.PREF_BIGRAM_PREDICTIONS, res.getBoolean(
+ R.bool.config_default_next_word_prediction));
+ }
+
+ private static float readAutoCorrectionThreshold(final Resources res,
+ final String currentAutoCorrectionSetting) {
+ final String[] autoCorrectionThresholdValues = res.getStringArray(
+ R.array.auto_correction_threshold_values);
+ // When autoCorrectionThreshold is greater than 1.0, it's like auto correction is off.
+ final float autoCorrectionThreshold;
+ try {
+ final int arrayIndex = Integer.parseInt(currentAutoCorrectionSetting);
+ if (arrayIndex >= 0 && arrayIndex < autoCorrectionThresholdValues.length) {
+ final String val = autoCorrectionThresholdValues[arrayIndex];
+ if (FLOAT_MAX_VALUE_MARKER_STRING.equals(val)) {
+ autoCorrectionThreshold = Float.MAX_VALUE;
+ } else if (FLOAT_NEGATIVE_INFINITY_MARKER_STRING.equals(val)) {
+ autoCorrectionThreshold = Float.NEGATIVE_INFINITY;
+ } else {
+ autoCorrectionThreshold = Float.parseFloat(val);
+ }
+ } else {
+ autoCorrectionThreshold = Float.MAX_VALUE;
+ }
+ } catch (final NumberFormatException e) {
+ // Whenever the threshold settings are correct, never come here.
+ Log.w(TAG, "Cannot load auto correction threshold setting."
+ + " currentAutoCorrectionSetting: " + currentAutoCorrectionSetting
+ + ", autoCorrectionThresholdValues: "
+ + Arrays.toString(autoCorrectionThresholdValues), e);
+ return Float.MAX_VALUE;
+ }
+ return autoCorrectionThreshold;
+ }
+
+ private static boolean needsToShowVoiceInputKey(final SharedPreferences prefs,
+ final Resources res) {
+ // Migrate preference from {@link Settings#PREF_VOICE_MODE_OBSOLETE} to
+ // {@link Settings#PREF_VOICE_INPUT_KEY}.
+ if (prefs.contains(Settings.PREF_VOICE_MODE_OBSOLETE)) {
+ final String voiceModeMain = res.getString(R.string.voice_mode_main);
+ final String voiceMode = prefs.getString(
+ Settings.PREF_VOICE_MODE_OBSOLETE, voiceModeMain);
+ final boolean shouldShowVoiceInputKey = voiceModeMain.equals(voiceMode);
+ prefs.edit()
+ .putBoolean(Settings.PREF_VOICE_INPUT_KEY, shouldShowVoiceInputKey)
+ // Remove the obsolete preference if exists.
+ .remove(Settings.PREF_VOICE_MODE_OBSOLETE)
+ .apply();
+ }
+ return prefs.getBoolean(Settings.PREF_VOICE_INPUT_KEY, true);
+ }
+
+ public String dump() {
+ final StringBuilder sb = new StringBuilder("Current settings :");
+ sb.append("\n mSpacingAndPunctuations = ");
+ sb.append("" + mSpacingAndPunctuations.dump());
+ sb.append("\n mDelayInMillisecondsToUpdateOldSuggestions = ");
+ sb.append("" + mDelayInMillisecondsToUpdateOldSuggestions);
+ sb.append("\n mAutoCap = ");
+ sb.append("" + mAutoCap);
+ sb.append("\n mVibrateOn = ");
+ sb.append("" + mVibrateOn);
+ sb.append("\n mSoundOn = ");
+ sb.append("" + mSoundOn);
+ sb.append("\n mKeyPreviewPopupOn = ");
+ sb.append("" + mKeyPreviewPopupOn);
+ sb.append("\n mShowsVoiceInputKey = ");
+ sb.append("" + mShowsVoiceInputKey);
+ sb.append("\n mIncludesOtherImesInLanguageSwitchList = ");
+ sb.append("" + mIncludesOtherImesInLanguageSwitchList);
+ sb.append("\n mShowsLanguageSwitchKey = ");
+ sb.append("" + mShowsLanguageSwitchKey);
+ sb.append("\n mUseContactsDict = ");
+ sb.append("" + mUseContactsDict);
+ sb.append("\n mUsePersonalizedDicts = ");
+ sb.append("" + mUsePersonalizedDicts);
+ sb.append("\n mUseDoubleSpacePeriod = ");
+ sb.append("" + mUseDoubleSpacePeriod);
+ sb.append("\n mBlockPotentiallyOffensive = ");
+ sb.append("" + mBlockPotentiallyOffensive);
+ sb.append("\n mBigramPredictionEnabled = ");
+ sb.append("" + mBigramPredictionEnabled);
+ sb.append("\n mGestureInputEnabled = ");
+ sb.append("" + mGestureInputEnabled);
+ sb.append("\n mGestureTrailEnabled = ");
+ sb.append("" + mGestureTrailEnabled);
+ sb.append("\n mGestureFloatingPreviewTextEnabled = ");
+ sb.append("" + mGestureFloatingPreviewTextEnabled);
+ sb.append("\n mSlidingKeyInputPreviewEnabled = ");
+ sb.append("" + mSlidingKeyInputPreviewEnabled);
+ sb.append("\n mKeyLongpressTimeout = ");
+ sb.append("" + mKeyLongpressTimeout);
+ sb.append("\n mLocale = ");
+ sb.append("" + mLocale);
+ sb.append("\n mInputAttributes = ");
+ sb.append("" + mInputAttributes);
+ sb.append("\n mKeypressVibrationDuration = ");
+ sb.append("" + mKeypressVibrationDuration);
+ sb.append("\n mKeypressSoundVolume = ");
+ sb.append("" + mKeypressSoundVolume);
+ sb.append("\n mKeyPreviewPopupDismissDelay = ");
+ sb.append("" + mKeyPreviewPopupDismissDelay);
+ sb.append("\n mAutoCorrectEnabled = ");
+ sb.append("" + mAutoCorrectEnabled);
+ sb.append("\n mAutoCorrectionThreshold = ");
+ sb.append("" + mAutoCorrectionThreshold);
+ sb.append("\n mAutoCorrectionEnabledPerUserSettings = ");
+ sb.append("" + mAutoCorrectionEnabledPerUserSettings);
+ sb.append("\n mSuggestionsEnabledPerUserSettings = ");
+ sb.append("" + mSuggestionsEnabledPerUserSettings);
+ sb.append("\n mDisplayOrientation = ");
+ sb.append("" + mDisplayOrientation);
+ sb.append("\n mAppWorkarounds = ");
+ final AppWorkaroundsUtils awu = mAppWorkarounds.get(null, 0);
+ sb.append("" + (null == awu ? "null" : awu.toString()));
+ sb.append("\n mIsInternal = ");
+ sb.append("" + mIsInternal);
+ sb.append("\n mKeyPreviewShowUpDuration = ");
+ sb.append("" + mKeyPreviewShowUpDuration);
+ sb.append("\n mKeyPreviewDismissDuration = ");
+ sb.append("" + mKeyPreviewDismissDuration);
+ sb.append("\n mKeyPreviewShowUpStartScaleX = ");
+ sb.append("" + mKeyPreviewShowUpStartXScale);
+ sb.append("\n mKeyPreviewShowUpStartScaleY = ");
+ sb.append("" + mKeyPreviewShowUpStartYScale);
+ sb.append("\n mKeyPreviewDismissEndScaleX = ");
+ sb.append("" + mKeyPreviewDismissEndXScale);
+ sb.append("\n mKeyPreviewDismissEndScaleY = ");
+ sb.append("" + mKeyPreviewDismissEndYScale);
+ return sb.toString();
+ }
+}
diff --git a/java/src/org/kelar/inputmethod/latin/settings/SettingsValuesForSuggestion.java b/java/src/org/kelar/inputmethod/latin/settings/SettingsValuesForSuggestion.java
new file mode 100644
index 000000000..b0b3c1d73
--- /dev/null
+++ b/java/src/org/kelar/inputmethod/latin/settings/SettingsValuesForSuggestion.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.kelar.inputmethod.latin.settings;
+
+public class SettingsValuesForSuggestion {
+ public final boolean mBlockPotentiallyOffensive;
+
+ public SettingsValuesForSuggestion(final boolean blockPotentiallyOffensive) {
+ mBlockPotentiallyOffensive = blockPotentiallyOffensive;
+ }
+}
diff --git a/java/src/org/kelar/inputmethod/latin/settings/SpacingAndPunctuations.java b/java/src/org/kelar/inputmethod/latin/settings/SpacingAndPunctuations.java
new file mode 100644
index 000000000..0145ead8e
--- /dev/null
+++ b/java/src/org/kelar/inputmethod/latin/settings/SpacingAndPunctuations.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.kelar.inputmethod.latin.settings;
+
+import android.content.res.Resources;
+
+import org.kelar.inputmethod.annotations.UsedForTesting;
+import org.kelar.inputmethod.keyboard.internal.MoreKeySpec;
+import org.kelar.inputmethod.latin.PunctuationSuggestions;
+import org.kelar.inputmethod.latin.R;
+import org.kelar.inputmethod.latin.common.Constants;
+import org.kelar.inputmethod.latin.common.StringUtils;
+
+import java.util.Arrays;
+import java.util.Locale;
+
+public final class SpacingAndPunctuations {
+ private final int[] mSortedSymbolsPrecededBySpace;
+ private final int[] mSortedSymbolsFollowedBySpace;
+ private final int[] mSortedSymbolsClusteringTogether;
+ private final int[] mSortedWordConnectors;
+ public final int[] mSortedWordSeparators;
+ public final PunctuationSuggestions mSuggestPuncList;
+ private final int mSentenceSeparator;
+ private final int mAbbreviationMarker;
+ private final int[] mSortedSentenceTerminators;
+ public final String mSentenceSeparatorAndSpace;
+ public final boolean mCurrentLanguageHasSpaces;
+ public final boolean mUsesAmericanTypography;
+ public final boolean mUsesGermanRules;
+
+ public SpacingAndPunctuations(final Resources res) {
+ // To be able to binary search the code point. See {@link #isUsuallyPrecededBySpace(int)}.
+ mSortedSymbolsPrecededBySpace = StringUtils.toSortedCodePointArray(
+ res.getString(R.string.symbols_preceded_by_space));
+ // To be able to binary search the code point. See {@link #isUsuallyFollowedBySpace(int)}.
+ mSortedSymbolsFollowedBySpace = StringUtils.toSortedCodePointArray(
+ res.getString(R.string.symbols_followed_by_space));
+ mSortedSymbolsClusteringTogether = StringUtils.toSortedCodePointArray(
+ res.getString(R.string.symbols_clustering_together));
+ // To be able to binary search the code point. See {@link #isWordConnector(int)}.
+ mSortedWordConnectors = StringUtils.toSortedCodePointArray(
+ res.getString(R.string.symbols_word_connectors));
+ mSortedWordSeparators = StringUtils.toSortedCodePointArray(
+ res.getString(R.string.symbols_word_separators));
+ mSortedSentenceTerminators = StringUtils.toSortedCodePointArray(
+ res.getString(R.string.symbols_sentence_terminators));
+ mSentenceSeparator = res.getInteger(R.integer.sentence_separator);
+ mAbbreviationMarker = res.getInteger(R.integer.abbreviation_marker);
+ mSentenceSeparatorAndSpace = new String(new int[] {
+ mSentenceSeparator, Constants.CODE_SPACE }, 0, 2);
+ mCurrentLanguageHasSpaces = res.getBoolean(R.bool.current_language_has_spaces);
+ final Locale locale = res.getConfiguration().locale;
+ // Heuristic: we use American Typography rules because it's the most common rules for all
+ // English variants. German rules (not "German typography") also have small gotchas.
+ mUsesAmericanTypography = Locale.ENGLISH.getLanguage().equals(locale.getLanguage());
+ mUsesGermanRules = Locale.GERMAN.getLanguage().equals(locale.getLanguage());
+ final String[] suggestPuncsSpec = MoreKeySpec.splitKeySpecs(
+ res.getString(R.string.suggested_punctuations));
+ mSuggestPuncList = PunctuationSuggestions.newPunctuationSuggestions(suggestPuncsSpec);
+ }
+
+ @UsedForTesting
+ public SpacingAndPunctuations(final SpacingAndPunctuations model,
+ final int[] overrideSortedWordSeparators) {
+ mSortedSymbolsPrecededBySpace = model.mSortedSymbolsPrecededBySpace;
+ mSortedSymbolsFollowedBySpace = model.mSortedSymbolsFollowedBySpace;
+ mSortedSymbolsClusteringTogether = model.mSortedSymbolsClusteringTogether;
+ mSortedWordConnectors = model.mSortedWordConnectors;
+ mSortedWordSeparators = overrideSortedWordSeparators;
+ mSortedSentenceTerminators = model.mSortedSentenceTerminators;
+ mSuggestPuncList = model.mSuggestPuncList;
+ mSentenceSeparator = model.mSentenceSeparator;
+ mAbbreviationMarker = model.mAbbreviationMarker;
+ mSentenceSeparatorAndSpace = model.mSentenceSeparatorAndSpace;
+ mCurrentLanguageHasSpaces = model.mCurrentLanguageHasSpaces;
+ mUsesAmericanTypography = model.mUsesAmericanTypography;
+ mUsesGermanRules = model.mUsesGermanRules;
+ }
+
+ public boolean isWordSeparator(final int code) {
+ return Arrays.binarySearch(mSortedWordSeparators, code) >= 0;
+ }
+
+ public boolean isWordConnector(final int code) {
+ return Arrays.binarySearch(mSortedWordConnectors, code) >= 0;
+ }
+
+ public boolean isWordCodePoint(final int code) {
+ return Character.isLetter(code) || isWordConnector(code);
+ }
+
+ public boolean isUsuallyPrecededBySpace(final int code) {
+ return Arrays.binarySearch(mSortedSymbolsPrecededBySpace, code) >= 0;
+ }
+
+ public boolean isUsuallyFollowedBySpace(final int code) {
+ return Arrays.binarySearch(mSortedSymbolsFollowedBySpace, code) >= 0;
+ }
+
+ public boolean isClusteringSymbol(final int code) {
+ return Arrays.binarySearch(mSortedSymbolsClusteringTogether, code) >= 0;
+ }
+
+ public boolean isSentenceTerminator(final int code) {
+ return Arrays.binarySearch(mSortedSentenceTerminators, code) >= 0;
+ }
+
+ public boolean isAbbreviationMarker(final int code) {
+ return code == mAbbreviationMarker;
+ }
+
+ public boolean isSentenceSeparator(final int code) {
+ return code == mSentenceSeparator;
+ }
+
+ public String dump() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("mSortedSymbolsPrecededBySpace = ");
+ sb.append("" + Arrays.toString(mSortedSymbolsPrecededBySpace));
+ sb.append("\n mSortedSymbolsFollowedBySpace = ");
+ sb.append("" + Arrays.toString(mSortedSymbolsFollowedBySpace));
+ sb.append("\n mSortedWordConnectors = ");
+ sb.append("" + Arrays.toString(mSortedWordConnectors));
+ sb.append("\n mSortedWordSeparators = ");
+ sb.append("" + Arrays.toString(mSortedWordSeparators));
+ sb.append("\n mSuggestPuncList = ");
+ sb.append("" + mSuggestPuncList);
+ sb.append("\n mSentenceSeparator = ");
+ sb.append("" + mSentenceSeparator);
+ sb.append("\n mSentenceSeparatorAndSpace = ");
+ sb.append("" + mSentenceSeparatorAndSpace);
+ sb.append("\n mCurrentLanguageHasSpaces = ");
+ sb.append("" + mCurrentLanguageHasSpaces);
+ sb.append("\n mUsesAmericanTypography = ");
+ sb.append("" + mUsesAmericanTypography);
+ sb.append("\n mUsesGermanRules = ");
+ sb.append("" + mUsesGermanRules);
+ return sb.toString();
+ }
+}
diff --git a/java/src/org/kelar/inputmethod/latin/settings/SubScreenFragment.java b/java/src/org/kelar/inputmethod/latin/settings/SubScreenFragment.java
new file mode 100644
index 000000000..08c9bd441
--- /dev/null
+++ b/java/src/org/kelar/inputmethod/latin/settings/SubScreenFragment.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.kelar.inputmethod.latin.settings;
+
+import android.app.backup.BackupManager;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceScreen;
+import android.util.Log;
+
+/**
+ * A base abstract class for a {@link PreferenceFragment} that implements a nested
+ * {@link PreferenceScreen} of the main preference screen.
+ */
+public abstract class SubScreenFragment extends PreferenceFragment
+ implements OnSharedPreferenceChangeListener {
+ private OnSharedPreferenceChangeListener mSharedPreferenceChangeListener;
+
+ static void setPreferenceEnabled(final String prefKey, final boolean enabled,
+ final PreferenceScreen screen) {
+ final Preference preference = screen.findPreference(prefKey);
+ if (preference != null) {
+ preference.setEnabled(enabled);
+ }
+ }
+
+ static void removePreference(final String prefKey, final PreferenceScreen screen) {
+ final Preference preference = screen.findPreference(prefKey);
+ if (preference != null) {
+ screen.removePreference(preference);
+ }
+ }
+
+ static void updateListPreferenceSummaryToCurrentValue(final String prefKey,
+ final PreferenceScreen screen) {
+ // Because the "%s" summary trick of {@link ListPreference} doesn't work properly before
+ // KitKat, we need to update the summary programmatically.
+ final ListPreference listPreference = (ListPreference)screen.findPreference(prefKey);
+ if (listPreference == null) {
+ return;
+ }
+ final CharSequence entries[] = listPreference.getEntries();
+ final int entryIndex = listPreference.findIndexOfValue(listPreference.getValue());
+ listPreference.setSummary(entryIndex < 0 ? null : entries[entryIndex]);
+ }
+
+ final void setPreferenceEnabled(final String prefKey, final boolean enabled) {
+ setPreferenceEnabled(prefKey, enabled, getPreferenceScreen());
+ }
+
+ final void removePreference(final String prefKey) {
+ removePreference(prefKey, getPreferenceScreen());
+ }
+
+ final void updateListPreferenceSummaryToCurrentValue(final String prefKey) {
+ updateListPreferenceSummaryToCurrentValue(prefKey, getPreferenceScreen());
+ }
+
+ final SharedPreferences getSharedPreferences() {
+ return getPreferenceManager().getSharedPreferences();
+ }
+
+ /**
+ * Gets the application name to display on the UI.
+ */
+ final String getApplicationName() {
+ final Context context = getActivity();
+ final Resources res = getResources();
+ final int applicationLabelRes = context.getApplicationInfo().labelRes;
+ return res.getString(applicationLabelRes);
+ }
+
+ @Override
+ public void addPreferencesFromResource(final int preferencesResId) {
+ super.addPreferencesFromResource(preferencesResId);
+ TwoStatePreferenceHelper.replaceCheckBoxPreferencesBySwitchPreferences(
+ getPreferenceScreen());
+ }
+
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mSharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
+ @Override
+ public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
+ final SubScreenFragment fragment = SubScreenFragment.this;
+ final Context context = fragment.getActivity();
+ if (context == null || fragment.getPreferenceScreen() == null) {
+ final String tag = fragment.getClass().getSimpleName();
+ // TODO: Introduce a static function to register this class and ensure that
+ // onCreate must be called before "onSharedPreferenceChanged" is called.
+ Log.w(tag, "onSharedPreferenceChanged called before activity starts.");
+ return;
+ }
+ new BackupManager(context).dataChanged();
+ fragment.onSharedPreferenceChanged(prefs, key);
+ }
+ };
+ getSharedPreferences().registerOnSharedPreferenceChangeListener(
+ mSharedPreferenceChangeListener);
+ }
+
+ @Override
+ public void onDestroy() {
+ getSharedPreferences().unregisterOnSharedPreferenceChangeListener(
+ mSharedPreferenceChangeListener);
+ super.onDestroy();
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
+ // This method may be overridden by an extended class.
+ }
+}
diff --git a/java/src/org/kelar/inputmethod/latin/settings/TestFragmentActivity.java b/java/src/org/kelar/inputmethod/latin/settings/TestFragmentActivity.java
new file mode 100644
index 000000000..c235dc8f5
--- /dev/null
+++ b/java/src/org/kelar/inputmethod/latin/settings/TestFragmentActivity.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.kelar.inputmethod.latin.settings;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.content.Intent;
+import android.os.Bundle;
+
+/**
+ * Test activity to use when testing preference fragments. <br/>
+ * Usage: <br/>
+ * Create an ActivityInstrumentationTestCase2 for this activity
+ * and call setIntent() with an intent that specifies the fragment to load in the activity.
+ * The fragment can then be obtained from this activity and used for testing/verification.
+ */
+public final class TestFragmentActivity extends Activity {
+ /**
+ * The fragment name that should be loaded when starting this activity.
+ * This must be specified when starting this activity, as this activity is only
+ * meant to test fragments from instrumentation tests.
+ */
+ public static final String EXTRA_SHOW_FRAGMENT = "show_fragment";
+
+ public Fragment mFragment;
+
+ @Override
+ protected void onCreate(final Bundle savedState) {
+ super.onCreate(savedState);
+ final Intent intent = getIntent();
+ final String fragmentName = intent.getStringExtra(EXTRA_SHOW_FRAGMENT);
+ if (fragmentName == null) {
+ throw new IllegalArgumentException("No fragment name specified for testing");
+ }
+
+ mFragment = Fragment.instantiate(this, fragmentName);
+ FragmentManager fragmentManager = getFragmentManager();
+ fragmentManager.beginTransaction().add(mFragment, fragmentName).commit();
+ }
+}
diff --git a/java/src/org/kelar/inputmethod/latin/settings/ThemeSettingsFragment.java b/java/src/org/kelar/inputmethod/latin/settings/ThemeSettingsFragment.java
new file mode 100644
index 000000000..f0d51196c
--- /dev/null
+++ b/java/src/org/kelar/inputmethod/latin/settings/ThemeSettingsFragment.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.kelar.inputmethod.latin.settings;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceScreen;
+
+import org.kelar.inputmethod.keyboard.KeyboardTheme;
+import org.kelar.inputmethod.latin.R;
+import org.kelar.inputmethod.latin.settings.RadioButtonPreference.OnRadioButtonClickedListener;
+
+/**
+ * "Keyboard theme" settings sub screen.
+ */
+public final class ThemeSettingsFragment extends SubScreenFragment
+ implements OnRadioButtonClickedListener {
+ private int mSelectedThemeId;
+
+ static class KeyboardThemePreference extends RadioButtonPreference {
+ final int mThemeId;
+
+ KeyboardThemePreference(final Context context, final String name, final int id) {
+ super(context);
+ setTitle(name);
+ mThemeId = id;
+ }
+ }
+
+ static void updateKeyboardThemeSummary(final Preference pref) {
+ final Context context = pref.getContext();
+ final Resources res = context.getResources();
+ final KeyboardTheme keyboardTheme = KeyboardTheme.getKeyboardTheme(context);
+ final String[] keyboardThemeNames = res.getStringArray(R.array.keyboard_theme_names);
+ final int[] keyboardThemeIds = res.getIntArray(R.array.keyboard_theme_ids);
+ for (int index = 0; index < keyboardThemeNames.length; index++) {
+ if (keyboardTheme.mThemeId == keyboardThemeIds[index]) {
+ pref.setSummary(keyboardThemeNames[index]);
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onCreate(final Bundle icicle) {
+ super.onCreate(icicle);
+ addPreferencesFromResource(R.xml.prefs_screen_theme);
+ final PreferenceScreen screen = getPreferenceScreen();
+ final Context context = getActivity();
+ final Resources res = getResources();
+ final String[] keyboardThemeNames = res.getStringArray(R.array.keyboard_theme_names);
+ final int[] keyboardThemeIds = res.getIntArray(R.array.keyboard_theme_ids);
+ for (int index = 0; index < keyboardThemeNames.length; index++) {
+ final KeyboardThemePreference pref = new KeyboardThemePreference(
+ context, keyboardThemeNames[index], keyboardThemeIds[index]);
+ screen.addPreference(pref);
+ pref.setOnRadioButtonClickedListener(this);
+ }
+ final KeyboardTheme keyboardTheme = KeyboardTheme.getKeyboardTheme(context);
+ mSelectedThemeId = keyboardTheme.mThemeId;
+ }
+
+ @Override
+ public void onRadioButtonClicked(final RadioButtonPreference preference) {
+ if (preference instanceof KeyboardThemePreference) {
+ final KeyboardThemePreference pref = (KeyboardThemePreference)preference;
+ mSelectedThemeId = pref.mThemeId;
+ updateSelected();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ updateSelected();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ KeyboardTheme.saveKeyboardThemeId(mSelectedThemeId, getSharedPreferences());
+ }
+
+ private void updateSelected() {
+ final PreferenceScreen screen = getPreferenceScreen();
+ final int count = screen.getPreferenceCount();
+ for (int index = 0; index < count; index++) {
+ final Preference preference = screen.getPreference(index);
+ if (preference instanceof KeyboardThemePreference) {
+ final KeyboardThemePreference pref = (KeyboardThemePreference)preference;
+ final boolean selected = (mSelectedThemeId == pref.mThemeId);
+ pref.setSelected(selected);
+ }
+ }
+ }
+}
diff --git a/java/src/org/kelar/inputmethod/latin/settings/TwoStatePreferenceHelper.java b/java/src/org/kelar/inputmethod/latin/settings/TwoStatePreferenceHelper.java
new file mode 100644
index 000000000..7657a4022
--- /dev/null
+++ b/java/src/org/kelar/inputmethod/latin/settings/TwoStatePreferenceHelper.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.kelar.inputmethod.latin.settings;
+
+import android.os.Build;
+import android.preference.CheckBoxPreference;
+import android.preference.Preference;
+import android.preference.PreferenceGroup;
+import android.preference.SwitchPreference;
+
+import java.util.ArrayList;
+
+public class TwoStatePreferenceHelper {
+ private static final String EMPTY_TEXT = "";
+
+ private TwoStatePreferenceHelper() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static void replaceCheckBoxPreferencesBySwitchPreferences(final PreferenceGroup group) {
+ // The keyboard settings keeps using a CheckBoxPreference on KitKat or previous.
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
+ return;
+ }
+ // The keyboard settings starts using a SwitchPreference without switch on/off text on
+ // API versions newer than KitKat.
+ replaceAllCheckBoxPreferencesBySwitchPreferences(group);
+ }
+
+ private static void replaceAllCheckBoxPreferencesBySwitchPreferences(
+ final PreferenceGroup group) {
+ final ArrayList<Preference> preferences = new ArrayList<>();
+ final int count = group.getPreferenceCount();
+ for (int index = 0; index < count; index++) {
+ preferences.add(group.getPreference(index));
+ }
+ group.removeAll();
+ for (int index = 0; index < count; index++) {
+ final Preference preference = preferences.get(index);
+ if (preference instanceof CheckBoxPreference) {
+ addSwitchPreferenceBasedOnCheckBoxPreference((CheckBoxPreference)preference, group);
+ } else {
+ group.addPreference(preference);
+ if (preference instanceof PreferenceGroup) {
+ replaceAllCheckBoxPreferencesBySwitchPreferences((PreferenceGroup)preference);
+ }
+ }
+ }
+ }
+
+ static void addSwitchPreferenceBasedOnCheckBoxPreference(final CheckBoxPreference checkBox,
+ final PreferenceGroup group) {
+ final SwitchPreference switchPref = new SwitchPreference(checkBox.getContext());
+ switchPref.setTitle(checkBox.getTitle());
+ switchPref.setKey(checkBox.getKey());
+ switchPref.setOrder(checkBox.getOrder());
+ switchPref.setPersistent(checkBox.isPersistent());
+ switchPref.setEnabled(checkBox.isEnabled());
+ switchPref.setChecked(checkBox.isChecked());
+ switchPref.setSummary(checkBox.getSummary());
+ switchPref.setSummaryOn(checkBox.getSummaryOn());
+ switchPref.setSummaryOff(checkBox.getSummaryOff());
+ switchPref.setSwitchTextOn(EMPTY_TEXT);
+ switchPref.setSwitchTextOff(EMPTY_TEXT);
+ group.addPreference(switchPref);
+ switchPref.setDependency(checkBox.getDependency());
+ }
+}