1 /* 2 * Copyright (C) 2023 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.server.input 18 19 import android.content.Context 20 import android.content.ContextWrapper 21 import android.content.pm.ActivityInfo 22 import android.content.pm.ApplicationInfo 23 import android.content.pm.PackageManager 24 import android.content.pm.ResolveInfo 25 import android.content.pm.ServiceInfo 26 import android.hardware.input.IInputManager 27 import android.hardware.input.InputManager 28 import android.hardware.input.InputManagerGlobal 29 import android.hardware.input.KeyboardLayout 30 import android.icu.util.ULocale 31 import android.os.Bundle 32 import android.os.test.TestLooper 33 import android.platform.test.annotations.Presubmit 34 import android.provider.Settings 35 import android.view.InputDevice 36 import android.view.inputmethod.InputMethodInfo 37 import android.view.inputmethod.InputMethodSubtype 38 import androidx.test.core.R 39 import androidx.test.core.app.ApplicationProvider 40 import org.junit.Assert.assertEquals 41 import org.junit.Assert.assertNotEquals 42 import org.junit.Assert.assertNull 43 import org.junit.Assert.assertTrue 44 import org.junit.Assert.assertThrows 45 import org.junit.Before 46 import org.junit.Rule 47 import org.junit.Test 48 import org.mockito.Mock 49 import org.mockito.Mockito 50 import org.mockito.junit.MockitoJUnit 51 import java.io.FileNotFoundException 52 import java.io.FileOutputStream 53 import java.io.IOException 54 import java.io.InputStream 55 56 private fun createKeyboard( 57 deviceId: Int, 58 vendorId: Int, 59 productId: Int, 60 languageTag: String, 61 layoutType: String 62 ): InputDevice = 63 InputDevice.Builder() 64 .setId(deviceId) 65 .setName("Device $deviceId") 66 .setDescriptor("descriptor $deviceId") 67 .setSources(InputDevice.SOURCE_KEYBOARD) 68 .setKeyboardType(InputDevice.KEYBOARD_TYPE_ALPHABETIC) 69 .setExternal(true) 70 .setVendorId(vendorId) 71 .setProductId(productId) 72 .setKeyboardLanguageTag(languageTag) 73 .setKeyboardLayoutType(layoutType) 74 .build() 75 76 /** 77 * Tests for {@link Default UI} and {@link New UI}. 78 * 79 * Build/Install/Run: 80 * atest FrameworksServicesTests:KeyboardLayoutManagerTests 81 */ 82 @Presubmit 83 class KeyboardLayoutManagerTests { 84 companion object { 85 const val DEVICE_ID = 1 86 const val VENDOR_SPECIFIC_DEVICE_ID = 2 87 const val ENGLISH_DVORAK_DEVICE_ID = 3 88 const val ENGLISH_QWERTY_DEVICE_ID = 4 89 const val DEFAULT_VENDOR_ID = 123 90 const val DEFAULT_PRODUCT_ID = 456 91 const val USER_ID = 4 92 const val IME_ID = "ime_id" 93 const val PACKAGE_NAME = "KeyboardLayoutManagerTests" 94 const val RECEIVER_NAME = "DummyReceiver" 95 private const val ENGLISH_US_LAYOUT_NAME = "keyboard_layout_english_us" 96 private const val ENGLISH_UK_LAYOUT_NAME = "keyboard_layout_english_uk" 97 private const val VENDOR_SPECIFIC_LAYOUT_NAME = "keyboard_layout_vendorId:1,productId:1" 98 } 99 100 private val ENGLISH_US_LAYOUT_DESCRIPTOR = createLayoutDescriptor(ENGLISH_US_LAYOUT_NAME) 101 private val ENGLISH_UK_LAYOUT_DESCRIPTOR = createLayoutDescriptor(ENGLISH_UK_LAYOUT_NAME) 102 private val VENDOR_SPECIFIC_LAYOUT_DESCRIPTOR = 103 createLayoutDescriptor(VENDOR_SPECIFIC_LAYOUT_NAME) 104 105 @get:Rule 106 val rule = MockitoJUnit.rule()!! 107 108 @Mock 109 private lateinit var iInputManager: IInputManager 110 111 @Mock 112 private lateinit var native: NativeInputManagerService 113 114 @Mock 115 private lateinit var packageManager: PackageManager 116 private lateinit var keyboardLayoutManager: KeyboardLayoutManager 117 118 private lateinit var imeInfo: InputMethodInfo 119 private var nextImeSubtypeId = 0 120 private lateinit var context: Context 121 private lateinit var dataStore: PersistentDataStore 122 private lateinit var testLooper: TestLooper 123 124 // Devices 125 private lateinit var keyboardDevice: InputDevice 126 private lateinit var vendorSpecificKeyboardDevice: InputDevice 127 private lateinit var englishDvorakKeyboardDevice: InputDevice 128 private lateinit var englishQwertyKeyboardDevice: InputDevice 129 130 @Before 131 fun setup() { 132 context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext())) 133 dataStore = PersistentDataStore(object : PersistentDataStore.Injector() { 134 override fun openRead(): InputStream? { 135 throw FileNotFoundException() 136 } 137 138 override fun startWrite(): FileOutputStream? { 139 throw IOException() 140 } 141 142 override fun finishWrite(fos: FileOutputStream?, success: Boolean) {} 143 }) 144 testLooper = TestLooper() 145 keyboardLayoutManager = KeyboardLayoutManager(context, native, dataStore, testLooper.looper) 146 setupInputDevices() 147 setupBroadcastReceiver() 148 setupIme() 149 } 150 151 private fun setupInputDevices() { 152 InputManagerGlobal.resetInstance(iInputManager) 153 val inputManager = InputManager(context) 154 Mockito.`when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))) 155 .thenReturn(inputManager) 156 157 keyboardDevice = createKeyboard(DEVICE_ID, DEFAULT_VENDOR_ID, DEFAULT_PRODUCT_ID, "", "") 158 vendorSpecificKeyboardDevice = createKeyboard(VENDOR_SPECIFIC_DEVICE_ID, 1, 1, "", "") 159 englishDvorakKeyboardDevice = createKeyboard(ENGLISH_DVORAK_DEVICE_ID, DEFAULT_VENDOR_ID, 160 DEFAULT_PRODUCT_ID, "en", "dvorak") 161 englishQwertyKeyboardDevice = createKeyboard(ENGLISH_QWERTY_DEVICE_ID, DEFAULT_VENDOR_ID, 162 DEFAULT_PRODUCT_ID, "en", "qwerty") 163 Mockito.`when`(iInputManager.inputDeviceIds) 164 .thenReturn(intArrayOf( 165 DEVICE_ID, 166 VENDOR_SPECIFIC_DEVICE_ID, 167 ENGLISH_DVORAK_DEVICE_ID, 168 ENGLISH_QWERTY_DEVICE_ID 169 )) 170 Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardDevice) 171 Mockito.`when`(iInputManager.getInputDevice(VENDOR_SPECIFIC_DEVICE_ID)) 172 .thenReturn(vendorSpecificKeyboardDevice) 173 Mockito.`when`(iInputManager.getInputDevice(ENGLISH_DVORAK_DEVICE_ID)) 174 .thenReturn(englishDvorakKeyboardDevice) 175 Mockito.`when`(iInputManager.getInputDevice(ENGLISH_QWERTY_DEVICE_ID)) 176 .thenReturn(englishQwertyKeyboardDevice) 177 } 178 179 private fun setupBroadcastReceiver() { 180 Mockito.`when`(context.packageManager).thenReturn(packageManager) 181 182 val info = createMockReceiver() 183 Mockito.`when`(packageManager.queryBroadcastReceiversAsUser(Mockito.any(), Mockito.anyInt(), 184 Mockito.anyInt())).thenReturn(listOf(info)) 185 Mockito.`when`(packageManager.getReceiverInfo(Mockito.any(), Mockito.anyInt())) 186 .thenReturn(info.activityInfo) 187 188 val resources = context.resources 189 Mockito.`when`( 190 packageManager.getResourcesForApplication( 191 Mockito.any( 192 ApplicationInfo::class.java 193 ) 194 ) 195 ).thenReturn(resources) 196 } 197 198 private fun setupIme() { 199 imeInfo = InputMethodInfo(PACKAGE_NAME, RECEIVER_NAME, "", "", 0) 200 } 201 202 @Test 203 fun testDefaultUi_getKeyboardLayouts() { 204 NewSettingsApiFlag(false).use { 205 val keyboardLayouts = keyboardLayoutManager.keyboardLayouts 206 assertNotEquals( 207 "Default UI: Keyboard layout API should not return empty array", 208 0, 209 keyboardLayouts.size 210 ) 211 assertTrue( 212 "Default UI: Keyboard layout API should provide English(US) layout", 213 hasLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) 214 ) 215 } 216 } 217 218 @Test 219 fun testNewUi_getKeyboardLayouts() { 220 NewSettingsApiFlag(true).use { 221 val keyboardLayouts = keyboardLayoutManager.keyboardLayouts 222 assertNotEquals( 223 "New UI: Keyboard layout API should not return empty array", 224 0, 225 keyboardLayouts.size 226 ) 227 assertTrue( 228 "New UI: Keyboard layout API should provide English(US) layout", 229 hasLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) 230 ) 231 } 232 } 233 234 @Test 235 fun testDefaultUi_getKeyboardLayoutsForInputDevice() { 236 NewSettingsApiFlag(false).use { 237 val keyboardLayouts = 238 keyboardLayoutManager.getKeyboardLayoutsForInputDevice(keyboardDevice.identifier) 239 assertNotEquals( 240 "Default UI: getKeyboardLayoutsForInputDevice API should not return empty array", 241 0, 242 keyboardLayouts.size 243 ) 244 assertTrue( 245 "Default UI: getKeyboardLayoutsForInputDevice API should provide English(US) " + 246 "layout", 247 hasLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) 248 ) 249 250 val vendorSpecificKeyboardLayouts = 251 keyboardLayoutManager.getKeyboardLayoutsForInputDevice( 252 vendorSpecificKeyboardDevice.identifier 253 ) 254 assertEquals( 255 "Default UI: getKeyboardLayoutsForInputDevice API should return only vendor " + 256 "specific layout", 257 1, 258 vendorSpecificKeyboardLayouts.size 259 ) 260 assertEquals( 261 "Default UI: getKeyboardLayoutsForInputDevice API should return vendor specific " + 262 "layout", 263 VENDOR_SPECIFIC_LAYOUT_DESCRIPTOR, 264 vendorSpecificKeyboardLayouts[0].descriptor 265 ) 266 } 267 } 268 269 @Test 270 fun testNewUi_getKeyboardLayoutsForInputDevice() { 271 NewSettingsApiFlag(true).use { 272 val keyboardLayouts = keyboardLayoutManager.keyboardLayouts 273 assertNotEquals( 274 "New UI: getKeyboardLayoutsForInputDevice API should not return empty array", 275 0, 276 keyboardLayouts.size 277 ) 278 assertTrue( 279 "New UI: getKeyboardLayoutsForInputDevice API should provide English(US) " + 280 "layout", 281 hasLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) 282 ) 283 } 284 } 285 286 @Test 287 fun testDefaultUi_getSetCurrentKeyboardLayoutForInputDevice() { 288 NewSettingsApiFlag(false).use { 289 assertNull( 290 "Default UI: getCurrentKeyboardLayoutForInputDevice API should return null if " + 291 "nothing was set", 292 keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice( 293 keyboardDevice.identifier 294 ) 295 ) 296 297 keyboardLayoutManager.setCurrentKeyboardLayoutForInputDevice( 298 keyboardDevice.identifier, 299 ENGLISH_US_LAYOUT_DESCRIPTOR 300 ) 301 val keyboardLayout = 302 keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice( 303 keyboardDevice.identifier 304 ) 305 assertEquals( 306 "Default UI: getCurrentKeyboardLayoutForInputDevice API should return the set " + 307 "layout", 308 ENGLISH_US_LAYOUT_DESCRIPTOR, 309 keyboardLayout 310 ) 311 } 312 } 313 314 @Test 315 fun testNewUi_getSetCurrentKeyboardLayoutForInputDevice() { 316 NewSettingsApiFlag(true).use { 317 keyboardLayoutManager.setCurrentKeyboardLayoutForInputDevice( 318 keyboardDevice.identifier, 319 ENGLISH_US_LAYOUT_DESCRIPTOR 320 ) 321 assertNull( 322 "New UI: getCurrentKeyboardLayoutForInputDevice API should always return null " + 323 "even after setCurrentKeyboardLayoutForInputDevice", 324 keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice( 325 keyboardDevice.identifier 326 ) 327 ) 328 } 329 } 330 331 @Test 332 fun testDefaultUi_getEnabledKeyboardLayoutsForInputDevice() { 333 NewSettingsApiFlag(false).use { 334 keyboardLayoutManager.addKeyboardLayoutForInputDevice( 335 keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR 336 ) 337 338 val keyboardLayouts = 339 keyboardLayoutManager.getEnabledKeyboardLayoutsForInputDevice( 340 keyboardDevice.identifier 341 ) 342 assertEquals( 343 "Default UI: getEnabledKeyboardLayoutsForInputDevice API should return added " + 344 "layout", 345 1, 346 keyboardLayouts.size 347 ) 348 assertEquals( 349 "Default UI: getEnabledKeyboardLayoutsForInputDevice API should return " + 350 "English(US) layout", 351 ENGLISH_US_LAYOUT_DESCRIPTOR, 352 keyboardLayouts[0] 353 ) 354 assertEquals( 355 "Default UI: getCurrentKeyboardLayoutForInputDevice API should return " + 356 "English(US) layout (Auto select the first enabled layout)", 357 ENGLISH_US_LAYOUT_DESCRIPTOR, 358 keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice( 359 keyboardDevice.identifier 360 ) 361 ) 362 363 keyboardLayoutManager.removeKeyboardLayoutForInputDevice( 364 keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR 365 ) 366 assertEquals( 367 "Default UI: getKeyboardLayoutsForInputDevice API should return 0 layouts", 368 0, 369 keyboardLayoutManager.getEnabledKeyboardLayoutsForInputDevice( 370 keyboardDevice.identifier 371 ).size 372 ) 373 assertNull( 374 "Default UI: getCurrentKeyboardLayoutForInputDevice API should return null after " + 375 "the enabled layout is removed", 376 keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice( 377 keyboardDevice.identifier 378 ) 379 ) 380 } 381 } 382 383 @Test 384 fun testNewUi_getEnabledKeyboardLayoutsForInputDevice() { 385 NewSettingsApiFlag(true).use { 386 keyboardLayoutManager.addKeyboardLayoutForInputDevice( 387 keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR 388 ) 389 390 assertEquals( 391 "New UI: getEnabledKeyboardLayoutsForInputDevice API should return always return " + 392 "an empty array", 393 0, 394 keyboardLayoutManager.getEnabledKeyboardLayoutsForInputDevice( 395 keyboardDevice.identifier 396 ).size 397 ) 398 assertNull( 399 "New UI: getCurrentKeyboardLayoutForInputDevice API should always return null", 400 keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice( 401 keyboardDevice.identifier 402 ) 403 ) 404 } 405 } 406 407 @Test 408 fun testDefaultUi_switchKeyboardLayout() { 409 NewSettingsApiFlag(false).use { 410 keyboardLayoutManager.addKeyboardLayoutForInputDevice( 411 keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR 412 ) 413 keyboardLayoutManager.addKeyboardLayoutForInputDevice( 414 keyboardDevice.identifier, ENGLISH_UK_LAYOUT_DESCRIPTOR 415 ) 416 assertEquals( 417 "Default UI: getCurrentKeyboardLayoutForInputDevice API should return " + 418 "English(US) layout", 419 ENGLISH_US_LAYOUT_DESCRIPTOR, 420 keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice( 421 keyboardDevice.identifier 422 ) 423 ) 424 425 keyboardLayoutManager.switchKeyboardLayout(DEVICE_ID, 1) 426 427 // Throws null pointer because trying to show toast using TestLooper 428 assertThrows(NullPointerException::class.java) { testLooper.dispatchAll() } 429 assertEquals("Default UI: getCurrentKeyboardLayoutForInputDevice API should return " + 430 "English(UK) layout", 431 ENGLISH_UK_LAYOUT_DESCRIPTOR, 432 keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice( 433 keyboardDevice.identifier 434 ) 435 ) 436 } 437 } 438 439 @Test 440 fun testNewUi_switchKeyboardLayout() { 441 NewSettingsApiFlag(true).use { 442 keyboardLayoutManager.addKeyboardLayoutForInputDevice( 443 keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR 444 ) 445 keyboardLayoutManager.addKeyboardLayoutForInputDevice( 446 keyboardDevice.identifier, ENGLISH_UK_LAYOUT_DESCRIPTOR 447 ) 448 449 keyboardLayoutManager.switchKeyboardLayout(DEVICE_ID, 1) 450 testLooper.dispatchAll() 451 452 assertNull("New UI: getCurrentKeyboardLayoutForInputDevice API should always return " + 453 "null", 454 keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice( 455 keyboardDevice.identifier 456 ) 457 ) 458 } 459 } 460 461 @Test 462 fun testDefaultUi_getKeyboardLayout() { 463 NewSettingsApiFlag(false).use { 464 val keyboardLayout = 465 keyboardLayoutManager.getKeyboardLayout(ENGLISH_US_LAYOUT_DESCRIPTOR) 466 assertEquals("Default UI: getKeyboardLayout API should return correct Layout from " + 467 "available layouts", 468 ENGLISH_US_LAYOUT_DESCRIPTOR, 469 keyboardLayout!!.descriptor 470 ) 471 } 472 } 473 474 @Test 475 fun testNewUi_getKeyboardLayout() { 476 NewSettingsApiFlag(true).use { 477 val keyboardLayout = 478 keyboardLayoutManager.getKeyboardLayout(ENGLISH_US_LAYOUT_DESCRIPTOR) 479 assertEquals("New UI: getKeyboardLayout API should return correct Layout from " + 480 "available layouts", 481 ENGLISH_US_LAYOUT_DESCRIPTOR, 482 keyboardLayout!!.descriptor 483 ) 484 } 485 } 486 487 @Test 488 fun testDefaultUi_getSetKeyboardLayoutForInputDevice_WithImeInfo() { 489 NewSettingsApiFlag(false).use { 490 val imeSubtype = createImeSubtype() 491 keyboardLayoutManager.setKeyboardLayoutForInputDevice( 492 keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype, 493 ENGLISH_UK_LAYOUT_DESCRIPTOR 494 ) 495 val keyboardLayout = 496 keyboardLayoutManager.getKeyboardLayoutForInputDevice( 497 keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype 498 ) 499 assertNull( 500 "Default UI: getKeyboardLayoutForInputDevice API should always return null", 501 keyboardLayout 502 ) 503 } 504 } 505 506 @Test 507 fun testNewUi_getSetKeyboardLayoutForInputDevice_withImeInfo() { 508 NewSettingsApiFlag(true).use { 509 val imeSubtype = createImeSubtype() 510 511 keyboardLayoutManager.setKeyboardLayoutForInputDevice( 512 keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype, 513 ENGLISH_UK_LAYOUT_DESCRIPTOR 514 ) 515 assertEquals( 516 "New UI: getKeyboardLayoutForInputDevice API should return the set layout", 517 ENGLISH_UK_LAYOUT_DESCRIPTOR, 518 keyboardLayoutManager.getKeyboardLayoutForInputDevice( 519 keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype 520 ) 521 ) 522 523 // This should replace previously set layout 524 keyboardLayoutManager.setKeyboardLayoutForInputDevice( 525 keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype, 526 ENGLISH_US_LAYOUT_DESCRIPTOR 527 ) 528 assertEquals( 529 "New UI: getKeyboardLayoutForInputDevice API should return the last set layout", 530 ENGLISH_US_LAYOUT_DESCRIPTOR, 531 keyboardLayoutManager.getKeyboardLayoutForInputDevice( 532 keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype 533 ) 534 ) 535 } 536 } 537 538 @Test 539 fun testDefaultUi_getKeyboardLayoutListForInputDevice() { 540 NewSettingsApiFlag(false).use { 541 val keyboardLayouts = 542 keyboardLayoutManager.getKeyboardLayoutListForInputDevice( 543 keyboardDevice.identifier, USER_ID, imeInfo, 544 createImeSubtype() 545 ) 546 assertEquals("Default UI: getKeyboardLayoutListForInputDevice API should always " + 547 "return empty array", 548 0, 549 keyboardLayouts.size 550 ) 551 } 552 } 553 554 @Test 555 fun testNewUi_getKeyboardLayoutListForInputDevice() { 556 NewSettingsApiFlag(true).use { 557 // Check Layouts for "hi-Latn". It should return all 'Latn' keyboard layouts 558 var keyboardLayouts = 559 keyboardLayoutManager.getKeyboardLayoutListForInputDevice( 560 keyboardDevice.identifier, USER_ID, imeInfo, 561 createImeSubtypeForLanguageTag("hi-Latn") 562 ) 563 assertNotEquals( 564 "New UI: getKeyboardLayoutListForInputDevice API should return the list of " + 565 "supported layouts with matching script code", 566 0, 567 keyboardLayouts.size 568 ) 569 assertTrue("New UI: getKeyboardLayoutListForInputDevice API should return a list " + 570 "containing English(US) layout for hi-Latn", 571 containsLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) 572 ) 573 assertTrue("New UI: getKeyboardLayoutListForInputDevice API should return a list " + 574 "containing English(No script code) layout for hi-Latn", 575 containsLayout( 576 keyboardLayouts, 577 createLayoutDescriptor("keyboard_layout_english_without_script_code") 578 ) 579 ) 580 581 // Check Layouts for "hi" which by default uses 'Deva' script. 582 keyboardLayouts = 583 keyboardLayoutManager.getKeyboardLayoutListForInputDevice( 584 keyboardDevice.identifier, USER_ID, imeInfo, 585 createImeSubtypeForLanguageTag("hi") 586 ) 587 assertEquals("New UI: getKeyboardLayoutListForInputDevice API should return empty " + 588 "list if no supported layouts available", 589 0, 590 keyboardLayouts.size 591 ) 592 593 // If user manually selected some layout, always provide it in the layout list 594 val imeSubtype = createImeSubtypeForLanguageTag("hi") 595 keyboardLayoutManager.setKeyboardLayoutForInputDevice( 596 keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype, 597 ENGLISH_US_LAYOUT_DESCRIPTOR 598 ) 599 keyboardLayouts = 600 keyboardLayoutManager.getKeyboardLayoutListForInputDevice( 601 keyboardDevice.identifier, USER_ID, imeInfo, 602 imeSubtype 603 ) 604 assertEquals("New UI: getKeyboardLayoutListForInputDevice API should return user " + 605 "selected layout even if the script is incompatible with IME", 606 1, 607 keyboardLayouts.size 608 ) 609 610 // Special case Japanese: UScript ignores provided script code for certain language tags 611 // Should manually match provided script codes and then rely on Uscript to derive 612 // script from language tags and match those. 613 keyboardLayouts = 614 keyboardLayoutManager.getKeyboardLayoutListForInputDevice( 615 keyboardDevice.identifier, USER_ID, imeInfo, 616 createImeSubtypeForLanguageTag("ja-Latn-JP") 617 ) 618 assertNotEquals( 619 "New UI: getKeyboardLayoutListForInputDevice API should return the list of " + 620 "supported layouts with matching script code for ja-Latn-JP", 621 0, 622 keyboardLayouts.size 623 ) 624 assertTrue("New UI: getKeyboardLayoutListForInputDevice API should return a list " + 625 "containing English(US) layout for ja-Latn-JP", 626 containsLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) 627 ) 628 assertTrue("New UI: getKeyboardLayoutListForInputDevice API should return a list " + 629 "containing English(No script code) layout for ja-Latn-JP", 630 containsLayout( 631 keyboardLayouts, 632 createLayoutDescriptor("keyboard_layout_english_without_script_code") 633 ) 634 ) 635 636 // If script code not explicitly provided for Japanese should rely on Uscript to find 637 // derived script code and hence no suitable layout will be found. 638 keyboardLayouts = 639 keyboardLayoutManager.getKeyboardLayoutListForInputDevice( 640 keyboardDevice.identifier, USER_ID, imeInfo, 641 createImeSubtypeForLanguageTag("ja-JP") 642 ) 643 assertEquals( 644 "New UI: getKeyboardLayoutListForInputDevice API should return empty list of " + 645 "supported layouts with matching script code for ja-JP", 646 0, 647 keyboardLayouts.size 648 ) 649 650 // If IME doesn't have a corresponding language tag, then should show all available 651 // layouts no matter the script code. 652 keyboardLayouts = 653 keyboardLayoutManager.getKeyboardLayoutListForInputDevice( 654 keyboardDevice.identifier, USER_ID, imeInfo, null 655 ) 656 assertNotEquals( 657 "New UI: getKeyboardLayoutListForInputDevice API should return all layouts if" + 658 "language tag or subtype not provided", 659 0, 660 keyboardLayouts.size 661 ) 662 assertTrue("New UI: getKeyboardLayoutListForInputDevice API should contain Latin " + 663 "layouts if language tag or subtype not provided", 664 containsLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) 665 ) 666 assertTrue("New UI: getKeyboardLayoutListForInputDevice API should contain Cyrillic " + 667 "layouts if language tag or subtype not provided", 668 containsLayout( 669 keyboardLayouts, 670 createLayoutDescriptor("keyboard_layout_russian") 671 ) 672 ) 673 } 674 } 675 676 @Test 677 fun testNewUi_getDefaultKeyboardLayoutForInputDevice_withImeLanguageTag() { 678 NewSettingsApiFlag(true).use { 679 assertCorrectLayout( 680 keyboardDevice, 681 createImeSubtypeForLanguageTag("en-US"), 682 ENGLISH_US_LAYOUT_DESCRIPTOR 683 ) 684 assertCorrectLayout( 685 keyboardDevice, 686 createImeSubtypeForLanguageTag("en-GB"), 687 ENGLISH_UK_LAYOUT_DESCRIPTOR 688 ) 689 assertCorrectLayout( 690 keyboardDevice, 691 createImeSubtypeForLanguageTag("de"), 692 createLayoutDescriptor("keyboard_layout_german") 693 ) 694 assertCorrectLayout( 695 keyboardDevice, 696 createImeSubtypeForLanguageTag("fr-FR"), 697 createLayoutDescriptor("keyboard_layout_french") 698 ) 699 assertCorrectLayout( 700 keyboardDevice, 701 createImeSubtypeForLanguageTag("ru"), 702 createLayoutDescriptor("keyboard_layout_russian") 703 ) 704 assertNull( 705 "New UI: getDefaultKeyboardLayoutForInputDevice should return null when no " + 706 "layout available", 707 keyboardLayoutManager.getKeyboardLayoutForInputDevice( 708 keyboardDevice.identifier, USER_ID, imeInfo, 709 createImeSubtypeForLanguageTag("it") 710 ) 711 ) 712 assertNull( 713 "New UI: getDefaultKeyboardLayoutForInputDevice should return null when no " + 714 "layout for script code is available", 715 keyboardLayoutManager.getKeyboardLayoutForInputDevice( 716 keyboardDevice.identifier, USER_ID, imeInfo, 717 createImeSubtypeForLanguageTag("en-Deva") 718 ) 719 ) 720 } 721 } 722 723 @Test 724 fun testNewUi_getDefaultKeyboardLayoutForInputDevice_withImeLanguageTagAndLayoutType() { 725 NewSettingsApiFlag(true).use { 726 assertCorrectLayout( 727 keyboardDevice, 728 createImeSubtypeForLanguageTagAndLayoutType("en-US", "qwerty"), 729 ENGLISH_US_LAYOUT_DESCRIPTOR 730 ) 731 assertCorrectLayout( 732 keyboardDevice, 733 createImeSubtypeForLanguageTagAndLayoutType("en-US", "dvorak"), 734 createLayoutDescriptor("keyboard_layout_english_us_dvorak") 735 ) 736 // Try to match layout type even if country doesn't match 737 assertCorrectLayout( 738 keyboardDevice, 739 createImeSubtypeForLanguageTagAndLayoutType("en-GB", "dvorak"), 740 createLayoutDescriptor("keyboard_layout_english_us_dvorak") 741 ) 742 // Choose layout based on layout type priority, if layout type is not provided by IME 743 // (Qwerty > Dvorak > Extended) 744 assertCorrectLayout( 745 keyboardDevice, 746 createImeSubtypeForLanguageTagAndLayoutType("en-US", ""), 747 ENGLISH_US_LAYOUT_DESCRIPTOR 748 ) 749 assertCorrectLayout( 750 keyboardDevice, 751 createImeSubtypeForLanguageTagAndLayoutType("en-GB", "qwerty"), 752 ENGLISH_UK_LAYOUT_DESCRIPTOR 753 ) 754 assertCorrectLayout( 755 keyboardDevice, 756 createImeSubtypeForLanguageTagAndLayoutType("de", "qwertz"), 757 createLayoutDescriptor("keyboard_layout_german") 758 ) 759 // Wrong layout type should match with language if provided layout type not available 760 assertCorrectLayout( 761 keyboardDevice, 762 createImeSubtypeForLanguageTagAndLayoutType("de", "qwerty"), 763 createLayoutDescriptor("keyboard_layout_german") 764 ) 765 assertCorrectLayout( 766 keyboardDevice, 767 createImeSubtypeForLanguageTagAndLayoutType("fr-FR", "azerty"), 768 createLayoutDescriptor("keyboard_layout_french") 769 ) 770 assertCorrectLayout( 771 keyboardDevice, 772 createImeSubtypeForLanguageTagAndLayoutType("ru", "qwerty"), 773 createLayoutDescriptor("keyboard_layout_russian_qwerty") 774 ) 775 // If layout type is empty then prioritize KCM with empty layout type 776 assertCorrectLayout( 777 keyboardDevice, 778 createImeSubtypeForLanguageTagAndLayoutType("ru", ""), 779 createLayoutDescriptor("keyboard_layout_russian") 780 ) 781 assertNull("New UI: getDefaultKeyboardLayoutForInputDevice should return null when " + 782 "no layout for script code is available", 783 keyboardLayoutManager.getKeyboardLayoutForInputDevice( 784 keyboardDevice.identifier, USER_ID, imeInfo, 785 createImeSubtypeForLanguageTagAndLayoutType("en-Deva-US", "") 786 ) 787 ) 788 } 789 } 790 791 @Test 792 fun testNewUi_getDefaultKeyboardLayoutForInputDevice_withHwLanguageTagAndLayoutType() { 793 NewSettingsApiFlag(true).use { 794 val frenchSubtype = createImeSubtypeForLanguageTagAndLayoutType("fr", "azerty") 795 // Should return English dvorak even if IME current layout is French, since HW says the 796 // keyboard is a Dvorak keyboard 797 assertCorrectLayout( 798 englishDvorakKeyboardDevice, 799 frenchSubtype, 800 createLayoutDescriptor("keyboard_layout_english_us_dvorak") 801 ) 802 803 // Back to back changing HW keyboards with same product and vendor ID but different 804 // language and layout type should configure the layouts correctly. 805 assertCorrectLayout( 806 englishQwertyKeyboardDevice, 807 frenchSubtype, 808 createLayoutDescriptor("keyboard_layout_english_us") 809 ) 810 811 // Fallback to IME information if the HW provided layout script is incompatible with the 812 // provided IME subtype 813 assertCorrectLayout( 814 englishDvorakKeyboardDevice, 815 createImeSubtypeForLanguageTagAndLayoutType("ru", ""), 816 createLayoutDescriptor("keyboard_layout_russian") 817 ) 818 } 819 } 820 821 private fun assertCorrectLayout( 822 device: InputDevice, 823 imeSubtype: InputMethodSubtype, 824 expectedLayout: String 825 ) { 826 assertEquals( 827 "New UI: getDefaultKeyboardLayoutForInputDevice should return $expectedLayout", 828 expectedLayout, 829 keyboardLayoutManager.getKeyboardLayoutForInputDevice( 830 device.identifier, USER_ID, imeInfo, imeSubtype 831 ) 832 ) 833 } 834 835 private fun createImeSubtype(): InputMethodSubtype = 836 InputMethodSubtype.InputMethodSubtypeBuilder().setSubtypeId(nextImeSubtypeId++).build() 837 838 private fun createImeSubtypeForLanguageTag(languageTag: String): InputMethodSubtype = 839 InputMethodSubtype.InputMethodSubtypeBuilder().setSubtypeId(nextImeSubtypeId++) 840 .setLanguageTag(languageTag).build() 841 842 private fun createImeSubtypeForLanguageTagAndLayoutType( 843 languageTag: String, 844 layoutType: String 845 ): InputMethodSubtype = 846 InputMethodSubtype.InputMethodSubtypeBuilder().setSubtypeId(nextImeSubtypeId++) 847 .setPhysicalKeyboardHint(ULocale.forLanguageTag(languageTag), layoutType).build() 848 849 private fun hasLayout(layoutList: Array<KeyboardLayout>, layoutDesc: String): Boolean { 850 for (kl in layoutList) { 851 if (kl.descriptor == layoutDesc) { 852 return true 853 } 854 } 855 return false 856 } 857 858 private fun createLayoutDescriptor(keyboardName: String): String = 859 "$PACKAGE_NAME/$RECEIVER_NAME/$keyboardName" 860 861 private fun containsLayout(layoutList: Array<KeyboardLayout>, layoutDesc: String): Boolean { 862 for (kl in layoutList) { 863 if (kl.descriptor.equals(layoutDesc)) { 864 return true 865 } 866 } 867 return false 868 } 869 870 private fun createMockReceiver(): ResolveInfo { 871 val info = ResolveInfo() 872 info.activityInfo = ActivityInfo() 873 info.activityInfo.packageName = PACKAGE_NAME 874 info.activityInfo.name = RECEIVER_NAME 875 info.activityInfo.applicationInfo = ApplicationInfo() 876 info.activityInfo.metaData = Bundle() 877 info.activityInfo.metaData.putInt( 878 InputManager.META_DATA_KEYBOARD_LAYOUTS, 879 R.xml.keyboard_layouts 880 ) 881 info.serviceInfo = ServiceInfo() 882 info.serviceInfo.packageName = PACKAGE_NAME 883 info.serviceInfo.name = RECEIVER_NAME 884 return info 885 } 886 887 private inner class NewSettingsApiFlag constructor(enabled: Boolean) : AutoCloseable { 888 init { 889 Settings.Global.putString( 890 context.contentResolver, 891 "settings_new_keyboard_ui", enabled.toString() 892 ) 893 } 894 895 override fun close() { 896 Settings.Global.putString( 897 context.contentResolver, 898 "settings_new_keyboard_ui", 899 "" 900 ) 901 } 902 } 903 } 904