Home | History | Annotate | Download | only in inputmethod
      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
     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 
     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     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     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 
    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 
    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 
    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 
    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 
    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 
    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
    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
    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
    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