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