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.annotation.NonNull;
20import android.annotation.Nullable;
21import android.content.ContentResolver;
22import android.content.Context;
23import android.content.SharedPreferences;
24import android.content.res.Configuration;
25import android.icu.text.ListFormatter;
26import android.provider.Settings;
27import android.provider.Settings.SettingNotFoundException;
28import android.support.v14.preference.PreferenceFragment;
29import android.support.v7.preference.Preference;
30import android.support.v7.preference.PreferenceScreen;
31import android.support.v7.preference.TwoStatePreference;
32import android.text.TextUtils;
33import android.util.Log;
34import android.view.inputmethod.InputMethodInfo;
35import android.view.inputmethod.InputMethodSubtype;
36
37import com.android.internal.app.LocaleHelper;
38import com.android.internal.inputmethod.InputMethodUtils;
39
40import java.util.HashMap;
41import java.util.HashSet;
42import java.util.List;
43import java.util.Locale;
44import java.util.Map;
45
46// TODO: Consolidate this with {@link InputMethodSettingValuesWrapper}.
47public class InputMethodAndSubtypeUtil {
48
49    private static final boolean DEBUG = false;
50    private static final String TAG = "InputMethdAndSubtypeUtl";
51
52    private static final char INPUT_METHOD_SEPARATER = ':';
53    private static final char INPUT_METHOD_SUBTYPE_SEPARATER = ';';
54    private static final int NOT_A_SUBTYPE_ID = -1;
55
56    private static final TextUtils.SimpleStringSplitter sStringInputMethodSplitter
57            = new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATER);
58
59    private static final TextUtils.SimpleStringSplitter sStringInputMethodSubtypeSplitter
60            = new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATER);
61
62    // InputMethods and subtypes are saved in the settings as follows:
63    // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1
64    private static String buildInputMethodsAndSubtypesString(
65            final HashMap<String, HashSet<String>> imeToSubtypesMap) {
66        final StringBuilder builder = new StringBuilder();
67        for (final String imi : imeToSubtypesMap.keySet()) {
68            if (builder.length() > 0) {
69                builder.append(INPUT_METHOD_SEPARATER);
70            }
71            final HashSet<String> subtypeIdSet = imeToSubtypesMap.get(imi);
72            builder.append(imi);
73            for (final String subtypeId : subtypeIdSet) {
74                builder.append(INPUT_METHOD_SUBTYPE_SEPARATER).append(subtypeId);
75            }
76        }
77        return builder.toString();
78    }
79
80    private static String buildInputMethodsString(final HashSet<String> imiList) {
81        final StringBuilder builder = new StringBuilder();
82        for (final String imi : imiList) {
83            if (builder.length() > 0) {
84                builder.append(INPUT_METHOD_SEPARATER);
85            }
86            builder.append(imi);
87        }
88        return builder.toString();
89    }
90
91    private static int getInputMethodSubtypeSelected(ContentResolver resolver) {
92        try {
93            return Settings.Secure.getInt(resolver,
94                    Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE);
95        } catch (SettingNotFoundException e) {
96            return NOT_A_SUBTYPE_ID;
97        }
98    }
99
100    private static boolean isInputMethodSubtypeSelected(ContentResolver resolver) {
101        return getInputMethodSubtypeSelected(resolver) != NOT_A_SUBTYPE_ID;
102    }
103
104    private static void putSelectedInputMethodSubtype(ContentResolver resolver, int hashCode) {
105        Settings.Secure.putInt(resolver, Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, hashCode);
106    }
107
108    // Needs to modify InputMethodManageService if you want to change the format of saved string.
109    private static HashMap<String, HashSet<String>> getEnabledInputMethodsAndSubtypeList(
110            ContentResolver resolver) {
111        final String enabledInputMethodsStr = Settings.Secure.getString(
112                resolver, Settings.Secure.ENABLED_INPUT_METHODS);
113        if (DEBUG) {
114            Log.d(TAG, "--- Load enabled input methods: " + enabledInputMethodsStr);
115        }
116        return parseInputMethodsAndSubtypesString(enabledInputMethodsStr);
117    }
118
119    private static HashMap<String, HashSet<String>> parseInputMethodsAndSubtypesString(
120            final String inputMethodsAndSubtypesString) {
121        final HashMap<String, HashSet<String>> subtypesMap = new HashMap<>();
122        if (TextUtils.isEmpty(inputMethodsAndSubtypesString)) {
123            return subtypesMap;
124        }
125        sStringInputMethodSplitter.setString(inputMethodsAndSubtypesString);
126        while (sStringInputMethodSplitter.hasNext()) {
127            final String nextImsStr = sStringInputMethodSplitter.next();
128            sStringInputMethodSubtypeSplitter.setString(nextImsStr);
129            if (sStringInputMethodSubtypeSplitter.hasNext()) {
130                final HashSet<String> subtypeIdSet = new HashSet<>();
131                // The first element is {@link InputMethodInfoId}.
132                final String imiId = sStringInputMethodSubtypeSplitter.next();
133                while (sStringInputMethodSubtypeSplitter.hasNext()) {
134                    subtypeIdSet.add(sStringInputMethodSubtypeSplitter.next());
135                }
136                subtypesMap.put(imiId, subtypeIdSet);
137            }
138        }
139        return subtypesMap;
140    }
141
142    private static HashSet<String> getDisabledSystemIMEs(ContentResolver resolver) {
143        HashSet<String> set = new HashSet<>();
144        String disabledIMEsStr = Settings.Secure.getString(
145                resolver, Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS);
146        if (TextUtils.isEmpty(disabledIMEsStr)) {
147            return set;
148        }
149        sStringInputMethodSplitter.setString(disabledIMEsStr);
150        while(sStringInputMethodSplitter.hasNext()) {
151            set.add(sStringInputMethodSplitter.next());
152        }
153        return set;
154    }
155
156    public static void saveInputMethodSubtypeList(PreferenceFragment context,
157            ContentResolver resolver, List<InputMethodInfo> inputMethodInfos,
158            boolean hasHardKeyboard) {
159        String currentInputMethodId = Settings.Secure.getString(resolver,
160                Settings.Secure.DEFAULT_INPUT_METHOD);
161        final int selectedInputMethodSubtype = getInputMethodSubtypeSelected(resolver);
162        final HashMap<String, HashSet<String>> enabledIMEsAndSubtypesMap =
163                getEnabledInputMethodsAndSubtypeList(resolver);
164        final HashSet<String> disabledSystemIMEs = getDisabledSystemIMEs(resolver);
165
166        boolean needsToResetSelectedSubtype = false;
167        for (final InputMethodInfo imi : inputMethodInfos) {
168            final String imiId = imi.getId();
169            final Preference pref = context.findPreference(imiId);
170            if (pref == null) {
171                continue;
172            }
173            // In the choose input method screen or in the subtype enabler screen,
174            // <code>pref</code> is an instance of TwoStatePreference.
175            final boolean isImeChecked = (pref instanceof TwoStatePreference) ?
176                    ((TwoStatePreference) pref).isChecked()
177                    : enabledIMEsAndSubtypesMap.containsKey(imiId);
178            final boolean isCurrentInputMethod = imiId.equals(currentInputMethodId);
179            final boolean systemIme = InputMethodUtils.isSystemIme(imi);
180            if ((!hasHardKeyboard && InputMethodSettingValuesWrapper.getInstance(
181                    context.getActivity()).isAlwaysCheckedIme(imi, context.getActivity()))
182                    || isImeChecked) {
183                if (!enabledIMEsAndSubtypesMap.containsKey(imiId)) {
184                    // imiId has just been enabled
185                    enabledIMEsAndSubtypesMap.put(imiId, new HashSet<>());
186                }
187                final HashSet<String> subtypesSet = enabledIMEsAndSubtypesMap.get(imiId);
188
189                boolean subtypePrefFound = false;
190                final int subtypeCount = imi.getSubtypeCount();
191                for (int i = 0; i < subtypeCount; ++i) {
192                    final InputMethodSubtype subtype = imi.getSubtypeAt(i);
193                    final String subtypeHashCodeStr = String.valueOf(subtype.hashCode());
194                    final TwoStatePreference subtypePref = (TwoStatePreference) context
195                            .findPreference(imiId + subtypeHashCodeStr);
196                    // In the Configure input method screen which does not have subtype preferences.
197                    if (subtypePref == null) {
198                        continue;
199                    }
200                    if (!subtypePrefFound) {
201                        // Once subtype preference is found, subtypeSet needs to be cleared.
202                        // Because of system change, hashCode value could have been changed.
203                        subtypesSet.clear();
204                        // If selected subtype preference is disabled, needs to reset.
205                        needsToResetSelectedSubtype = true;
206                        subtypePrefFound = true;
207                    }
208                    // Checking <code>subtypePref.isEnabled()</code> is insufficient to determine
209                    // whether the user manually enabled this subtype or not.  Implicitly-enabled
210                    // subtypes are also checked just as an indicator to users.  We also need to
211                    // check <code>subtypePref.isEnabled()</code> so that only manually enabled
212                    // subtypes can be saved here.
213                    if (subtypePref.isEnabled() && subtypePref.isChecked()) {
214                        subtypesSet.add(subtypeHashCodeStr);
215                        if (isCurrentInputMethod) {
216                            if (selectedInputMethodSubtype == subtype.hashCode()) {
217                                // Selected subtype is still enabled, there is no need to reset
218                                // selected subtype.
219                                needsToResetSelectedSubtype = false;
220                            }
221                        }
222                    } else {
223                        subtypesSet.remove(subtypeHashCodeStr);
224                    }
225                }
226            } else {
227                enabledIMEsAndSubtypesMap.remove(imiId);
228                if (isCurrentInputMethod) {
229                    // We are processing the current input method, but found that it's not enabled.
230                    // This means that the current input method has been uninstalled.
231                    // If currentInputMethod is already uninstalled, InputMethodManagerService will
232                    // find the applicable IME from the history and the system locale.
233                    if (DEBUG) {
234                        Log.d(TAG, "Current IME was uninstalled or disabled.");
235                    }
236                    currentInputMethodId = null;
237                }
238            }
239            // If it's a disabled system ime, add it to the disabled list so that it
240            // doesn't get enabled automatically on any changes to the package list
241            if (systemIme && hasHardKeyboard) {
242                if (disabledSystemIMEs.contains(imiId)) {
243                    if (isImeChecked) {
244                        disabledSystemIMEs.remove(imiId);
245                    }
246                } else {
247                    if (!isImeChecked) {
248                        disabledSystemIMEs.add(imiId);
249                    }
250                }
251            }
252        }
253
254        final String enabledIMEsAndSubtypesString = buildInputMethodsAndSubtypesString(
255                enabledIMEsAndSubtypesMap);
256        final String disabledSystemIMEsString = buildInputMethodsString(disabledSystemIMEs);
257        if (DEBUG) {
258            Log.d(TAG, "--- Save enabled inputmethod settings. :" + enabledIMEsAndSubtypesString);
259            Log.d(TAG, "--- Save disabled system inputmethod settings. :"
260                    + disabledSystemIMEsString);
261            Log.d(TAG, "--- Save default inputmethod settings. :" + currentInputMethodId);
262            Log.d(TAG, "--- Needs to reset the selected subtype :" + needsToResetSelectedSubtype);
263            Log.d(TAG, "--- Subtype is selected :" + isInputMethodSubtypeSelected(resolver));
264        }
265
266        // Redefines SelectedSubtype when all subtypes are unchecked or there is no subtype
267        // selected. And if the selected subtype of the current input method was disabled,
268        // We should reset the selected input method's subtype.
269        if (needsToResetSelectedSubtype || !isInputMethodSubtypeSelected(resolver)) {
270            if (DEBUG) {
271                Log.d(TAG, "--- Reset inputmethod subtype because it's not defined.");
272            }
273            putSelectedInputMethodSubtype(resolver, NOT_A_SUBTYPE_ID);
274        }
275
276        Settings.Secure.putString(resolver,
277                Settings.Secure.ENABLED_INPUT_METHODS, enabledIMEsAndSubtypesString);
278        if (disabledSystemIMEsString.length() > 0) {
279            Settings.Secure.putString(resolver, Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS,
280                    disabledSystemIMEsString);
281        }
282        // If the current input method is unset, InputMethodManagerService will find the applicable
283        // IME from the history and the system locale.
284        Settings.Secure.putString(resolver, Settings.Secure.DEFAULT_INPUT_METHOD,
285                currentInputMethodId != null ? currentInputMethodId : "");
286    }
287
288    public static void loadInputMethodSubtypeList(final PreferenceFragment context,
289            final ContentResolver resolver, final List<InputMethodInfo> inputMethodInfos,
290            final Map<String, List<Preference>> inputMethodPrefsMap) {
291        final HashMap<String, HashSet<String>> enabledSubtypes =
292                getEnabledInputMethodsAndSubtypeList(resolver);
293
294        for (final InputMethodInfo imi : inputMethodInfos) {
295            final String imiId = imi.getId();
296            final Preference pref = context.findPreference(imiId);
297            if (pref instanceof TwoStatePreference) {
298                final TwoStatePreference subtypePref = (TwoStatePreference) pref;
299                final boolean isEnabled = enabledSubtypes.containsKey(imiId);
300                subtypePref.setChecked(isEnabled);
301                if (inputMethodPrefsMap != null) {
302                    for (final Preference childPref: inputMethodPrefsMap.get(imiId)) {
303                        childPref.setEnabled(isEnabled);
304                    }
305                }
306                setSubtypesPreferenceEnabled(context, inputMethodInfos, imiId, isEnabled);
307            }
308        }
309        updateSubtypesPreferenceChecked(context, inputMethodInfos, enabledSubtypes);
310    }
311
312    private static void setSubtypesPreferenceEnabled(final PreferenceFragment context,
313            final List<InputMethodInfo> inputMethodProperties, final String id,
314            final boolean enabled) {
315        final PreferenceScreen preferenceScreen = context.getPreferenceScreen();
316        for (final InputMethodInfo imi : inputMethodProperties) {
317            if (id.equals(imi.getId())) {
318                final int subtypeCount = imi.getSubtypeCount();
319                for (int i = 0; i < subtypeCount; ++i) {
320                    final InputMethodSubtype subtype = imi.getSubtypeAt(i);
321                    final TwoStatePreference pref = (TwoStatePreference) preferenceScreen
322                            .findPreference(id + subtype.hashCode());
323                    if (pref != null) {
324                        pref.setEnabled(enabled);
325                    }
326                }
327            }
328        }
329    }
330
331    private static void updateSubtypesPreferenceChecked(final PreferenceFragment context,
332            final List<InputMethodInfo> inputMethodProperties,
333            final HashMap<String, HashSet<String>> enabledSubtypes) {
334        final PreferenceScreen preferenceScreen = context.getPreferenceScreen();
335        for (final InputMethodInfo imi : inputMethodProperties) {
336            final String id = imi.getId();
337            if (!enabledSubtypes.containsKey(id)) {
338                // There is no need to enable/disable subtypes of disabled IMEs.
339                continue;
340            }
341            final HashSet<String> enabledSubtypesSet = enabledSubtypes.get(id);
342            final int subtypeCount = imi.getSubtypeCount();
343            for (int i = 0; i < subtypeCount; ++i) {
344                final InputMethodSubtype subtype = imi.getSubtypeAt(i);
345                final String hashCode = String.valueOf(subtype.hashCode());
346                if (DEBUG) {
347                    Log.d(TAG, "--- Set checked state: " + "id" + ", " + hashCode + ", "
348                            + enabledSubtypesSet.contains(hashCode));
349                }
350                final TwoStatePreference pref = (TwoStatePreference) preferenceScreen
351                        .findPreference(id + hashCode);
352                if (pref != null) {
353                    pref.setChecked(enabledSubtypesSet.contains(hashCode));
354                }
355            }
356        }
357    }
358
359    public static void removeUnnecessaryNonPersistentPreference(final Preference pref) {
360        final String key = pref.getKey();
361        if (pref.isPersistent() || key == null) {
362            return;
363        }
364        final SharedPreferences prefs = pref.getSharedPreferences();
365        if (prefs != null && prefs.contains(key)) {
366            prefs.edit().remove(key).apply();
367        }
368    }
369
370    @NonNull
371    public static String getSubtypeLocaleNameAsSentence(@Nullable InputMethodSubtype subtype,
372            @NonNull final Context context, @NonNull final InputMethodInfo inputMethodInfo) {
373        if (subtype == null) {
374            return "";
375        }
376        final Locale locale = getDisplayLocale(context);
377        final CharSequence subtypeName = subtype.getDisplayName(context,
378                inputMethodInfo.getPackageName(), inputMethodInfo.getServiceInfo()
379                        .applicationInfo);
380        return LocaleHelper.toSentenceCase(subtypeName.toString(), locale);
381    }
382
383    @NonNull
384    public static String getSubtypeLocaleNameListAsSentence(
385            @NonNull final List<InputMethodSubtype> subtypes, @NonNull final Context context,
386            @NonNull final InputMethodInfo inputMethodInfo) {
387        if (subtypes.isEmpty()) {
388            return "";
389        }
390        final Locale locale = getDisplayLocale(context);
391        final int subtypeCount = subtypes.size();
392        final CharSequence[] subtypeNames = new CharSequence[subtypeCount];
393        for (int i = 0; i < subtypeCount; i++) {
394            subtypeNames[i] = subtypes.get(i).getDisplayName(context,
395                    inputMethodInfo.getPackageName(), inputMethodInfo.getServiceInfo()
396                            .applicationInfo);
397        }
398        return LocaleHelper.toSentenceCase(
399                ListFormatter.getInstance(locale).format((Object[]) subtypeNames), locale);
400    }
401
402    @NonNull
403    private static Locale getDisplayLocale(@Nullable final Context context) {
404        if (context == null) {
405            return Locale.getDefault();
406        }
407        if (context.getResources() == null) {
408            return Locale.getDefault();
409        }
410        final Configuration configuration = context.getResources().getConfiguration();
411        if (configuration == null) {
412            return Locale.getDefault();
413        }
414        final Locale configurationLocale = configuration.getLocales().get(0);
415        if (configurationLocale == null) {
416            return Locale.getDefault();
417        }
418        return configurationLocale;
419    }
420}
421