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