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