1 /* 2 * Copyright (C) 2013 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 android.os; 18 19 import android.content.Context; 20 import android.content.pm.ApplicationInfo; 21 import android.content.pm.ResolveInfo; 22 import android.content.pm.ServiceInfo; 23 import android.test.InstrumentationTestCase; 24 import android.test.suitebuilder.annotation.SmallTest; 25 import android.view.inputmethod.InputMethodInfo; 26 import android.view.inputmethod.InputMethodSubtype; 27 import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder; 28 29 import com.android.internal.inputmethod.InputMethodUtils; 30 31 import java.util.ArrayList; 32 import java.util.Arrays; 33 import java.util.HashSet; 34 import java.util.List; 35 import java.util.Locale; 36 import java.util.Objects; 37 38 public class InputMethodTest extends InstrumentationTestCase { 39 private static final boolean IS_AUX = true; 40 private static final boolean IS_DEFAULT = true; 41 private static final boolean IS_AUTO = true; 42 private static final boolean IS_ASCII_CAPABLE = true; 43 private static final boolean IS_SYSTEM_READY = true; 44 private static final ArrayList<InputMethodSubtype> NO_SUBTYPE = null; 45 private static final Locale LOCALE_EN_US = new Locale("en", "US"); 46 private static final Locale LOCALE_EN_GB = new Locale("en", "GB"); 47 private static final Locale LOCALE_EN_IN = new Locale("en", "IN"); 48 private static final Locale LOCALE_HI = new Locale("hi"); 49 private static final Locale LOCALE_JA_JP = new Locale("ja", "JP"); 50 private static final Locale LOCALE_ZH_CN = new Locale("zh", "CN"); 51 private static final Locale LOCALE_ZH_TW = new Locale("zh", "TW"); 52 private static final String SUBTYPE_MODE_KEYBOARD = "keyboard"; 53 private static final String SUBTYPE_MODE_VOICE = "voice"; 54 55 @SmallTest 56 public void testVoiceImes() throws Exception { 57 // locale: en_US 58 assertDefaultEnabledImes(getImesWithDefaultVoiceIme(), LOCALE_EN_US, 59 !IS_SYSTEM_READY, "DummyDefaultEnKeyboardIme", "DummyDefaultAutoVoiceIme"); 60 assertDefaultEnabledImes(getImesWithoutDefaultVoiceIme(), LOCALE_EN_US, 61 !IS_SYSTEM_READY, "DummyDefaultEnKeyboardIme"); 62 assertDefaultEnabledImes(getImesWithDefaultVoiceIme(), LOCALE_EN_US, 63 IS_SYSTEM_READY, "DummyDefaultEnKeyboardIme", "DummyDefaultAutoVoiceIme"); 64 assertDefaultEnabledImes(getImesWithoutDefaultVoiceIme(), LOCALE_EN_US, 65 IS_SYSTEM_READY, "DummyDefaultEnKeyboardIme", "DummyNonDefaultAutoVoiceIme0", 66 "DummyNonDefaultAutoVoiceIme1"); 67 68 // locale: en_GB 69 assertDefaultEnabledImes(getImesWithDefaultVoiceIme(), LOCALE_EN_GB, 70 !IS_SYSTEM_READY, "DummyDefaultEnKeyboardIme", "DummyDefaultAutoVoiceIme"); 71 assertDefaultEnabledImes(getImesWithoutDefaultVoiceIme(), LOCALE_EN_GB, 72 !IS_SYSTEM_READY, "DummyDefaultEnKeyboardIme"); 73 assertDefaultEnabledImes(getImesWithDefaultVoiceIme(), LOCALE_EN_GB, 74 IS_SYSTEM_READY, "DummyDefaultEnKeyboardIme", "DummyDefaultAutoVoiceIme"); 75 assertDefaultEnabledImes(getImesWithoutDefaultVoiceIme(), LOCALE_EN_GB, 76 IS_SYSTEM_READY, "DummyDefaultEnKeyboardIme", "DummyNonDefaultAutoVoiceIme0", 77 "DummyNonDefaultAutoVoiceIme1"); 78 79 // locale: ja_JP 80 assertDefaultEnabledImes(getImesWithDefaultVoiceIme(), LOCALE_JA_JP, 81 !IS_SYSTEM_READY, "DummyDefaultEnKeyboardIme", "DummyDefaultAutoVoiceIme"); 82 assertDefaultEnabledImes(getImesWithoutDefaultVoiceIme(), LOCALE_JA_JP, 83 !IS_SYSTEM_READY, "DummyDefaultEnKeyboardIme"); 84 assertDefaultEnabledImes(getImesWithDefaultVoiceIme(), LOCALE_JA_JP, 85 IS_SYSTEM_READY, "DummyDefaultEnKeyboardIme", "DummyDefaultAutoVoiceIme"); 86 assertDefaultEnabledImes(getImesWithoutDefaultVoiceIme(), LOCALE_JA_JP, 87 IS_SYSTEM_READY, "DummyDefaultEnKeyboardIme", "DummyNonDefaultAutoVoiceIme0", 88 "DummyNonDefaultAutoVoiceIme1"); 89 } 90 91 @SmallTest 92 public void testKeyboardImes() throws Exception { 93 // locale: en_US 94 assertDefaultEnabledImes(getSamplePreinstalledImes("en-rUS"), LOCALE_EN_US, 95 !IS_SYSTEM_READY, "com.android.apps.inputmethod.latin"); 96 assertDefaultEnabledImes(getSamplePreinstalledImes("en-rUS"), LOCALE_EN_US, 97 IS_SYSTEM_READY, "com.android.apps.inputmethod.latin", 98 "com.android.apps.inputmethod.voice"); 99 100 // locale: en_GB 101 assertDefaultEnabledImes(getSamplePreinstalledImes("en-rGB"), LOCALE_EN_GB, 102 !IS_SYSTEM_READY, "com.android.apps.inputmethod.latin"); 103 assertDefaultEnabledImes(getSamplePreinstalledImes("en-rGB"), LOCALE_EN_GB, 104 IS_SYSTEM_READY, "com.android.apps.inputmethod.latin", 105 "com.android.apps.inputmethod.voice"); 106 107 // locale: en_IN 108 assertDefaultEnabledImes(getSamplePreinstalledImes("en-rIN"), LOCALE_EN_IN, 109 !IS_SYSTEM_READY, "com.android.apps.inputmethod.latin"); 110 assertDefaultEnabledImes(getSamplePreinstalledImes("en-rIN"), LOCALE_EN_IN, 111 IS_SYSTEM_READY, "com.android.apps.inputmethod.hindi", 112 "com.android.apps.inputmethod.latin", "com.android.apps.inputmethod.voice"); 113 114 // locale: hi 115 assertDefaultEnabledImes(getSamplePreinstalledImes("hi"), LOCALE_HI, 116 !IS_SYSTEM_READY, "com.android.apps.inputmethod.latin"); 117 assertDefaultEnabledImes(getSamplePreinstalledImes("hi"), LOCALE_HI, 118 IS_SYSTEM_READY, "com.android.apps.inputmethod.hindi", 119 "com.android.apps.inputmethod.latin", "com.android.apps.inputmethod.voice"); 120 121 // locale: ja_JP 122 assertDefaultEnabledImes(getSamplePreinstalledImes("ja-rJP"), LOCALE_JA_JP, 123 !IS_SYSTEM_READY, "com.android.apps.inputmethod.latin"); 124 assertDefaultEnabledImes(getSamplePreinstalledImes("ja-rJP"), LOCALE_JA_JP, 125 IS_SYSTEM_READY, "com.android.apps.inputmethod.japanese", 126 "com.android.apps.inputmethod.voice"); 127 128 // locale: zh_CN 129 assertDefaultEnabledImes(getSamplePreinstalledImes("zh-rCN"), LOCALE_ZH_CN, 130 !IS_SYSTEM_READY, "com.android.apps.inputmethod.latin"); 131 assertDefaultEnabledImes(getSamplePreinstalledImes("zh-rCN"), LOCALE_ZH_CN, 132 IS_SYSTEM_READY, "com.android.apps.inputmethod.pinyin", 133 "com.android.apps.inputmethod.voice"); 134 135 // locale: zh_TW 136 // Note: In this case, no IME is suitable for the system locale. Hence we will pick up a 137 // fallback IME regardless of the "default" attribute. 138 assertDefaultEnabledImes(getSamplePreinstalledImes("zh-rTW"), LOCALE_ZH_TW, 139 !IS_SYSTEM_READY, "com.android.apps.inputmethod.latin"); 140 assertDefaultEnabledImes(getSamplePreinstalledImes("zh-rTW"), LOCALE_ZH_TW, 141 IS_SYSTEM_READY, "com.android.apps.inputmethod.latin", 142 "com.android.apps.inputmethod.voice"); 143 } 144 145 @SmallTest 146 public void testParcelable() throws Exception { 147 final ArrayList<InputMethodInfo> originalList = getSamplePreinstalledImes("en-rUS"); 148 final List<InputMethodInfo> clonedList = cloneViaParcel(originalList); 149 assertNotNull(clonedList); 150 final List<InputMethodInfo> clonedClonedList = cloneViaParcel(clonedList); 151 assertNotNull(clonedClonedList); 152 assertEquals(originalList, clonedList); 153 assertEquals(clonedList, clonedClonedList); 154 assertEquals(originalList.size(), clonedList.size()); 155 assertEquals(clonedList.size(), clonedClonedList.size()); 156 for (int imeIndex = 0; imeIndex < originalList.size(); ++imeIndex) { 157 verifyEquality(originalList.get(imeIndex), clonedList.get(imeIndex)); 158 verifyEquality(clonedList.get(imeIndex), clonedClonedList.get(imeIndex)); 159 } 160 } 161 162 private void assertDefaultEnabledImes(final ArrayList<InputMethodInfo> preinstalledImes, 163 final Locale systemLocale, final boolean isSystemReady, String... expectedImeNames) { 164 final Context context = getInstrumentation().getTargetContext(); 165 final String[] actualImeNames = getPackageNames(callGetDefaultEnabledImesUnderWithLocale( 166 context, isSystemReady, preinstalledImes, systemLocale)); 167 assertEquals(expectedImeNames.length, actualImeNames.length); 168 for (int i = 0; i < expectedImeNames.length; ++i) { 169 assertEquals(expectedImeNames[i], actualImeNames[i]); 170 } 171 } 172 173 private static List<InputMethodInfo> cloneViaParcel(final List<InputMethodInfo> list) { 174 Parcel p = null; 175 try { 176 p = Parcel.obtain(); 177 p.writeTypedList(list); 178 p.setDataPosition(0); 179 return p.createTypedArrayList(InputMethodInfo.CREATOR); 180 } finally { 181 if (p != null) { 182 p.recycle(); 183 } 184 } 185 } 186 187 private static ArrayList<InputMethodInfo> callGetDefaultEnabledImesUnderWithLocale( 188 final Context context, final boolean isSystemReady, 189 final ArrayList<InputMethodInfo> imis, final Locale locale) { 190 final Locale initialLocale = context.getResources().getConfiguration().locale; 191 try { 192 context.getResources().getConfiguration().setLocale(locale); 193 return InputMethodUtils.getDefaultEnabledImes(context, isSystemReady, imis); 194 } finally { 195 context.getResources().getConfiguration().setLocale(initialLocale); 196 } 197 } 198 199 private String[] getPackageNames(final ArrayList<InputMethodInfo> imis) { 200 final String[] packageNames = new String[imis.size()]; 201 for (int i = 0; i < imis.size(); ++i) { 202 packageNames[i] = imis.get(i).getPackageName(); 203 } 204 return packageNames; 205 } 206 207 private static void verifyEquality(InputMethodInfo expected, InputMethodInfo actual) { 208 assertEquals(expected, actual); 209 assertEquals(expected.getSubtypeCount(), actual.getSubtypeCount()); 210 for (int subtypeIndex = 0; subtypeIndex < expected.getSubtypeCount(); ++subtypeIndex) { 211 final InputMethodSubtype expectedSubtype = expected.getSubtypeAt(subtypeIndex); 212 final InputMethodSubtype actualSubtype = actual.getSubtypeAt(subtypeIndex); 213 assertEquals(expectedSubtype, actualSubtype); 214 assertEquals(expectedSubtype.hashCode(), actualSubtype.hashCode()); 215 } 216 } 217 218 private static InputMethodInfo createDummyInputMethodInfo(String packageName, String name, 219 CharSequence label, boolean isAuxIme, boolean isDefault, 220 List<InputMethodSubtype> subtypes) { 221 final ResolveInfo ri = new ResolveInfo(); 222 final ServiceInfo si = new ServiceInfo(); 223 final ApplicationInfo ai = new ApplicationInfo(); 224 ai.packageName = packageName; 225 ai.enabled = true; 226 ai.flags |= ApplicationInfo.FLAG_SYSTEM; 227 si.applicationInfo = ai; 228 si.enabled = true; 229 si.packageName = packageName; 230 si.name = name; 231 si.exported = true; 232 si.nonLocalizedLabel = label; 233 ri.serviceInfo = si; 234 return new InputMethodInfo(ri, isAuxIme, "", subtypes, 1, isDefault); 235 } 236 237 private static InputMethodSubtype createDummyInputMethodSubtype(String locale, String mode, 238 boolean isAuxiliary, boolean overridesImplicitlyEnabledSubtype, 239 boolean isAsciiCapable) { 240 return new InputMethodSubtypeBuilder() 241 .setSubtypeNameResId(0) 242 .setSubtypeIconResId(0) 243 .setSubtypeLocale(locale) 244 .setSubtypeMode(mode) 245 .setSubtypeExtraValue("") 246 .setIsAuxiliary(isAuxiliary) 247 .setOverridesImplicitlyEnabledSubtype(overridesImplicitlyEnabledSubtype) 248 .setIsAsciiCapable(isAsciiCapable) 249 .build(); 250 } 251 252 private static ArrayList<InputMethodInfo> getImesWithDefaultVoiceIme() { 253 ArrayList<InputMethodInfo> preinstalledImes = new ArrayList<>(); 254 { 255 final ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>(); 256 subtypes.add(createDummyInputMethodSubtype("auto", SUBTYPE_MODE_VOICE, IS_AUX, IS_AUTO, 257 !IS_ASCII_CAPABLE)); 258 subtypes.add(createDummyInputMethodSubtype("en_US", SUBTYPE_MODE_VOICE, IS_AUX, 259 !IS_AUTO, !IS_ASCII_CAPABLE)); 260 preinstalledImes.add(createDummyInputMethodInfo("DummyDefaultAutoVoiceIme", 261 "dummy.voice0", "DummyVoice0", IS_AUX, IS_DEFAULT, subtypes)); 262 } 263 preinstalledImes.addAll(getImesWithoutDefaultVoiceIme()); 264 return preinstalledImes; 265 } 266 267 private static ArrayList<InputMethodInfo> getImesWithoutDefaultVoiceIme() { 268 ArrayList<InputMethodInfo> preinstalledImes = new ArrayList<>(); 269 { 270 final ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>(); 271 subtypes.add(createDummyInputMethodSubtype("auto", SUBTYPE_MODE_VOICE, IS_AUX, IS_AUTO, 272 !IS_ASCII_CAPABLE)); 273 subtypes.add(createDummyInputMethodSubtype("en_US", SUBTYPE_MODE_VOICE, IS_AUX, 274 !IS_AUTO, !IS_ASCII_CAPABLE)); 275 preinstalledImes.add(createDummyInputMethodInfo("DummyNonDefaultAutoVoiceIme0", 276 "dummy.voice1", "DummyVoice1", IS_AUX, !IS_DEFAULT, subtypes)); 277 } 278 { 279 final ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>(); 280 subtypes.add(createDummyInputMethodSubtype("auto", SUBTYPE_MODE_VOICE, IS_AUX, IS_AUTO, 281 !IS_ASCII_CAPABLE)); 282 subtypes.add(createDummyInputMethodSubtype("en_US", SUBTYPE_MODE_VOICE, IS_AUX, 283 !IS_AUTO, !IS_ASCII_CAPABLE)); 284 preinstalledImes.add(createDummyInputMethodInfo("DummyNonDefaultAutoVoiceIme1", 285 "dummy.voice2", "DummyVoice2", IS_AUX, !IS_DEFAULT, subtypes)); 286 } 287 { 288 final ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>(); 289 subtypes.add(createDummyInputMethodSubtype("en_US", SUBTYPE_MODE_VOICE, IS_AUX, 290 !IS_AUTO, !IS_ASCII_CAPABLE)); 291 preinstalledImes.add(createDummyInputMethodInfo("DummyNonDefaultVoiceIme2", 292 "dummy.voice3", "DummyVoice3", IS_AUX, !IS_DEFAULT, subtypes)); 293 } 294 { 295 final ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>(); 296 subtypes.add(createDummyInputMethodSubtype("en_US", SUBTYPE_MODE_KEYBOARD, !IS_AUX, 297 !IS_AUTO, IS_ASCII_CAPABLE)); 298 preinstalledImes.add(createDummyInputMethodInfo("DummyDefaultEnKeyboardIme", 299 "dummy.keyboard0", "DummyKeyboard0", !IS_AUX, IS_DEFAULT, subtypes)); 300 } 301 return preinstalledImes; 302 } 303 304 private static boolean contains(final String[] textList, final String textToBeChecked) { 305 if (textList == null) { 306 return false; 307 } 308 for (final String text : textList) { 309 if (Objects.equals(textToBeChecked, text)) { 310 return true; 311 } 312 } 313 return false; 314 } 315 316 private static ArrayList<InputMethodInfo> getSamplePreinstalledImes(final String localeString) { 317 ArrayList<InputMethodInfo> preinstalledImes = new ArrayList<>(); 318 319 // a dummy Voice IME 320 { 321 final boolean isDefaultIme = false; 322 final ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>(); 323 subtypes.add(createDummyInputMethodSubtype("", SUBTYPE_MODE_VOICE, IS_AUX, 324 IS_AUTO, !IS_ASCII_CAPABLE)); 325 preinstalledImes.add(createDummyInputMethodInfo("com.android.apps.inputmethod.voice", 326 "com.android.inputmethod.voice", "DummyVoiceIme", IS_AUX, isDefaultIme, 327 subtypes)); 328 } 329 // a dummy Hindi IME 330 { 331 final boolean isDefaultIme = contains(new String[]{ "hi", "en-rIN" }, localeString); 332 final ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>(); 333 // TODO: This subtype should be marked as IS_ASCII_CAPABLE 334 subtypes.add(createDummyInputMethodSubtype("en_IN", SUBTYPE_MODE_KEYBOARD, !IS_AUX, 335 !IS_AUTO, !IS_ASCII_CAPABLE)); 336 subtypes.add(createDummyInputMethodSubtype("hi", SUBTYPE_MODE_KEYBOARD, !IS_AUX, 337 !IS_AUTO, !IS_ASCII_CAPABLE)); 338 preinstalledImes.add(createDummyInputMethodInfo("com.android.apps.inputmethod.hindi", 339 "com.android.inputmethod.hindi", "DummyHindiIme", !IS_AUX, isDefaultIme, 340 subtypes)); 341 } 342 343 // a dummy Pinyin IME 344 { 345 final boolean isDefaultIme = contains(new String[]{ "zh-rCN" }, localeString); 346 final ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>(); 347 subtypes.add(createDummyInputMethodSubtype("zh_CN", SUBTYPE_MODE_KEYBOARD, !IS_AUX, 348 !IS_AUTO, !IS_ASCII_CAPABLE)); 349 preinstalledImes.add(createDummyInputMethodInfo("com.android.apps.inputmethod.pinyin", 350 "com.android.apps.inputmethod.pinyin", "DummyPinyinIme", !IS_AUX, isDefaultIme, 351 subtypes)); 352 } 353 354 // a dummy Korean IME 355 { 356 final boolean isDefaultIme = contains(new String[]{ "ko" }, localeString); 357 final ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>(); 358 subtypes.add(createDummyInputMethodSubtype("ko", SUBTYPE_MODE_KEYBOARD, !IS_AUX, 359 !IS_AUTO, !IS_ASCII_CAPABLE)); 360 preinstalledImes.add(createDummyInputMethodInfo("com.android.apps.inputmethod.korean", 361 "com.android.apps.inputmethod.korean", "DummyKoreanIme", !IS_AUX, isDefaultIme, 362 subtypes)); 363 } 364 365 // a dummy Latin IME 366 { 367 final boolean isDefaultIme = contains( 368 new String[]{ "en-rUS", "en-rGB", "en-rIN", "en", "hi" }, localeString); 369 final ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>(); 370 subtypes.add(createDummyInputMethodSubtype("en_US", SUBTYPE_MODE_KEYBOARD, !IS_AUX, 371 !IS_AUTO, IS_ASCII_CAPABLE)); 372 subtypes.add(createDummyInputMethodSubtype("en_GB", SUBTYPE_MODE_KEYBOARD, !IS_AUX, 373 !IS_AUTO, IS_ASCII_CAPABLE)); 374 subtypes.add(createDummyInputMethodSubtype("en_IN", SUBTYPE_MODE_KEYBOARD, !IS_AUX, 375 !IS_AUTO, IS_ASCII_CAPABLE)); 376 subtypes.add(createDummyInputMethodSubtype("hi", SUBTYPE_MODE_KEYBOARD, !IS_AUX, 377 !IS_AUTO, IS_ASCII_CAPABLE)); 378 preinstalledImes.add(createDummyInputMethodInfo("com.android.apps.inputmethod.latin", 379 "com.android.apps.inputmethod.latin", "DummyLatinIme", !IS_AUX, isDefaultIme, 380 subtypes)); 381 } 382 383 // a dummy Japanese IME 384 { 385 final boolean isDefaultIme = contains(new String[]{ "ja", "ja-rJP" }, localeString); 386 final ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>(); 387 subtypes.add(createDummyInputMethodSubtype("ja", SUBTYPE_MODE_KEYBOARD, !IS_AUX, 388 !IS_AUTO, !IS_ASCII_CAPABLE)); 389 subtypes.add(createDummyInputMethodSubtype("emoji", SUBTYPE_MODE_KEYBOARD, !IS_AUX, 390 !IS_AUTO, !IS_ASCII_CAPABLE)); 391 preinstalledImes.add(createDummyInputMethodInfo("com.android.apps.inputmethod.japanese", 392 "com.android.apps.inputmethod.japanese", "DummyJapaneseIme", !IS_AUX, 393 isDefaultIme, subtypes)); 394 } 395 396 return preinstalledImes; 397 } 398 } 399