Home | History | Annotate | Download | only in compat
      1 /*
      2  * Copyright (C) 2011 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.compat;
     18 
     19 import android.app.AlertDialog;
     20 import android.content.Context;
     21 import android.content.DialogInterface;
     22 import android.content.DialogInterface.OnClickListener;
     23 import android.content.Intent;
     24 import android.content.pm.ApplicationInfo;
     25 import android.content.pm.PackageManager;
     26 import android.os.IBinder;
     27 import android.text.TextUtils;
     28 import android.util.Log;
     29 import android.view.inputmethod.InputMethodInfo;
     30 import android.view.inputmethod.InputMethodManager;
     31 
     32 import com.android.inputmethod.deprecated.LanguageSwitcherProxy;
     33 import com.android.inputmethod.latin.R;
     34 import com.android.inputmethod.latin.SubtypeSwitcher;
     35 import com.android.inputmethod.latin.Utils;
     36 
     37 import java.lang.reflect.Method;
     38 import java.util.ArrayList;
     39 import java.util.Collections;
     40 import java.util.Comparator;
     41 import java.util.HashMap;
     42 import java.util.List;
     43 import java.util.Locale;
     44 import java.util.Map;
     45 
     46 // TODO: Override this class with the concrete implementation if we need to take care of the
     47 // performance.
     48 public class InputMethodManagerCompatWrapper {
     49     private static final String TAG = InputMethodManagerCompatWrapper.class.getSimpleName();
     50     private static final Method METHOD_getCurrentInputMethodSubtype =
     51             CompatUtils.getMethod(InputMethodManager.class, "getCurrentInputMethodSubtype");
     52     private static final Method METHOD_getEnabledInputMethodSubtypeList =
     53             CompatUtils.getMethod(InputMethodManager.class, "getEnabledInputMethodSubtypeList",
     54                     InputMethodInfo.class, boolean.class);
     55     private static final Method METHOD_getShortcutInputMethodsAndSubtypes =
     56             CompatUtils.getMethod(InputMethodManager.class, "getShortcutInputMethodsAndSubtypes");
     57     private static final Method METHOD_setInputMethodAndSubtype =
     58             CompatUtils.getMethod(
     59                     InputMethodManager.class, "setInputMethodAndSubtype", IBinder.class,
     60                     String.class, InputMethodSubtypeCompatWrapper.CLASS_InputMethodSubtype);
     61     private static final Method METHOD_switchToLastInputMethod = CompatUtils.getMethod(
     62             InputMethodManager.class, "switchToLastInputMethod", IBinder.class);
     63 
     64     private static final InputMethodManagerCompatWrapper sInstance =
     65             new InputMethodManagerCompatWrapper();
     66 
     67     public static final boolean SUBTYPE_SUPPORTED;
     68 
     69     static {
     70         // This static initializer guarantees that METHOD_getShortcutInputMethodsAndSubtypes is
     71         // already instantiated.
     72         SUBTYPE_SUPPORTED = METHOD_getShortcutInputMethodsAndSubtypes != null;
     73     }
     74 
     75     // For the compatibility, IMM will create dummy subtypes if subtypes are not found.
     76     // This is required to be false if the current behavior is broken. For now, it's ok to be true.
     77     public static final boolean FORCE_ENABLE_VOICE_EVEN_WITH_NO_VOICE_SUBTYPES =
     78             !InputMethodServiceCompatWrapper.CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED;
     79     private static final String VOICE_MODE = "voice";
     80     private static final String KEYBOARD_MODE = "keyboard";
     81 
     82     private InputMethodServiceCompatWrapper mService;
     83     private InputMethodManager mImm;
     84     private PackageManager mPackageManager;
     85     private ApplicationInfo mApplicationInfo;
     86     private LanguageSwitcherProxy mLanguageSwitcherProxy;
     87     private String mLatinImePackageName;
     88 
     89     public static InputMethodManagerCompatWrapper getInstance() {
     90         if (sInstance.mImm == null)
     91             Log.w(TAG, "getInstance() is called before initialization");
     92         return sInstance;
     93     }
     94 
     95     public static void init(InputMethodServiceCompatWrapper service) {
     96         sInstance.mService = service;
     97         sInstance.mImm = (InputMethodManager) service.getSystemService(
     98                 Context.INPUT_METHOD_SERVICE);
     99         sInstance.mLatinImePackageName = service.getPackageName();
    100         sInstance.mPackageManager = service.getPackageManager();
    101         sInstance.mApplicationInfo = service.getApplicationInfo();
    102         sInstance.mLanguageSwitcherProxy = LanguageSwitcherProxy.getInstance();
    103     }
    104 
    105     public InputMethodSubtypeCompatWrapper getCurrentInputMethodSubtype() {
    106         if (!SUBTYPE_SUPPORTED) {
    107             return new InputMethodSubtypeCompatWrapper(
    108                     0, 0, mLanguageSwitcherProxy.getInputLocale().toString(), KEYBOARD_MODE, "");
    109         }
    110         Object o = CompatUtils.invoke(mImm, null, METHOD_getCurrentInputMethodSubtype);
    111         return new InputMethodSubtypeCompatWrapper(o);
    112     }
    113 
    114     public List<InputMethodSubtypeCompatWrapper> getEnabledInputMethodSubtypeList(
    115             InputMethodInfoCompatWrapper imi, boolean allowsImplicitlySelectedSubtypes) {
    116         if (!SUBTYPE_SUPPORTED) {
    117             String[] languages = mLanguageSwitcherProxy.getEnabledLanguages(
    118                     allowsImplicitlySelectedSubtypes);
    119             List<InputMethodSubtypeCompatWrapper> subtypeList =
    120                     new ArrayList<InputMethodSubtypeCompatWrapper>();
    121             for (String lang: languages) {
    122                 subtypeList.add(new InputMethodSubtypeCompatWrapper(0, 0, lang, KEYBOARD_MODE, ""));
    123             }
    124             return subtypeList;
    125         }
    126         Object retval = CompatUtils.invoke(mImm, null, METHOD_getEnabledInputMethodSubtypeList,
    127                 (imi != null ? imi.getInputMethodInfo() : null), allowsImplicitlySelectedSubtypes);
    128         if (retval == null || !(retval instanceof List<?>) || ((List<?>)retval).isEmpty()) {
    129             if (!FORCE_ENABLE_VOICE_EVEN_WITH_NO_VOICE_SUBTYPES) {
    130                 // Returns an empty list
    131                 return Collections.emptyList();
    132             }
    133             // Creates dummy subtypes
    134             @SuppressWarnings("unused")
    135             List<InputMethodSubtypeCompatWrapper> subtypeList =
    136                     new ArrayList<InputMethodSubtypeCompatWrapper>();
    137             InputMethodSubtypeCompatWrapper keyboardSubtype = getLastResortSubtype(KEYBOARD_MODE);
    138             InputMethodSubtypeCompatWrapper voiceSubtype = getLastResortSubtype(VOICE_MODE);
    139             if (keyboardSubtype != null) {
    140                 subtypeList.add(keyboardSubtype);
    141             }
    142             if (voiceSubtype != null) {
    143                 subtypeList.add(voiceSubtype);
    144             }
    145             return subtypeList;
    146         }
    147         return CompatUtils.copyInputMethodSubtypeListToWrapper(retval);
    148     }
    149 
    150     private InputMethodInfoCompatWrapper getLatinImeInputMethodInfo() {
    151         if (TextUtils.isEmpty(mLatinImePackageName))
    152             return null;
    153         return Utils.getInputMethodInfo(this, mLatinImePackageName);
    154     }
    155 
    156     @SuppressWarnings("unused")
    157     private InputMethodSubtypeCompatWrapper getLastResortSubtype(String mode) {
    158         if (VOICE_MODE.equals(mode) && !FORCE_ENABLE_VOICE_EVEN_WITH_NO_VOICE_SUBTYPES)
    159             return null;
    160         Locale inputLocale = SubtypeSwitcher.getInstance().getInputLocale();
    161         if (inputLocale == null)
    162             return null;
    163         return new InputMethodSubtypeCompatWrapper(0, 0, inputLocale.toString(), mode, "");
    164     }
    165 
    166     public Map<InputMethodInfoCompatWrapper, List<InputMethodSubtypeCompatWrapper>>
    167             getShortcutInputMethodsAndSubtypes() {
    168         Object retval = CompatUtils.invoke(mImm, null, METHOD_getShortcutInputMethodsAndSubtypes);
    169         if (retval == null || !(retval instanceof Map<?, ?>) || ((Map<?, ?>)retval).isEmpty()) {
    170             if (!FORCE_ENABLE_VOICE_EVEN_WITH_NO_VOICE_SUBTYPES) {
    171                 // Returns an empty map
    172                 return Collections.emptyMap();
    173             }
    174             // Creates dummy subtypes
    175             @SuppressWarnings("unused")
    176             InputMethodInfoCompatWrapper imi = getLatinImeInputMethodInfo();
    177             InputMethodSubtypeCompatWrapper voiceSubtype = getLastResortSubtype(VOICE_MODE);
    178             if (imi != null && voiceSubtype != null) {
    179                 Map<InputMethodInfoCompatWrapper, List<InputMethodSubtypeCompatWrapper>>
    180                         shortcutMap =
    181                                 new HashMap<InputMethodInfoCompatWrapper,
    182                                         List<InputMethodSubtypeCompatWrapper>>();
    183                 List<InputMethodSubtypeCompatWrapper> subtypeList =
    184                         new ArrayList<InputMethodSubtypeCompatWrapper>();
    185                 subtypeList.add(voiceSubtype);
    186                 shortcutMap.put(imi, subtypeList);
    187                 return shortcutMap;
    188             } else {
    189                 return Collections.emptyMap();
    190             }
    191         }
    192         Map<InputMethodInfoCompatWrapper, List<InputMethodSubtypeCompatWrapper>> shortcutMap =
    193                 new HashMap<InputMethodInfoCompatWrapper, List<InputMethodSubtypeCompatWrapper>>();
    194         final Map<?, ?> retvalMap = (Map<?, ?>)retval;
    195         for (Object key : retvalMap.keySet()) {
    196             if (!(key instanceof InputMethodInfo)) {
    197                 Log.e(TAG, "Class type error.");
    198                 return null;
    199             }
    200             shortcutMap.put(new InputMethodInfoCompatWrapper((InputMethodInfo)key),
    201                     CompatUtils.copyInputMethodSubtypeListToWrapper(retvalMap.get(key)));
    202         }
    203         return shortcutMap;
    204     }
    205 
    206     // We don't call this method when we switch between subtypes within this IME.
    207     public void setInputMethodAndSubtype(
    208             IBinder token, String id, InputMethodSubtypeCompatWrapper subtype) {
    209         // TODO: Support subtype change on non-subtype-supported platform.
    210         if (subtype != null && subtype.hasOriginalObject()) {
    211             CompatUtils.invoke(mImm, null, METHOD_setInputMethodAndSubtype,
    212                     token, id, subtype.getOriginalObject());
    213         } else {
    214             mImm.setInputMethod(token, id);
    215         }
    216     }
    217 
    218     public boolean switchToLastInputMethod(IBinder token) {
    219         if (SubtypeSwitcher.getInstance().isDummyVoiceMode()) {
    220             return true;
    221         }
    222         return (Boolean)CompatUtils.invoke(mImm, false, METHOD_switchToLastInputMethod, token);
    223     }
    224 
    225     public List<InputMethodInfoCompatWrapper> getEnabledInputMethodList() {
    226         if (mImm == null) return null;
    227         List<InputMethodInfoCompatWrapper> imis = new ArrayList<InputMethodInfoCompatWrapper>();
    228         for (InputMethodInfo imi : mImm.getEnabledInputMethodList()) {
    229             imis.add(new InputMethodInfoCompatWrapper(imi));
    230         }
    231         return imis;
    232     }
    233 
    234     public void showInputMethodPicker() {
    235         if (mImm == null) return;
    236         if (SUBTYPE_SUPPORTED) {
    237             mImm.showInputMethodPicker();
    238             return;
    239         }
    240 
    241         // The code below are based on {@link InputMethodManager#showInputMethodMenuInternal}.
    242 
    243         final InputMethodInfoCompatWrapper myImi = Utils.getInputMethodInfo(
    244                 this, mLatinImePackageName);
    245         final List<InputMethodSubtypeCompatWrapper> myImsList = getEnabledInputMethodSubtypeList(
    246                 myImi, true);
    247         final InputMethodSubtypeCompatWrapper currentIms = getCurrentInputMethodSubtype();
    248         final List<InputMethodInfoCompatWrapper> imiList = getEnabledInputMethodList();
    249         imiList.remove(myImi);
    250         Collections.sort(imiList, new Comparator<InputMethodInfoCompatWrapper>() {
    251             @Override
    252             public int compare(InputMethodInfoCompatWrapper imi1,
    253                     InputMethodInfoCompatWrapper imi2) {
    254                 final CharSequence imiId1 = imi1.loadLabel(mPackageManager) + "/" + imi1.getId();
    255                 final CharSequence imiId2 = imi2.loadLabel(mPackageManager) + "/" + imi2.getId();
    256                 return imiId1.toString().compareTo(imiId2.toString());
    257             }
    258         });
    259 
    260         final int myImsCount = myImsList.size();
    261         final int imiCount = imiList.size();
    262         final CharSequence[] items = new CharSequence[myImsCount + imiCount];
    263 
    264         int checkedItem = 0;
    265         int index = 0;
    266         final CharSequence myImiLabel = myImi.loadLabel(mPackageManager);
    267         for (int i = 0; i < myImsCount; i++) {
    268             InputMethodSubtypeCompatWrapper ims = myImsList.get(i);
    269             if (currentIms.equals(ims))
    270                 checkedItem = index;
    271             final CharSequence title = TextUtils.concat(
    272                     ims.getDisplayName(mService, mLatinImePackageName, mApplicationInfo),
    273                     " (" + myImiLabel, ")");
    274             items[index] = title;
    275             index++;
    276         }
    277 
    278         for (int i = 0; i < imiCount; i++) {
    279             final InputMethodInfoCompatWrapper imi = imiList.get(i);
    280             final CharSequence title = imi.loadLabel(mPackageManager);
    281             items[index] = title;
    282             index++;
    283         }
    284 
    285         final OnClickListener buttonListener = new OnClickListener() {
    286             @Override
    287             public void onClick(DialogInterface di, int whichButton) {
    288                 final Intent intent = new Intent("android.settings.INPUT_METHOD_SETTINGS");
    289                 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
    290                         | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
    291                         | Intent.FLAG_ACTIVITY_CLEAR_TOP);
    292                 mService.startActivity(intent);
    293             }
    294         };
    295         final InputMethodServiceCompatWrapper service = mService;
    296         final IBinder token = service.getWindow().getWindow().getAttributes().token;
    297         final OnClickListener selectionListener = new OnClickListener() {
    298             @Override
    299             public void onClick(DialogInterface di, int which) {
    300                 di.dismiss();
    301                 if (which < myImsCount) {
    302                     final int imsIndex = which;
    303                     final InputMethodSubtypeCompatWrapper ims = myImsList.get(imsIndex);
    304                     service.notifyOnCurrentInputMethodSubtypeChanged(ims);
    305                 } else {
    306                     final int imiIndex = which - myImsCount;
    307                     final InputMethodInfoCompatWrapper imi = imiList.get(imiIndex);
    308                     setInputMethodAndSubtype(token, imi.getId(), null);
    309                 }
    310             }
    311         };
    312 
    313         final AlertDialog.Builder builder = new AlertDialog.Builder(mService)
    314                 .setTitle(mService.getString(R.string.selectInputMethod))
    315                 .setNeutralButton(R.string.configure_input_method, buttonListener)
    316                 .setSingleChoiceItems(items, checkedItem, selectionListener);
    317         mService.showOptionDialogInternal(builder.create());
    318     }
    319 }
    320