Home | History | Annotate | Download | only in latin
      1 /*
      2  * Copyright (C) 2012 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.inputmethod.latin;
     18 
     19 import static com.android.inputmethod.latin.Constants.Subtype.KEYBOARD_MODE;
     20 
     21 import android.content.Context;
     22 import android.content.SharedPreferences;
     23 import android.os.Build;
     24 import android.os.IBinder;
     25 import android.preference.PreferenceManager;
     26 import android.util.Log;
     27 import android.view.inputmethod.InputMethodInfo;
     28 import android.view.inputmethod.InputMethodManager;
     29 import android.view.inputmethod.InputMethodSubtype;
     30 
     31 import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
     32 import com.android.inputmethod.latin.settings.Settings;
     33 import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils;
     34 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
     35 
     36 import java.util.Collections;
     37 import java.util.HashMap;
     38 import java.util.List;
     39 
     40 /**
     41  * Enrichment class for InputMethodManager to simplify interaction and add functionality.
     42  */
     43 public final class RichInputMethodManager {
     44     private static final String TAG = RichInputMethodManager.class.getSimpleName();
     45 
     46     private RichInputMethodManager() {
     47         // This utility class is not publicly instantiable.
     48     }
     49 
     50     private static final RichInputMethodManager sInstance = new RichInputMethodManager();
     51 
     52     private InputMethodManagerCompatWrapper mImmWrapper;
     53     private InputMethodInfoCache mInputMethodInfoCache;
     54     final HashMap<InputMethodInfo, List<InputMethodSubtype>>
     55             mSubtypeListCacheWithImplicitlySelectedSubtypes = new HashMap<>();
     56     final HashMap<InputMethodInfo, List<InputMethodSubtype>>
     57             mSubtypeListCacheWithoutImplicitlySelectedSubtypes = new HashMap<>();
     58 
     59     private static final int INDEX_NOT_FOUND = -1;
     60 
     61     public static RichInputMethodManager getInstance() {
     62         sInstance.checkInitialized();
     63         return sInstance;
     64     }
     65 
     66     public static void init(final Context context) {
     67         sInstance.initInternal(context);
     68     }
     69 
     70     private boolean isInitialized() {
     71         return mImmWrapper != null;
     72     }
     73 
     74     private void checkInitialized() {
     75         if (!isInitialized()) {
     76             throw new RuntimeException(TAG + " is used before initialization");
     77         }
     78     }
     79 
     80     private void initInternal(final Context context) {
     81         if (isInitialized()) {
     82             return;
     83         }
     84         mImmWrapper = new InputMethodManagerCompatWrapper(context);
     85         mInputMethodInfoCache = new InputMethodInfoCache(
     86                 mImmWrapper.mImm, context.getPackageName());
     87 
     88         // Initialize additional subtypes.
     89         SubtypeLocaleUtils.init(context);
     90         final InputMethodSubtype[] additionalSubtypes = getAdditionalSubtypes(context);
     91         setAdditionalInputMethodSubtypes(additionalSubtypes);
     92     }
     93 
     94     public InputMethodSubtype[] getAdditionalSubtypes(final Context context) {
     95         SubtypeLocaleUtils.init(context);
     96         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
     97         final String prefAdditionalSubtypes = Settings.readPrefAdditionalSubtypes(
     98                 prefs, context.getResources());
     99         return AdditionalSubtypeUtils.createAdditionalSubtypesArray(prefAdditionalSubtypes);
    100     }
    101 
    102     public InputMethodManager getInputMethodManager() {
    103         checkInitialized();
    104         return mImmWrapper.mImm;
    105     }
    106 
    107     public List<InputMethodSubtype> getMyEnabledInputMethodSubtypeList(
    108             boolean allowsImplicitlySelectedSubtypes) {
    109         return getEnabledInputMethodSubtypeList(
    110                 getInputMethodInfoOfThisIme(), allowsImplicitlySelectedSubtypes);
    111     }
    112 
    113     public boolean switchToNextInputMethod(final IBinder token, final boolean onlyCurrentIme) {
    114         if (mImmWrapper.switchToNextInputMethod(token, onlyCurrentIme)) {
    115             return true;
    116         }
    117         // Was not able to call {@link InputMethodManager#switchToNextInputMethodIBinder,boolean)}
    118         // because the current device is running ICS or previous and lacks the API.
    119         if (switchToNextInputSubtypeInThisIme(token, onlyCurrentIme)) {
    120             return true;
    121         }
    122         return switchToNextInputMethodAndSubtype(token);
    123     }
    124 
    125     private boolean switchToNextInputSubtypeInThisIme(final IBinder token,
    126             final boolean onlyCurrentIme) {
    127         final InputMethodManager imm = mImmWrapper.mImm;
    128         final InputMethodSubtype currentSubtype = imm.getCurrentInputMethodSubtype();
    129         final List<InputMethodSubtype> enabledSubtypes = getMyEnabledInputMethodSubtypeList(
    130                 true /* allowsImplicitlySelectedSubtypes */);
    131         final int currentIndex = getSubtypeIndexInList(currentSubtype, enabledSubtypes);
    132         if (currentIndex == INDEX_NOT_FOUND) {
    133             Log.w(TAG, "Can't find current subtype in enabled subtypes: subtype="
    134                     + SubtypeLocaleUtils.getSubtypeNameForLogging(currentSubtype));
    135             return false;
    136         }
    137         final int nextIndex = (currentIndex + 1) % enabledSubtypes.size();
    138         if (nextIndex <= currentIndex && !onlyCurrentIme) {
    139             // The current subtype is the last or only enabled one and it needs to switch to
    140             // next IME.
    141             return false;
    142         }
    143         final InputMethodSubtype nextSubtype = enabledSubtypes.get(nextIndex);
    144         setInputMethodAndSubtype(token, nextSubtype);
    145         return true;
    146     }
    147 
    148     private boolean switchToNextInputMethodAndSubtype(final IBinder token) {
    149         final InputMethodManager imm = mImmWrapper.mImm;
    150         final List<InputMethodInfo> enabledImis = imm.getEnabledInputMethodList();
    151         final int currentIndex = getImiIndexInList(getInputMethodInfoOfThisIme(), enabledImis);
    152         if (currentIndex == INDEX_NOT_FOUND) {
    153             Log.w(TAG, "Can't find current IME in enabled IMEs: IME package="
    154                     + getInputMethodInfoOfThisIme().getPackageName());
    155             return false;
    156         }
    157         final InputMethodInfo nextImi = getNextNonAuxiliaryIme(currentIndex, enabledImis);
    158         final List<InputMethodSubtype> enabledSubtypes = getEnabledInputMethodSubtypeList(nextImi,
    159                 true /* allowsImplicitlySelectedSubtypes */);
    160         if (enabledSubtypes.isEmpty()) {
    161             // The next IME has no subtype.
    162             imm.setInputMethod(token, nextImi.getId());
    163             return true;
    164         }
    165         final InputMethodSubtype firstSubtype = enabledSubtypes.get(0);
    166         imm.setInputMethodAndSubtype(token, nextImi.getId(), firstSubtype);
    167         return true;
    168     }
    169 
    170     private static int getImiIndexInList(final InputMethodInfo inputMethodInfo,
    171             final List<InputMethodInfo> imiList) {
    172         final int count = imiList.size();
    173         for (int index = 0; index < count; index++) {
    174             final InputMethodInfo imi = imiList.get(index);
    175             if (imi.equals(inputMethodInfo)) {
    176                 return index;
    177             }
    178         }
    179         return INDEX_NOT_FOUND;
    180     }
    181 
    182     // This method mimics {@link InputMethodManager#switchToNextInputMethod(IBinder,boolean)}.
    183     private static InputMethodInfo getNextNonAuxiliaryIme(final int currentIndex,
    184             final List<InputMethodInfo> imiList) {
    185         final int count = imiList.size();
    186         for (int i = 1; i < count; i++) {
    187             final int nextIndex = (currentIndex + i) % count;
    188             final InputMethodInfo nextImi = imiList.get(nextIndex);
    189             if (!isAuxiliaryIme(nextImi)) {
    190                 return nextImi;
    191             }
    192         }
    193         return imiList.get(currentIndex);
    194     }
    195 
    196     // Copied from {@link InputMethodInfo}. See how auxiliary of IME is determined.
    197     private static boolean isAuxiliaryIme(final InputMethodInfo imi) {
    198         final int count = imi.getSubtypeCount();
    199         if (count == 0) {
    200             return false;
    201         }
    202         for (int index = 0; index < count; index++) {
    203             final InputMethodSubtype subtype = imi.getSubtypeAt(index);
    204             if (!subtype.isAuxiliary()) {
    205                 return false;
    206             }
    207         }
    208         return true;
    209     }
    210 
    211     private static class InputMethodInfoCache {
    212         private final InputMethodManager mImm;
    213         private final String mImePackageName;
    214 
    215         private InputMethodInfo mCachedValue;
    216 
    217         public InputMethodInfoCache(final InputMethodManager imm, final String imePackageName) {
    218             mImm = imm;
    219             mImePackageName = imePackageName;
    220         }
    221 
    222         public synchronized InputMethodInfo get() {
    223             if (mCachedValue != null) {
    224                 return mCachedValue;
    225             }
    226             for (final InputMethodInfo imi : mImm.getInputMethodList()) {
    227                 if (imi.getPackageName().equals(mImePackageName)) {
    228                     mCachedValue = imi;
    229                     return imi;
    230                 }
    231             }
    232             throw new RuntimeException("Input method id for " + mImePackageName + " not found.");
    233         }
    234 
    235         public synchronized void clear() {
    236             mCachedValue = null;
    237         }
    238     }
    239 
    240     public InputMethodInfo getInputMethodInfoOfThisIme() {
    241         return mInputMethodInfoCache.get();
    242     }
    243 
    244     public String getInputMethodIdOfThisIme() {
    245         return getInputMethodInfoOfThisIme().getId();
    246     }
    247 
    248     public boolean checkIfSubtypeBelongsToThisImeAndEnabled(final InputMethodSubtype subtype) {
    249         return checkIfSubtypeBelongsToImeAndEnabled(getInputMethodInfoOfThisIme(), subtype);
    250     }
    251 
    252     public boolean checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(
    253             final InputMethodSubtype subtype) {
    254         final boolean subtypeEnabled = checkIfSubtypeBelongsToThisImeAndEnabled(subtype);
    255         final boolean subtypeExplicitlyEnabled = checkIfSubtypeBelongsToList(
    256                 subtype, getMyEnabledInputMethodSubtypeList(
    257                         false /* allowsImplicitlySelectedSubtypes */));
    258         return subtypeEnabled && !subtypeExplicitlyEnabled;
    259     }
    260 
    261     public boolean checkIfSubtypeBelongsToImeAndEnabled(final InputMethodInfo imi,
    262             final InputMethodSubtype subtype) {
    263         return checkIfSubtypeBelongsToList(subtype, getEnabledInputMethodSubtypeList(imi,
    264                 true /* allowsImplicitlySelectedSubtypes */));
    265     }
    266 
    267     private static boolean checkIfSubtypeBelongsToList(final InputMethodSubtype subtype,
    268             final List<InputMethodSubtype> subtypes) {
    269         return getSubtypeIndexInList(subtype, subtypes) != INDEX_NOT_FOUND;
    270     }
    271 
    272     private static int getSubtypeIndexInList(final InputMethodSubtype subtype,
    273             final List<InputMethodSubtype> subtypes) {
    274         final int count = subtypes.size();
    275         for (int index = 0; index < count; index++) {
    276             final InputMethodSubtype ims = subtypes.get(index);
    277             if (ims.equals(subtype)) {
    278                 return index;
    279             }
    280         }
    281         return INDEX_NOT_FOUND;
    282     }
    283 
    284     public boolean checkIfSubtypeBelongsToThisIme(final InputMethodSubtype subtype) {
    285         return getSubtypeIndexInIme(subtype, getInputMethodInfoOfThisIme()) != INDEX_NOT_FOUND;
    286     }
    287 
    288     private static int getSubtypeIndexInIme(final InputMethodSubtype subtype,
    289             final InputMethodInfo imi) {
    290         final int count = imi.getSubtypeCount();
    291         for (int index = 0; index < count; index++) {
    292             final InputMethodSubtype ims = imi.getSubtypeAt(index);
    293             if (ims.equals(subtype)) {
    294                 return index;
    295             }
    296         }
    297         return INDEX_NOT_FOUND;
    298     }
    299 
    300     public InputMethodSubtype getCurrentInputMethodSubtype(
    301             final InputMethodSubtype defaultSubtype) {
    302         final InputMethodSubtype currentSubtype = mImmWrapper.mImm.getCurrentInputMethodSubtype();
    303         return (currentSubtype != null) ? currentSubtype : defaultSubtype;
    304     }
    305 
    306     public boolean hasMultipleEnabledIMEsOrSubtypes(final boolean shouldIncludeAuxiliarySubtypes) {
    307         final List<InputMethodInfo> enabledImis = mImmWrapper.mImm.getEnabledInputMethodList();
    308         return hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, enabledImis);
    309     }
    310 
    311     public boolean hasMultipleEnabledSubtypesInThisIme(
    312             final boolean shouldIncludeAuxiliarySubtypes) {
    313         final List<InputMethodInfo> imiList = Collections.singletonList(
    314                 getInputMethodInfoOfThisIme());
    315         return hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, imiList);
    316     }
    317 
    318     private boolean hasMultipleEnabledSubtypes(final boolean shouldIncludeAuxiliarySubtypes,
    319             final List<InputMethodInfo> imiList) {
    320         // Number of the filtered IMEs
    321         int filteredImisCount = 0;
    322 
    323         for (InputMethodInfo imi : imiList) {
    324             // We can return true immediately after we find two or more filtered IMEs.
    325             if (filteredImisCount > 1) return true;
    326             final List<InputMethodSubtype> subtypes = getEnabledInputMethodSubtypeList(imi, true);
    327             // IMEs that have no subtypes should be counted.
    328             if (subtypes.isEmpty()) {
    329                 ++filteredImisCount;
    330                 continue;
    331             }
    332 
    333             int auxCount = 0;
    334             for (InputMethodSubtype subtype : subtypes) {
    335                 if (subtype.isAuxiliary()) {
    336                     ++auxCount;
    337                 }
    338             }
    339             final int nonAuxCount = subtypes.size() - auxCount;
    340 
    341             // IMEs that have one or more non-auxiliary subtypes should be counted.
    342             // If shouldIncludeAuxiliarySubtypes is true, IMEs that have two or more auxiliary
    343             // subtypes should be counted as well.
    344             if (nonAuxCount > 0 || (shouldIncludeAuxiliarySubtypes && auxCount > 1)) {
    345                 ++filteredImisCount;
    346                 continue;
    347             }
    348         }
    349 
    350         if (filteredImisCount > 1) {
    351             return true;
    352         }
    353         final List<InputMethodSubtype> subtypes = getMyEnabledInputMethodSubtypeList(true);
    354         int keyboardCount = 0;
    355         // imm.getEnabledInputMethodSubtypeList(null, true) will return the current IME's
    356         // both explicitly and implicitly enabled input method subtype.
    357         // (The current IME should be LatinIME.)
    358         for (InputMethodSubtype subtype : subtypes) {
    359             if (KEYBOARD_MODE.equals(subtype.getMode())) {
    360                 ++keyboardCount;
    361             }
    362         }
    363         return keyboardCount > 1;
    364     }
    365 
    366     public InputMethodSubtype findSubtypeByLocaleAndKeyboardLayoutSet(final String localeString,
    367             final String keyboardLayoutSetName) {
    368         final InputMethodInfo myImi = getInputMethodInfoOfThisIme();
    369         final int count = myImi.getSubtypeCount();
    370         for (int i = 0; i < count; i++) {
    371             final InputMethodSubtype subtype = myImi.getSubtypeAt(i);
    372             final String layoutName = SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype);
    373             if (localeString.equals(subtype.getLocale())
    374                     && keyboardLayoutSetName.equals(layoutName)) {
    375                 return subtype;
    376             }
    377         }
    378         return null;
    379     }
    380 
    381     public void setInputMethodAndSubtype(final IBinder token, final InputMethodSubtype subtype) {
    382         mImmWrapper.mImm.setInputMethodAndSubtype(
    383                 token, getInputMethodIdOfThisIme(), subtype);
    384     }
    385 
    386     public void setAdditionalInputMethodSubtypes(final InputMethodSubtype[] subtypes) {
    387         mImmWrapper.mImm.setAdditionalInputMethodSubtypes(
    388                 getInputMethodIdOfThisIme(), subtypes);
    389         // Clear the cache so that we go read the {@link InputMethodInfo} of this IME and list of
    390         // subtypes again next time.
    391         clearSubtypeCaches();
    392     }
    393 
    394     private List<InputMethodSubtype> getEnabledInputMethodSubtypeList(final InputMethodInfo imi,
    395             final boolean allowsImplicitlySelectedSubtypes) {
    396         final HashMap<InputMethodInfo, List<InputMethodSubtype>> cache =
    397                 allowsImplicitlySelectedSubtypes
    398                 ? mSubtypeListCacheWithImplicitlySelectedSubtypes
    399                 : mSubtypeListCacheWithoutImplicitlySelectedSubtypes;
    400         final List<InputMethodSubtype> cachedList = cache.get(imi);
    401         if (null != cachedList) return cachedList;
    402         final List<InputMethodSubtype> result = mImmWrapper.mImm.getEnabledInputMethodSubtypeList(
    403                 imi, allowsImplicitlySelectedSubtypes);
    404         cache.put(imi, result);
    405         return result;
    406     }
    407 
    408     public void clearSubtypeCaches() {
    409         mSubtypeListCacheWithImplicitlySelectedSubtypes.clear();
    410         mSubtypeListCacheWithoutImplicitlySelectedSubtypes.clear();
    411         mInputMethodInfoCache.clear();
    412     }
    413 
    414     public boolean shouldOfferSwitchingToNextInputMethod(final IBinder binder,
    415             boolean defaultValue) {
    416         // Use the default value instead on Jelly Bean MR2 and previous where
    417         // {@link InputMethodManager#shouldOfferSwitchingToNextInputMethod} isn't yet available
    418         // and on KitKat where the API is still just a stub to return true always.
    419         if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
    420             return defaultValue;
    421         }
    422         return mImmWrapper.shouldOfferSwitchingToNextInputMethod(binder);
    423     }
    424 }
    425