• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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.animation.ValueAnimator
20 import android.content.Context
21 import android.content.ContextWrapper
22 import android.content.res.Resources
23 import android.graphics.Color
24 import android.hardware.input.IKeyboardBacklightListener
25 import android.hardware.input.IKeyboardBacklightState
26 import android.hardware.input.InputManager
27 import android.hardware.lights.Light
28 import android.os.SystemProperties
29 import android.os.test.TestLooper
30 import android.platform.test.annotations.Presubmit
31 import android.util.TypedValue
32 import android.view.InputDevice
33 import androidx.test.annotation.UiThreadTest
34 import androidx.test.core.app.ApplicationProvider
35 import com.android.dx.mockito.inline.extended.ExtendedMockito
36 import com.android.internal.R
37 import com.android.modules.utils.testing.ExtendedMockitoRule
38 import com.android.server.input.KeyboardBacklightController.DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL
39 import com.android.server.input.KeyboardBacklightController.MAX_BRIGHTNESS_CHANGE_STEPS
40 import com.android.test.input.MockInputManagerRule
41 import org.junit.Assert.assertEquals
42 import org.junit.Assert.assertNotEquals
43 import org.junit.Assert.assertNotNull
44 import org.junit.Assert.assertNull
45 import org.junit.Assert.assertTrue
46 import org.junit.Before
47 import org.junit.Rule
48 import org.junit.Test
49 import org.mockito.Mock
50 import org.mockito.Mockito.any
51 import org.mockito.Mockito.anyBoolean
52 import org.mockito.Mockito.anyInt
53 import org.mockito.Mockito.eq
54 import org.mockito.Mockito.spy
55 import org.mockito.Mockito.`when`
56 
createKeyboardnull57 private fun createKeyboard(deviceId: Int): InputDevice =
58     InputDevice.Builder()
59         .setId(deviceId)
60         .setName("Device $deviceId")
61         .setDescriptor("descriptor $deviceId")
62         .setSources(InputDevice.SOURCE_KEYBOARD)
63         .setKeyboardType(InputDevice.KEYBOARD_TYPE_ALPHABETIC)
64         .setExternal(true)
65         .build()
66 
67 private fun createLight(lightId: Int, lightType: Int): Light = createLight(lightId, lightType, null)
68 
69 private fun createLight(lightId: Int, lightType: Int, suggestedBrightnessLevels: IntArray?): Light =
70     Light(
71         lightId,
72         "Light $lightId",
73         1,
74         lightType,
75         Light.LIGHT_CAPABILITY_BRIGHTNESS,
76         suggestedBrightnessLevels,
77     )
78 
79 /**
80  * Tests for {@link KeyboardBacklightController}.
81  *
82  * Build/Install/Run: atest InputTests:KeyboardBacklightControllerTests
83  */
84 @Presubmit
85 class KeyboardBacklightControllerTests {
86     companion object {
87         const val DEVICE_ID = 1
88         const val LIGHT_ID = 2
89         const val SECOND_LIGHT_ID = 3
90         const val MAX_BRIGHTNESS = 255
91         const val USER_INACTIVITY_THRESHOLD_MILLIS = 30000
92     }
93 
94     @get:Rule
95     val extendedMockitoRule =
96         ExtendedMockitoRule.Builder(this).mockStatic(SystemProperties::class.java).build()!!
97     @get:Rule val inputManagerRule = MockInputManagerRule()
98 
99     @Mock private lateinit var native: NativeInputManagerService
100     @Mock private lateinit var resources: Resources
101     private lateinit var keyboardBacklightController: KeyboardBacklightController
102     private lateinit var context: Context
103     private lateinit var testLooper: TestLooper
104     private var lightColorMap: HashMap<Int, Int> = HashMap()
105     private var lastBacklightState: KeyboardBacklightState? = null
106     private var lastAnimationValues = IntArray(2)
107 
108     @Before
109     fun setup() {
110         context = spy(ContextWrapper(ApplicationProvider.getApplicationContext()))
111         `when`(context.resources).thenReturn(resources)
112         testLooper = TestLooper()
113         setupConfig()
114         val inputManager = InputManager(context)
115         `when`(context.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(inputManager)
116         `when`(inputManagerRule.mock.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID))
117         `when`(native.setLightColor(anyInt(), anyInt(), anyInt())).then {
118             val args = it.arguments
119             lightColorMap.put(args[1] as Int, args[2] as Int)
120         }
121         `when`(native.getLightColor(anyInt(), anyInt())).thenAnswer {
122             val args = it.arguments
123             lightColorMap.getOrDefault(args[1] as Int, 0)
124         }
125         lightColorMap.clear()
126     }
127 
128     private fun setupConfig() {
129         val brightnessValues = intArrayOf(100, 200, 0)
130         val decreaseThresholds = intArrayOf(-1, 900, 1900)
131         val increaseThresholds = intArrayOf(1000, 2000, -1)
132         `when`(resources.getIntArray(R.array.config_autoKeyboardBacklightBrightnessValues))
133             .thenReturn(brightnessValues)
134         `when`(resources.getIntArray(R.array.config_autoKeyboardBacklightDecreaseLuxThreshold))
135             .thenReturn(decreaseThresholds)
136         `when`(resources.getIntArray(R.array.config_autoKeyboardBacklightIncreaseLuxThreshold))
137             .thenReturn(increaseThresholds)
138         `when`(resources.getInteger(R.integer.config_keyboardBacklightTimeoutMs))
139             .thenReturn(USER_INACTIVITY_THRESHOLD_MILLIS)
140         `when`(
141                 resources.getValue(
142                     eq(R.dimen.config_autoKeyboardBrightnessSmoothingConstant),
143                     any(TypedValue::class.java),
144                     anyBoolean(),
145                 )
146             )
147             .then {
148                 val args = it.arguments
149                 val outValue = args[1] as TypedValue
150                 outValue.data = java.lang.Float.floatToRawIntBits(1.0f)
151                 Unit
152             }
153     }
154 
155     private fun setupController() {
156         keyboardBacklightController =
157             KeyboardBacklightController(context, native, testLooper.looper, FakeAnimatorFactory())
158     }
159 
160     @Test
161     fun testKeyboardBacklightIncrementDecrement() {
162         setupController()
163         val keyboardWithBacklight = createKeyboard(DEVICE_ID)
164         val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
165         `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
166         `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
167         keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
168 
169         assertIncrementDecrementForLevels(
170             keyboardWithBacklight,
171             keyboardBacklight,
172             DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL,
173         )
174     }
175 
176     @Test
177     fun testKeyboardWithoutBacklight() {
178         setupController()
179         val keyboardWithoutBacklight = createKeyboard(DEVICE_ID)
180         val keyboardInputLight = createLight(LIGHT_ID, Light.LIGHT_TYPE_INPUT)
181         `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithoutBacklight)
182         `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardInputLight))
183         keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
184 
185         incrementKeyboardBacklight(DEVICE_ID)
186         assertTrue("Non Keyboard backlights should not change", lightColorMap.isEmpty())
187     }
188 
189     @Test
190     fun testKeyboardWithMultipleLight() {
191         setupController()
192         val keyboardWithBacklight = createKeyboard(DEVICE_ID)
193         val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
194         val keyboardInputLight = createLight(SECOND_LIGHT_ID, Light.LIGHT_TYPE_INPUT)
195         `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
196         `when`(inputManagerRule.mock.getLights(DEVICE_ID))
197             .thenReturn(listOf(keyboardBacklight, keyboardInputLight))
198         keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
199 
200         incrementKeyboardBacklight(DEVICE_ID)
201         assertEquals("Only keyboard backlights should change", 1, lightColorMap.size)
202         assertNotNull("Keyboard backlight should change", lightColorMap[LIGHT_ID])
203         assertNull("Input lights should not change", lightColorMap[SECOND_LIGHT_ID])
204     }
205 
206     @Test
207     fun testKeyboardBacklight_registerUnregisterListener() {
208         setupController()
209         val keyboardWithBacklight = createKeyboard(DEVICE_ID)
210         val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
211         val maxLevel = DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL.size - 1
212         `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
213         `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
214         keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
215 
216         // Register backlight listener
217         val listener = KeyboardBacklightListener()
218         keyboardBacklightController.registerKeyboardBacklightListener(listener, 0)
219 
220         lastBacklightState = null
221         keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID)
222         testLooper.dispatchNext()
223 
224         assertEquals(
225             "Backlight state device Id should be $DEVICE_ID",
226             DEVICE_ID,
227             lastBacklightState!!.deviceId,
228         )
229         assertEquals(
230             "Backlight state brightnessLevel should be 1",
231             1,
232             lastBacklightState!!.brightnessLevel,
233         )
234         assertEquals(
235             "Backlight state maxBrightnessLevel should be $maxLevel",
236             maxLevel,
237             lastBacklightState!!.maxBrightnessLevel,
238         )
239         assertEquals(
240             "Backlight state isTriggeredByKeyPress should be true",
241             true,
242             lastBacklightState!!.isTriggeredByKeyPress,
243         )
244 
245         // Unregister listener
246         keyboardBacklightController.unregisterKeyboardBacklightListener(listener, 0)
247 
248         lastBacklightState = null
249         incrementKeyboardBacklight(DEVICE_ID)
250 
251         assertNull("Listener should not receive any updates", lastBacklightState)
252     }
253 
254     @Test
255     fun testKeyboardBacklight_userActivity() {
256         setupController()
257         val keyboardWithBacklight = createKeyboard(DEVICE_ID)
258         val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
259         `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
260         `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
261         keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
262         incrementKeyboardBacklight(DEVICE_ID)
263         assertNotEquals(
264             "Keyboard backlight level should be incremented to a non-zero value",
265             0,
266             lightColorMap[LIGHT_ID],
267         )
268 
269         testLooper.moveTimeForward((USER_INACTIVITY_THRESHOLD_MILLIS + 1000).toLong())
270         testLooper.dispatchNext()
271         assertEquals(
272             "Keyboard backlight level should be turned off after inactivity",
273             0,
274             lightColorMap[LIGHT_ID],
275         )
276     }
277 
278     @Test
279     fun testKeyboardBacklight_displayOnOff() {
280         setupController()
281         val keyboardWithBacklight = createKeyboard(DEVICE_ID)
282         val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
283         `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
284         `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
285         keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
286         incrementKeyboardBacklight(DEVICE_ID)
287 
288         val currentValue = lightColorMap[LIGHT_ID]
289         assertNotEquals(
290             "Keyboard backlight level should be incremented to a non-zero value",
291             0,
292             lightColorMap[LIGHT_ID],
293         )
294 
295         keyboardBacklightController.handleInteractiveStateChange(false /* isDisplayOn */)
296         assertEquals(
297             "Keyboard backlight level should be turned off after display is turned off",
298             0,
299             lightColorMap[LIGHT_ID],
300         )
301 
302         keyboardBacklightController.handleInteractiveStateChange(true /* isDisplayOn */)
303         assertEquals(
304             "Keyboard backlight level should be turned on after display is turned on",
305             currentValue,
306             lightColorMap[LIGHT_ID],
307         )
308     }
309 
310     @Test
311     @UiThreadTest
312     fun testKeyboardBacklightAnimation_onChangeLevels() {
313         ExtendedMockito.doReturn("true").`when` {
314             SystemProperties.get(eq("persist.input.keyboard.backlight_animation.enabled"))
315         }
316         setupController()
317         val keyboardWithBacklight = createKeyboard(DEVICE_ID)
318         val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
319         `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
320         `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
321         keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
322 
323         incrementKeyboardBacklight(DEVICE_ID)
324         assertEquals(
325             "Should start animation from level 0",
326             DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[0],
327             lastAnimationValues[0],
328         )
329         assertEquals(
330             "Should start animation to level 1",
331             DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[1],
332             lastAnimationValues[1],
333         )
334     }
335 
336     @Test
337     fun testKeyboardBacklightPreferredLevels() {
338         setupController()
339         val keyboardWithBacklight = createKeyboard(DEVICE_ID)
340         val suggestedLevels = intArrayOf(0, 22, 63, 135, 196, 255)
341         val keyboardBacklight =
342             createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, suggestedLevels)
343         `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
344         `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
345         keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
346 
347         assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight, suggestedLevels)
348     }
349 
350     @Test
351     fun testKeyboardBacklightPreferredLevels_moreThanMax_shouldUseDefault() {
352         setupController()
353         val keyboardWithBacklight = createKeyboard(DEVICE_ID)
354         val suggestedLevels = IntArray(MAX_BRIGHTNESS_CHANGE_STEPS + 1) { 10 * (it + 1) }
355         val keyboardBacklight =
356             createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, suggestedLevels)
357         `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
358         `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
359         keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
360 
361         assertIncrementDecrementForLevels(
362             keyboardWithBacklight,
363             keyboardBacklight,
364             DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL,
365         )
366     }
367 
368     @Test
369     fun testKeyboardBacklightPreferredLevels_mustHaveZeroAndMaxBrightnessAsBounds() {
370         setupController()
371         val keyboardWithBacklight = createKeyboard(DEVICE_ID)
372         val suggestedLevels = intArrayOf(22, 63, 135, 196)
373         val keyboardBacklight =
374             createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, suggestedLevels)
375         `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
376         `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
377         keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
378 
379         // Framework will add the lowest and maximum levels if not provided via config
380         assertIncrementDecrementForLevels(
381             keyboardWithBacklight,
382             keyboardBacklight,
383             intArrayOf(0, 22, 63, 135, 196, 255),
384         )
385     }
386 
387     @Test
388     fun testKeyboardBacklightPreferredLevels_dropsOutOfBoundsLevels() {
389         setupController()
390         val keyboardWithBacklight = createKeyboard(DEVICE_ID)
391         val suggestedLevels = intArrayOf(22, 63, 135, 400, 196, 1000)
392         val keyboardBacklight =
393             createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, suggestedLevels)
394         `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
395         `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
396         keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
397 
398         // Framework will drop out of bound levels in the config
399         assertIncrementDecrementForLevels(
400             keyboardWithBacklight,
401             keyboardBacklight,
402             intArrayOf(0, 22, 63, 135, 196, 255),
403         )
404     }
405 
406     @Test
407     fun testAmbientBacklightControl_incrementLevel_afterAmbientChange() {
408         setupController()
409         val keyboardWithBacklight = createKeyboard(DEVICE_ID)
410         val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
411         `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
412         `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
413         keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
414         sendAmbientBacklightValue(1)
415         assertEquals(
416             "Light value should be changed to ambient provided value",
417             Color.argb(1, 0, 0, 0),
418             lightColorMap[LIGHT_ID],
419         )
420 
421         incrementKeyboardBacklight(DEVICE_ID)
422 
423         assertEquals(
424             "Light value for level after increment post Ambient change is mismatched",
425             Color.argb(DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[1], 0, 0, 0),
426             lightColorMap[LIGHT_ID],
427         )
428     }
429 
430     @Test
431     fun testAmbientBacklightControl_decrementLevel_afterAmbientChange() {
432         setupController()
433         val keyboardWithBacklight = createKeyboard(DEVICE_ID)
434         val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
435         `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
436         `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
437         keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
438         sendAmbientBacklightValue(254)
439         assertEquals(
440             "Light value should be changed to ambient provided value",
441             Color.argb(254, 0, 0, 0),
442             lightColorMap[LIGHT_ID],
443         )
444 
445         decrementKeyboardBacklight(DEVICE_ID)
446 
447         val numLevels = DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL.size
448         assertEquals(
449             "Light value for level after decrement post Ambient change is mismatched",
450             Color.argb(DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[numLevels - 2], 0, 0, 0),
451             lightColorMap[LIGHT_ID],
452         )
453     }
454 
455     @Test
456     fun testAmbientBacklightControl_ambientChanges_afterManualChange() {
457         setupController()
458         val keyboardWithBacklight = createKeyboard(DEVICE_ID)
459         val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
460         `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
461         `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
462         keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
463         incrementKeyboardBacklight(DEVICE_ID)
464         assertEquals(
465             "Light value should be changed to the first level",
466             Color.argb(DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[1], 0, 0, 0),
467             lightColorMap[LIGHT_ID],
468         )
469 
470         sendAmbientBacklightValue(100)
471         assertNotEquals(
472             "Light value should not change based on ambient changes after manual changes",
473             Color.argb(100, 0, 0, 0),
474             lightColorMap[LIGHT_ID],
475         )
476     }
477 
478     private fun assertIncrementDecrementForLevels(
479         device: InputDevice,
480         light: Light,
481         expectedLevels: IntArray,
482     ) {
483         val deviceId = device.id
484         val lightId = light.id
485         for (level in 1 until expectedLevels.size) {
486             incrementKeyboardBacklight(deviceId)
487             assertEquals(
488                 "Light value for level $level mismatched",
489                 Color.argb(expectedLevels[level], 0, 0, 0),
490                 lightColorMap[lightId],
491             )
492         }
493 
494         // Increment above max level
495         incrementKeyboardBacklight(deviceId)
496         assertEquals(
497             "Light value for max level mismatched",
498             Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
499             lightColorMap[lightId],
500         )
501 
502         for (level in expectedLevels.size - 2 downTo 0) {
503             decrementKeyboardBacklight(deviceId)
504             assertEquals(
505                 "Light value for level $level mismatched",
506                 Color.argb(expectedLevels[level], 0, 0, 0),
507                 lightColorMap[lightId],
508             )
509         }
510 
511         // Decrement below min level
512         decrementKeyboardBacklight(deviceId)
513         assertEquals(
514             "Light value for min level mismatched",
515             Color.argb(0, 0, 0, 0),
516             lightColorMap[lightId],
517         )
518     }
519 
520     inner class KeyboardBacklightListener : IKeyboardBacklightListener.Stub() {
521         override fun onBrightnessChanged(
522             deviceId: Int,
523             state: IKeyboardBacklightState,
524             isTriggeredByKeyPress: Boolean,
525         ) {
526             lastBacklightState =
527                 KeyboardBacklightState(
528                     deviceId,
529                     state.brightnessLevel,
530                     state.maxBrightnessLevel,
531                     isTriggeredByKeyPress,
532                 )
533         }
534     }
535 
536     private fun incrementKeyboardBacklight(deviceId: Int) {
537         keyboardBacklightController.incrementKeyboardBacklight(deviceId)
538         keyboardBacklightController.notifyUserActivity()
539         testLooper.dispatchAll()
540     }
541 
542     private fun decrementKeyboardBacklight(deviceId: Int) {
543         keyboardBacklightController.decrementKeyboardBacklight(deviceId)
544         keyboardBacklightController.notifyUserActivity()
545         testLooper.dispatchAll()
546     }
547 
548     private fun sendAmbientBacklightValue(brightnessValue: Int) {
549         keyboardBacklightController.handleAmbientLightValueChanged(brightnessValue)
550         keyboardBacklightController.notifyUserActivity()
551         testLooper.dispatchAll()
552     }
553 
554     class KeyboardBacklightState(
555         val deviceId: Int,
556         val brightnessLevel: Int,
557         val maxBrightnessLevel: Int,
558         val isTriggeredByKeyPress: Boolean,
559     )
560 
561     private inner class FakeAnimatorFactory : KeyboardBacklightController.AnimatorFactory {
562         override fun makeIntAnimator(from: Int, to: Int): ValueAnimator {
563             lastAnimationValues[0] = from
564             lastAnimationValues[1] = to
565             return ValueAnimator.ofInt(from, to)
566         }
567     }
568 }
569