Home | History | Annotate | Download | only in inputmethod
      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.support.v14.preference.PreferenceFragment;
     29 import android.support.v7.preference.Preference;
     30 import android.support.v7.preference.PreferenceScreen;
     31 import android.support.v7.preference.TwoStatePreference;
     32 import android.text.TextUtils;
     33 import android.util.Log;
     34 import android.view.inputmethod.InputMethodInfo;
     35 import android.view.inputmethod.InputMethodSubtype;
     36 
     37 import com.android.internal.app.LocaleHelper;
     38 import com.android.internal.inputmethod.InputMethodUtils;
     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 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