• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 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.settings.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.support.v7.preference.Preference;
29 import android.support.v7.preference.PreferenceScreen;
30 import android.support.v7.preference.TwoStatePreference;
31 import android.text.TextUtils;
32 import android.util.Log;
33 import android.view.inputmethod.InputMethodInfo;
34 import android.view.inputmethod.InputMethodSubtype;
35 
36 import com.android.internal.app.LocaleHelper;
37 import com.android.internal.inputmethod.InputMethodUtils;
38 import com.android.settings.SettingsPreferenceFragment;
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 class InputMethodAndSubtypeUtil {
48 
49     private static final boolean DEBUG = false;
50     static final String TAG = "InputMethdAndSubtypeUtil";
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
buildInputMethodsAndSubtypesString( final HashMap<String, HashSet<String>> imeToSubtypesMap)64     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 
buildInputMethodsString(final HashSet<String> imiList)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 
getInputMethodSubtypeSelected(ContentResolver resolver)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 
isInputMethodSubtypeSelected(ContentResolver resolver)100     private static boolean isInputMethodSubtypeSelected(ContentResolver resolver) {
101         return getInputMethodSubtypeSelected(resolver) != NOT_A_SUBTYPE_ID;
102     }
103 
putSelectedInputMethodSubtype(ContentResolver resolver, int hashCode)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.
getEnabledInputMethodsAndSubtypeList( ContentResolver resolver)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 
parseInputMethodsAndSubtypesString( final String inputMethodsAndSubtypesString)119     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 
enableInputMethodSubtypesOf(final ContentResolver resolver, final String imiId, final HashSet<String> enabledSubtypeIdSet)142     static void enableInputMethodSubtypesOf(final ContentResolver resolver, final String imiId,
143             final HashSet<String> enabledSubtypeIdSet) {
144         final HashMap<String, HashSet<String>> enabledImeAndSubtypeIdsMap =
145                 getEnabledInputMethodsAndSubtypeList(resolver);
146         enabledImeAndSubtypeIdsMap.put(imiId, enabledSubtypeIdSet);
147         final String enabledImesAndSubtypesString = buildInputMethodsAndSubtypesString(
148                 enabledImeAndSubtypeIdsMap);
149         Settings.Secure.putString(resolver,
150                 Settings.Secure.ENABLED_INPUT_METHODS, enabledImesAndSubtypesString);
151     }
152 
getDisabledSystemIMEs(ContentResolver resolver)153     private static HashSet<String> getDisabledSystemIMEs(ContentResolver resolver) {
154         HashSet<String> set = new HashSet<>();
155         String disabledIMEsStr = Settings.Secure.getString(
156                 resolver, Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS);
157         if (TextUtils.isEmpty(disabledIMEsStr)) {
158             return set;
159         }
160         sStringInputMethodSplitter.setString(disabledIMEsStr);
161         while(sStringInputMethodSplitter.hasNext()) {
162             set.add(sStringInputMethodSplitter.next());
163         }
164         return set;
165     }
166 
saveInputMethodSubtypeList(SettingsPreferenceFragment context, ContentResolver resolver, List<InputMethodInfo> inputMethodInfos, boolean hasHardKeyboard)167     static void saveInputMethodSubtypeList(SettingsPreferenceFragment context,
168             ContentResolver resolver, List<InputMethodInfo> inputMethodInfos,
169             boolean hasHardKeyboard) {
170         String currentInputMethodId = Settings.Secure.getString(resolver,
171                 Settings.Secure.DEFAULT_INPUT_METHOD);
172         final int selectedInputMethodSubtype = getInputMethodSubtypeSelected(resolver);
173         final HashMap<String, HashSet<String>> enabledIMEsAndSubtypesMap =
174                 getEnabledInputMethodsAndSubtypeList(resolver);
175         final HashSet<String> disabledSystemIMEs = getDisabledSystemIMEs(resolver);
176 
177         boolean needsToResetSelectedSubtype = false;
178         for (final InputMethodInfo imi : inputMethodInfos) {
179             final String imiId = imi.getId();
180             final Preference pref = context.findPreference(imiId);
181             if (pref == null) {
182                 continue;
183             }
184             // In the choose input method screen or in the subtype enabler screen,
185             // <code>pref</code> is an instance of TwoStatePreference.
186             final boolean isImeChecked = (pref instanceof TwoStatePreference) ?
187                     ((TwoStatePreference) pref).isChecked()
188                     : enabledIMEsAndSubtypesMap.containsKey(imiId);
189             final boolean isCurrentInputMethod = imiId.equals(currentInputMethodId);
190             final boolean systemIme = InputMethodUtils.isSystemIme(imi);
191             if ((!hasHardKeyboard && InputMethodSettingValuesWrapper.getInstance(
192                     context.getActivity()).isAlwaysCheckedIme(imi, context.getActivity()))
193                     || isImeChecked) {
194                 if (!enabledIMEsAndSubtypesMap.containsKey(imiId)) {
195                     // imiId has just been enabled
196                     enabledIMEsAndSubtypesMap.put(imiId, new HashSet<String>());
197                 }
198                 final HashSet<String> subtypesSet = enabledIMEsAndSubtypesMap.get(imiId);
199 
200                 boolean subtypePrefFound = false;
201                 final int subtypeCount = imi.getSubtypeCount();
202                 for (int i = 0; i < subtypeCount; ++i) {
203                     final InputMethodSubtype subtype = imi.getSubtypeAt(i);
204                     final String subtypeHashCodeStr = String.valueOf(subtype.hashCode());
205                     final TwoStatePreference subtypePref = (TwoStatePreference) context
206                             .findPreference(imiId + subtypeHashCodeStr);
207                     // In the Configure input method screen which does not have subtype preferences.
208                     if (subtypePref == null) {
209                         continue;
210                     }
211                     if (!subtypePrefFound) {
212                         // Once subtype preference is found, subtypeSet needs to be cleared.
213                         // Because of system change, hashCode value could have been changed.
214                         subtypesSet.clear();
215                         // If selected subtype preference is disabled, needs to reset.
216                         needsToResetSelectedSubtype = true;
217                         subtypePrefFound = true;
218                     }
219                     // Checking <code>subtypePref.isEnabled()</code> is insufficient to determine
220                     // whether the user manually enabled this subtype or not.  Implicitly-enabled
221                     // subtypes are also checked just as an indicator to users.  We also need to
222                     // check <code>subtypePref.isEnabled()</code> so that only manually enabled
223                     // subtypes can be saved here.
224                     if (subtypePref.isEnabled() && subtypePref.isChecked()) {
225                         subtypesSet.add(subtypeHashCodeStr);
226                         if (isCurrentInputMethod) {
227                             if (selectedInputMethodSubtype == subtype.hashCode()) {
228                                 // Selected subtype is still enabled, there is no need to reset
229                                 // selected subtype.
230                                 needsToResetSelectedSubtype = false;
231                             }
232                         }
233                     } else {
234                         subtypesSet.remove(subtypeHashCodeStr);
235                     }
236                 }
237             } else {
238                 enabledIMEsAndSubtypesMap.remove(imiId);
239                 if (isCurrentInputMethod) {
240                     // We are processing the current input method, but found that it's not enabled.
241                     // This means that the current input method has been uninstalled.
242                     // If currentInputMethod is already uninstalled, InputMethodManagerService will
243                     // find the applicable IME from the history and the system locale.
244                     if (DEBUG) {
245                         Log.d(TAG, "Current IME was uninstalled or disabled.");
246                     }
247                     currentInputMethodId = null;
248                 }
249             }
250             // If it's a disabled system ime, add it to the disabled list so that it
251             // doesn't get enabled automatically on any changes to the package list
252             if (systemIme && hasHardKeyboard) {
253                 if (disabledSystemIMEs.contains(imiId)) {
254                     if (isImeChecked) {
255                         disabledSystemIMEs.remove(imiId);
256                     }
257                 } else {
258                     if (!isImeChecked) {
259                         disabledSystemIMEs.add(imiId);
260                     }
261                 }
262             }
263         }
264 
265         final String enabledIMEsAndSubtypesString = buildInputMethodsAndSubtypesString(
266                 enabledIMEsAndSubtypesMap);
267         final String disabledSystemIMEsString = buildInputMethodsString(disabledSystemIMEs);
268         if (DEBUG) {
269             Log.d(TAG, "--- Save enabled inputmethod settings. :" + enabledIMEsAndSubtypesString);
270             Log.d(TAG, "--- Save disabled system inputmethod settings. :"
271                     + disabledSystemIMEsString);
272             Log.d(TAG, "--- Save default inputmethod settings. :" + currentInputMethodId);
273             Log.d(TAG, "--- Needs to reset the selected subtype :" + needsToResetSelectedSubtype);
274             Log.d(TAG, "--- Subtype is selected :" + isInputMethodSubtypeSelected(resolver));
275         }
276 
277         // Redefines SelectedSubtype when all subtypes are unchecked or there is no subtype
278         // selected. And if the selected subtype of the current input method was disabled,
279         // We should reset the selected input method's subtype.
280         if (needsToResetSelectedSubtype || !isInputMethodSubtypeSelected(resolver)) {
281             if (DEBUG) {
282                 Log.d(TAG, "--- Reset inputmethod subtype because it's not defined.");
283             }
284             putSelectedInputMethodSubtype(resolver, NOT_A_SUBTYPE_ID);
285         }
286 
287         Settings.Secure.putString(resolver,
288                 Settings.Secure.ENABLED_INPUT_METHODS, enabledIMEsAndSubtypesString);
289         if (disabledSystemIMEsString.length() > 0) {
290             Settings.Secure.putString(resolver, Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS,
291                     disabledSystemIMEsString);
292         }
293         // If the current input method is unset, InputMethodManagerService will find the applicable
294         // IME from the history and the system locale.
295         Settings.Secure.putString(resolver, Settings.Secure.DEFAULT_INPUT_METHOD,
296                 currentInputMethodId != null ? currentInputMethodId : "");
297     }
298 
loadInputMethodSubtypeList(final SettingsPreferenceFragment context, final ContentResolver resolver, final List<InputMethodInfo> inputMethodInfos, final Map<String, List<Preference>> inputMethodPrefsMap)299     static void loadInputMethodSubtypeList(final SettingsPreferenceFragment context,
300             final ContentResolver resolver, final List<InputMethodInfo> inputMethodInfos,
301             final Map<String, List<Preference>> inputMethodPrefsMap) {
302         final HashMap<String, HashSet<String>> enabledSubtypes =
303                 getEnabledInputMethodsAndSubtypeList(resolver);
304 
305         for (final InputMethodInfo imi : inputMethodInfos) {
306             final String imiId = imi.getId();
307             final Preference pref = context.findPreference(imiId);
308             if (pref instanceof TwoStatePreference) {
309                 final TwoStatePreference subtypePref = (TwoStatePreference) pref;
310                 final boolean isEnabled = enabledSubtypes.containsKey(imiId);
311                 subtypePref.setChecked(isEnabled);
312                 if (inputMethodPrefsMap != null) {
313                     for (final Preference childPref: inputMethodPrefsMap.get(imiId)) {
314                         childPref.setEnabled(isEnabled);
315                     }
316                 }
317                 setSubtypesPreferenceEnabled(context, inputMethodInfos, imiId, isEnabled);
318             }
319         }
320         updateSubtypesPreferenceChecked(context, inputMethodInfos, enabledSubtypes);
321     }
322 
setSubtypesPreferenceEnabled(final SettingsPreferenceFragment context, final List<InputMethodInfo> inputMethodProperties, final String id, final boolean enabled)323     static void setSubtypesPreferenceEnabled(final SettingsPreferenceFragment context,
324             final List<InputMethodInfo> inputMethodProperties, final String id,
325             final boolean enabled) {
326         final PreferenceScreen preferenceScreen = context.getPreferenceScreen();
327         for (final InputMethodInfo imi : inputMethodProperties) {
328             if (id.equals(imi.getId())) {
329                 final int subtypeCount = imi.getSubtypeCount();
330                 for (int i = 0; i < subtypeCount; ++i) {
331                     final InputMethodSubtype subtype = imi.getSubtypeAt(i);
332                     final TwoStatePreference pref = (TwoStatePreference) preferenceScreen
333                             .findPreference(id + subtype.hashCode());
334                     if (pref != null) {
335                         pref.setEnabled(enabled);
336                     }
337                 }
338             }
339         }
340     }
341 
updateSubtypesPreferenceChecked(final SettingsPreferenceFragment context, final List<InputMethodInfo> inputMethodProperties, final HashMap<String, HashSet<String>> enabledSubtypes)342     private static void updateSubtypesPreferenceChecked(final SettingsPreferenceFragment context,
343             final List<InputMethodInfo> inputMethodProperties,
344             final HashMap<String, HashSet<String>> enabledSubtypes) {
345         final PreferenceScreen preferenceScreen = context.getPreferenceScreen();
346         for (final InputMethodInfo imi : inputMethodProperties) {
347             final String id = imi.getId();
348             if (!enabledSubtypes.containsKey(id)) {
349                 // There is no need to enable/disable subtypes of disabled IMEs.
350                 continue;
351             }
352             final HashSet<String> enabledSubtypesSet = enabledSubtypes.get(id);
353             final int subtypeCount = imi.getSubtypeCount();
354             for (int i = 0; i < subtypeCount; ++i) {
355                 final InputMethodSubtype subtype = imi.getSubtypeAt(i);
356                 final String hashCode = String.valueOf(subtype.hashCode());
357                 if (DEBUG) {
358                     Log.d(TAG, "--- Set checked state: " + "id" + ", " + hashCode + ", "
359                             + enabledSubtypesSet.contains(hashCode));
360                 }
361                 final TwoStatePreference pref = (TwoStatePreference) preferenceScreen
362                         .findPreference(id + hashCode);
363                 if (pref != null) {
364                     pref.setChecked(enabledSubtypesSet.contains(hashCode));
365                 }
366             }
367         }
368     }
369 
removeUnnecessaryNonPersistentPreference(final Preference pref)370     static void removeUnnecessaryNonPersistentPreference(final Preference pref) {
371         final String key = pref.getKey();
372         if (pref.isPersistent() || key == null) {
373             return;
374         }
375         final SharedPreferences prefs = pref.getSharedPreferences();
376         if (prefs != null && prefs.contains(key)) {
377             prefs.edit().remove(key).apply();
378         }
379     }
380 
381     @NonNull
getSubtypeLocaleNameAsSentence(@ullable InputMethodSubtype subtype, @NonNull final Context context, @NonNull final InputMethodInfo inputMethodInfo)382     static String getSubtypeLocaleNameAsSentence(@Nullable InputMethodSubtype subtype,
383             @NonNull final Context context, @NonNull final InputMethodInfo inputMethodInfo) {
384         if (subtype == null) {
385             return "";
386         }
387         final Locale locale = getDisplayLocale(context);
388         final CharSequence subtypeName = subtype.getDisplayName(context,
389                 inputMethodInfo.getPackageName(), inputMethodInfo.getServiceInfo()
390                         .applicationInfo);
391         return LocaleHelper.toSentenceCase(subtypeName.toString(), locale);
392     }
393 
394     @NonNull
getSubtypeLocaleNameListAsSentence( @onNull final List<InputMethodSubtype> subtypes, @NonNull final Context context, @NonNull final InputMethodInfo inputMethodInfo)395     static String getSubtypeLocaleNameListAsSentence(
396             @NonNull final List<InputMethodSubtype> subtypes, @NonNull final Context context,
397             @NonNull final InputMethodInfo inputMethodInfo) {
398         if (subtypes.isEmpty()) {
399             return "";
400         }
401         final Locale locale = getDisplayLocale(context);
402         final int subtypeCount = subtypes.size();
403         final CharSequence[] subtypeNames = new CharSequence[subtypeCount];
404         for (int i = 0; i < subtypeCount; i++) {
405             subtypeNames[i] = subtypes.get(i).getDisplayName(context,
406                     inputMethodInfo.getPackageName(), inputMethodInfo.getServiceInfo()
407                             .applicationInfo);
408         }
409         return LocaleHelper.toSentenceCase(
410                 ListFormatter.getInstance(locale).format(subtypeNames), locale);
411     }
412 
413     @NonNull
getDisplayLocale(@ullable final Context context)414     private static Locale getDisplayLocale(@Nullable final Context context) {
415         if (context == null) {
416             return Locale.getDefault();
417         }
418         if (context.getResources() == null) {
419             return Locale.getDefault();
420         }
421         final Configuration configuration = context.getResources().getConfiguration();
422         if (configuration == null) {
423             return Locale.getDefault();
424         }
425         final Locale configurationLocale = configuration.getLocales().get(0);
426         if (configurationLocale == null) {
427             return Locale.getDefault();
428         }
429         return configurationLocale;
430     }
431 }
432