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