1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.android.settingslib.inputmethod;
18
19import android.content.Context;
20import android.content.pm.PackageManager;
21import android.content.res.Configuration;
22import android.support.v14.preference.PreferenceFragment;
23import android.support.v7.preference.Preference;
24import android.support.v7.preference.PreferenceCategory;
25import android.support.v7.preference.PreferenceScreen;
26import android.support.v7.preference.TwoStatePreference;
27import android.text.TextUtils;
28import android.view.inputmethod.InputMethodInfo;
29import android.view.inputmethod.InputMethodManager;
30import android.view.inputmethod.InputMethodSubtype;
31
32import com.android.settingslib.R;
33
34import java.text.Collator;
35import java.util.ArrayList;
36import java.util.HashMap;
37import java.util.List;
38
39public class InputMethodAndSubtypeEnablerManager implements Preference.OnPreferenceChangeListener {
40
41    private final PreferenceFragment mFragment;
42
43    private boolean mHaveHardKeyboard;
44    private final HashMap<String, List<Preference>> mInputMethodAndSubtypePrefsMap =
45            new HashMap<>();
46    private final HashMap<String, TwoStatePreference> mAutoSelectionPrefsMap = new HashMap<>();
47    private InputMethodManager mImm;
48    // TODO: Change mInputMethodInfoList to Map
49    private List<InputMethodInfo> mInputMethodInfoList;
50    private final Collator mCollator = Collator.getInstance();
51
52    public InputMethodAndSubtypeEnablerManager(PreferenceFragment fragment) {
53        mFragment = fragment;
54        mImm = fragment.getContext().getSystemService(InputMethodManager.class);
55
56        mInputMethodInfoList = mImm.getInputMethodList();
57    }
58
59    public void init(PreferenceFragment fragment, String targetImi, PreferenceScreen root) {
60        final Configuration config = fragment.getResources().getConfiguration();
61        mHaveHardKeyboard = (config.keyboard == Configuration.KEYBOARD_QWERTY);
62
63        for (final InputMethodInfo imi : mInputMethodInfoList) {
64            // Add subtype preferences of this IME when it is specified or no IME is specified.
65            if (imi.getId().equals(targetImi) || TextUtils.isEmpty(targetImi)) {
66                addInputMethodSubtypePreferences(fragment, imi, root);
67            }
68        }
69    }
70
71    public void refresh(Context context, PreferenceFragment fragment) {
72        // Refresh internal states in mInputMethodSettingValues to keep the latest
73        // "InputMethodInfo"s and "InputMethodSubtype"s
74        InputMethodSettingValuesWrapper
75                .getInstance(context).refreshAllInputMethodAndSubtypes();
76        InputMethodAndSubtypeUtil.loadInputMethodSubtypeList(fragment, context.getContentResolver(),
77                mInputMethodInfoList, mInputMethodAndSubtypePrefsMap);
78        updateAutoSelectionPreferences();
79    }
80
81    public void save(Context context, PreferenceFragment fragment) {
82        InputMethodAndSubtypeUtil.saveInputMethodSubtypeList(fragment, context.getContentResolver(),
83                mInputMethodInfoList, mHaveHardKeyboard);
84    }
85
86    @Override
87    public boolean onPreferenceChange(final Preference pref, final Object newValue) {
88        if (!(newValue instanceof Boolean)) {
89            return true; // Invoke default behavior.
90        }
91        final boolean isChecking = (Boolean) newValue;
92        for (final String imiId : mAutoSelectionPrefsMap.keySet()) {
93            // An auto select subtype preference is changing.
94            if (mAutoSelectionPrefsMap.get(imiId) == pref) {
95                final TwoStatePreference autoSelectionPref = (TwoStatePreference) pref;
96                autoSelectionPref.setChecked(isChecking);
97                // Enable or disable subtypes depending on the auto selection preference.
98                setAutoSelectionSubtypesEnabled(imiId, autoSelectionPref.isChecked());
99                return false;
100            }
101        }
102        // A subtype preference is changing.
103        if (pref instanceof InputMethodSubtypePreference) {
104            final InputMethodSubtypePreference subtypePref = (InputMethodSubtypePreference) pref;
105            subtypePref.setChecked(isChecking);
106            if (!subtypePref.isChecked()) {
107                // It takes care of the case where no subtypes are explicitly enabled then the auto
108                // selection preference is going to be checked.
109                updateAutoSelectionPreferences();
110            }
111            return false;
112        }
113        return true; // Invoke default behavior.
114    }
115
116    private void addInputMethodSubtypePreferences(PreferenceFragment fragment, InputMethodInfo imi,
117            final PreferenceScreen root) {
118        Context prefContext = fragment.getPreferenceManager().getContext();
119
120        final int subtypeCount = imi.getSubtypeCount();
121        if (subtypeCount <= 1) {
122            return;
123        }
124        final String imiId = imi.getId();
125        final PreferenceCategory keyboardSettingsCategory =
126                new PreferenceCategory(prefContext);
127        root.addPreference(keyboardSettingsCategory);
128        final PackageManager pm = prefContext.getPackageManager();
129        final CharSequence label = imi.loadLabel(pm);
130
131        keyboardSettingsCategory.setTitle(label);
132        keyboardSettingsCategory.setKey(imiId);
133        // TODO: Use toggle Preference if images are ready.
134        final TwoStatePreference autoSelectionPref =
135                new SwitchWithNoTextPreference(prefContext);
136        mAutoSelectionPrefsMap.put(imiId, autoSelectionPref);
137        keyboardSettingsCategory.addPreference(autoSelectionPref);
138        autoSelectionPref.setOnPreferenceChangeListener(this);
139
140        final PreferenceCategory activeInputMethodsCategory =
141                new PreferenceCategory(prefContext);
142        activeInputMethodsCategory.setTitle(R.string.active_input_method_subtypes);
143        root.addPreference(activeInputMethodsCategory);
144
145        CharSequence autoSubtypeLabel = null;
146        final ArrayList<Preference> subtypePreferences = new ArrayList<>();
147        for (int index = 0; index < subtypeCount; ++index) {
148            final InputMethodSubtype subtype = imi.getSubtypeAt(index);
149            if (subtype.overridesImplicitlyEnabledSubtype()) {
150                if (autoSubtypeLabel == null) {
151                    autoSubtypeLabel = InputMethodAndSubtypeUtil.getSubtypeLocaleNameAsSentence(
152                            subtype, prefContext, imi);
153                }
154            } else {
155                final Preference subtypePref = new InputMethodSubtypePreference(
156                        prefContext, subtype, imi);
157                subtypePreferences.add(subtypePref);
158            }
159        }
160        subtypePreferences.sort((lhs, rhs) -> {
161            if (lhs instanceof InputMethodSubtypePreference) {
162                return ((InputMethodSubtypePreference) lhs).compareTo(rhs, mCollator);
163            }
164            return lhs.compareTo(rhs);
165        });
166        for (final Preference pref : subtypePreferences) {
167            activeInputMethodsCategory.addPreference(pref);
168            pref.setOnPreferenceChangeListener(this);
169            InputMethodAndSubtypeUtil.removeUnnecessaryNonPersistentPreference(pref);
170        }
171        mInputMethodAndSubtypePrefsMap.put(imiId, subtypePreferences);
172        if (TextUtils.isEmpty(autoSubtypeLabel)) {
173            autoSelectionPref.setTitle(
174                    R.string.use_system_language_to_select_input_method_subtypes);
175        } else {
176            autoSelectionPref.setTitle(autoSubtypeLabel);
177        }
178    }
179
180    private boolean isNoSubtypesExplicitlySelected(final String imiId) {
181        final List<Preference> subtypePrefs = mInputMethodAndSubtypePrefsMap.get(imiId);
182        for (final Preference pref : subtypePrefs) {
183            if (pref instanceof TwoStatePreference && ((TwoStatePreference) pref).isChecked()) {
184                return false;
185            }
186        }
187        return true;
188    }
189
190    private void setAutoSelectionSubtypesEnabled(final String imiId,
191            final boolean autoSelectionEnabled) {
192        final TwoStatePreference autoSelectionPref = mAutoSelectionPrefsMap.get(imiId);
193        if (autoSelectionPref == null) {
194            return;
195        }
196        autoSelectionPref.setChecked(autoSelectionEnabled);
197        final List<Preference> subtypePrefs = mInputMethodAndSubtypePrefsMap.get(imiId);
198        for (final Preference pref : subtypePrefs) {
199            if (pref instanceof TwoStatePreference) {
200                // When autoSelectionEnabled is true, all subtype prefs need to be disabled with
201                // implicitly checked subtypes. In case of false, all subtype prefs need to be
202                // enabled.
203                pref.setEnabled(!autoSelectionEnabled);
204                if (autoSelectionEnabled) {
205                    ((TwoStatePreference) pref).setChecked(false);
206                }
207            }
208        }
209        if (autoSelectionEnabled) {
210            InputMethodAndSubtypeUtil.saveInputMethodSubtypeList(
211                    mFragment, mFragment.getContext().getContentResolver(),
212                    mInputMethodInfoList, mHaveHardKeyboard);
213            updateImplicitlyEnabledSubtypes(imiId);
214        }
215    }
216
217    private void updateImplicitlyEnabledSubtypes(final String targetImiId) {
218        // When targetImiId is null, apply to all subtypes of all IMEs
219        for (final InputMethodInfo imi : mInputMethodInfoList) {
220            final String imiId = imi.getId();
221            final TwoStatePreference autoSelectionPref = mAutoSelectionPrefsMap.get(imiId);
222            // No need to update implicitly enabled subtypes when the user has unchecked the
223            // "subtype auto selection".
224            if (autoSelectionPref == null || !autoSelectionPref.isChecked()) {
225                continue;
226            }
227            if (imiId.equals(targetImiId) || targetImiId == null) {
228                updateImplicitlyEnabledSubtypesOf(imi);
229            }
230        }
231    }
232
233    private void updateImplicitlyEnabledSubtypesOf(final InputMethodInfo imi) {
234        final String imiId = imi.getId();
235        final List<Preference> subtypePrefs = mInputMethodAndSubtypePrefsMap.get(imiId);
236        final List<InputMethodSubtype> implicitlyEnabledSubtypes =
237                mImm.getEnabledInputMethodSubtypeList(imi, true);
238        if (subtypePrefs == null || implicitlyEnabledSubtypes == null) {
239            return;
240        }
241        for (final Preference pref : subtypePrefs) {
242            if (!(pref instanceof TwoStatePreference)) {
243                continue;
244            }
245            final TwoStatePreference subtypePref = (TwoStatePreference) pref;
246            subtypePref.setChecked(false);
247            for (final InputMethodSubtype subtype : implicitlyEnabledSubtypes) {
248                final String implicitlyEnabledSubtypePrefKey = imiId + subtype.hashCode();
249                if (subtypePref.getKey().equals(implicitlyEnabledSubtypePrefKey)) {
250                    subtypePref.setChecked(true);
251                    break;
252                }
253            }
254        }
255    }
256
257    private void updateAutoSelectionPreferences() {
258        for (final String imiId : mInputMethodAndSubtypePrefsMap.keySet()) {
259            setAutoSelectionSubtypesEnabled(imiId, isNoSubtypesExplicitlySelected(imiId));
260        }
261        updateImplicitlyEnabledSubtypes(null /* targetImiId */  /* check */);
262    }
263}
264