Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit aaf4eba0 authored by Vaibhav Devmurari's avatar Vaibhav Devmurari
Browse files

Add framework controlled backlight transition animations

Test: atest KeyboardBacklightControllerTests
Bug: 279072070
Change-Id: Icb1bcd0c3dca4a3c4448e5a1c600917ad98cd9bb
parent a67c59fa
Loading
Loading
Loading
Loading
+67 −0
Original line number Diff line number Diff line
/*
 * Copyright 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.input;

import android.os.SystemProperties;

import java.util.Optional;

/**
 * A component of {@link InputManagerService} responsible for managing the input sysprop flags
 *
 * @hide
 */
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public final class InputFeatureFlagProvider {

    // To disable Keyboard backlight control via Framework, run:
    // 'adb shell setprop persist.input.keyboard_backlight_control.enabled false' (requires restart)
    private static final boolean KEYBOARD_BACKLIGHT_CONTROL_ENABLED = SystemProperties.getBoolean(
            "persist.input.keyboard.backlight_control.enabled", true);

    // To disable Framework controlled keyboard backlight animation run:
    // adb shell setprop persist.input.keyboard_backlight_animation.enabled false (requires restart)
    private static final boolean KEYBOARD_BACKLIGHT_ANIMATION_ENABLED = SystemProperties.getBoolean(
            "persist.input.keyboard.keyboard_backlight_animation.enabled", false);

    private static Optional<Boolean> sKeyboardBacklightControlOverride = Optional.empty();
    private static Optional<Boolean> sKeyboardBacklightAnimationOverride = Optional.empty();

    public static boolean isKeyboardBacklightControlEnabled() {
        return sKeyboardBacklightControlOverride.orElse(KEYBOARD_BACKLIGHT_CONTROL_ENABLED);
    }

    public static boolean isKeyboardBacklightAnimationEnabled() {
        return sKeyboardBacklightAnimationOverride.orElse(KEYBOARD_BACKLIGHT_ANIMATION_ENABLED);
    }

    public static void setKeyboardBacklightControlEnabled(boolean enabled) {
        sKeyboardBacklightControlOverride = Optional.of(enabled);
    }

    public static void setKeyboardBacklightAnimationEnabled(boolean enabled) {
        sKeyboardBacklightAnimationOverride = Optional.of(enabled);
    }

    /**
     * Clears all input feature flag overrides.
     */
    public static void clearOverrides() {
        sKeyboardBacklightControlOverride = Optional.empty();
        sKeyboardBacklightAnimationOverride = Optional.empty();
    }
}
+3 −10
Original line number Diff line number Diff line
@@ -72,7 +72,6 @@ import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.VibrationEffect;
import android.os.vibrator.StepSegment;
@@ -157,11 +156,6 @@ public class InputManagerService extends IInputManager.Stub
    private static final AdditionalDisplayInputProperties
            DEFAULT_ADDITIONAL_DISPLAY_INPUT_PROPERTIES = new AdditionalDisplayInputProperties();

    // To disable Keyboard backlight control via Framework, run:
    // 'adb shell setprop persist.input.keyboard_backlight_control.enabled false' (requires restart)
    private static final boolean KEYBOARD_BACKLIGHT_CONTROL_ENABLED = SystemProperties.getBoolean(
            "persist.input.keyboard.backlight_control.enabled", true);

    private final NativeInputManagerService mNative;

    private final Context mContext;
@@ -431,10 +425,9 @@ public class InputManagerService extends IInputManager.Stub
        mKeyboardLayoutManager = new KeyboardLayoutManager(mContext, mNative, mDataStore,
                injector.getLooper());
        mBatteryController = new BatteryController(mContext, mNative, injector.getLooper());
        mKeyboardBacklightController =
                KEYBOARD_BACKLIGHT_CONTROL_ENABLED ? new KeyboardBacklightController(mContext,
                        mNative, mDataStore, injector.getLooper())
                        : new KeyboardBacklightControllerInterface() {};
        mKeyboardBacklightController = InputFeatureFlagProvider.isKeyboardBacklightControlEnabled()
                ? new KeyboardBacklightController(mContext, mNative, mDataStore,
                injector.getLooper()) : new KeyboardBacklightControllerInterface() {};
        mKeyRemapper = new KeyRemapper(mContext, mNative, mDataStore, injector.getLooper());

        mUseDevInputEventForAudioJack =
+81 −36
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.server.input;

import android.animation.ValueAnimator;
import android.annotation.BinderThread;
import android.content.Context;
import android.graphics.Color;
@@ -70,6 +71,8 @@ final class KeyboardBacklightController implements
    private static final int MSG_INTERACTIVE_STATE_CHANGED = 6;
    private static final int MAX_BRIGHTNESS = 255;
    private static final int NUM_BRIGHTNESS_CHANGE_STEPS = 10;
    private static final long TRANSITION_ANIMATION_DURATION_MILLIS = Duration.ofSeconds(
            1).toMillis();

    private static final String UEVENT_KEYBOARD_BACKLIGHT_TAG = "kbd_backlight";

@@ -85,6 +88,7 @@ final class KeyboardBacklightController implements
    @GuardedBy("mDataStore")
    private final PersistentDataStore mDataStore;
    private final Handler mHandler;
    private final AnimatorFactory mAnimatorFactory;
    // Always access on handler thread or need to lock this for synchronization.
    private final SparseArray<KeyboardBacklightState> mKeyboardBacklights = new SparseArray<>(1);
    // Maintains state if all backlights should be on or turned off
@@ -109,10 +113,17 @@ final class KeyboardBacklightController implements

    KeyboardBacklightController(Context context, NativeInputManagerService nativeService,
            PersistentDataStore dataStore, Looper looper) {
        this(context, nativeService, dataStore, looper, ValueAnimator::ofInt);
    }

    @VisibleForTesting
    KeyboardBacklightController(Context context, NativeInputManagerService nativeService,
            PersistentDataStore dataStore, Looper looper, AnimatorFactory animatorFactory) {
        mContext = context;
        mNative = nativeService;
        mDataStore = dataStore;
        mHandler = new Handler(looper, this::handleMessage);
        mAnimatorFactory = animatorFactory;
    }

    @Override
@@ -177,8 +188,7 @@ final class KeyboardBacklightController implements
        } else {
            newBrightnessLevel = Math.max(currBrightnessLevel - 1, 0);
        }
        updateBacklightState(deviceId, keyboardBacklight, newBrightnessLevel,
                true /* isTriggeredByKeyPress */);
        updateBacklightState(deviceId, newBrightnessLevel, true /* isTriggeredByKeyPress */);

        synchronized (mDataStore) {
            try {
@@ -203,8 +213,7 @@ final class KeyboardBacklightController implements
            if (index < 0) {
                index = Math.min(NUM_BRIGHTNESS_CHANGE_STEPS, -(index + 1));
            }
            updateBacklightState(inputDevice.getId(), keyboardBacklight, index,
                    false /* isTriggeredByKeyPress */);
            updateBacklightState(inputDevice.getId(), index, false /* isTriggeredByKeyPress */);
            if (DEBUG) {
                Slog.d(TAG, "Restoring brightness level " + brightness.getAsInt());
            }
@@ -217,14 +226,10 @@ final class KeyboardBacklightController implements
        if (!mIsInteractive) {
            return;
        }
        if (!mIsBacklightOn) {
        mIsBacklightOn = true;
        for (int i = 0; i < mKeyboardBacklights.size(); i++) {
                int deviceId = mKeyboardBacklights.keyAt(i);
            KeyboardBacklightState state = mKeyboardBacklights.valueAt(i);
                updateBacklightState(deviceId, state.mLight, state.mBrightnessLevel,
                        false /* isTriggeredByKeyPress */);
            }
            state.onBacklightStateChanged();
        }
        mHandler.removeMessages(MSG_NOTIFY_USER_INACTIVITY);
        mHandler.sendEmptyMessageAtTime(MSG_NOTIFY_USER_INACTIVITY,
@@ -232,14 +237,10 @@ final class KeyboardBacklightController implements
    }

    private void handleUserInactivity() {
        if (mIsBacklightOn) {
        mIsBacklightOn = false;
        for (int i = 0; i < mKeyboardBacklights.size(); i++) {
                int deviceId = mKeyboardBacklights.keyAt(i);
            KeyboardBacklightState state = mKeyboardBacklights.valueAt(i);
                updateBacklightState(deviceId, state.mLight, state.mBrightnessLevel,
                        false /* isTriggeredByKeyPress */);
            }
            state.onBacklightStateChanged();
        }
    }

@@ -310,7 +311,7 @@ final class KeyboardBacklightController implements
            return;
        }
        // The keyboard backlight was added or changed.
        mKeyboardBacklights.put(deviceId, new KeyboardBacklightState(keyboardBacklight));
        mKeyboardBacklights.put(deviceId, new KeyboardBacklightState(deviceId, keyboardBacklight));
        restoreBacklightBrightness(inputDevice, keyboardBacklight);
    }

@@ -372,21 +373,14 @@ final class KeyboardBacklightController implements
        }
    }

    private void updateBacklightState(int deviceId, Light light, int brightnessLevel,
    private void updateBacklightState(int deviceId, int brightnessLevel,
            boolean isTriggeredByKeyPress) {
        KeyboardBacklightState state = mKeyboardBacklights.get(deviceId);
        if (state == null) {
            return;
        }

        mNative.setLightColor(deviceId, light.getId(),
                mIsBacklightOn ? Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[brightnessLevel], 0, 0, 0)
                        : 0);
        if (DEBUG) {
            Slog.d(TAG, "Changing state from " + state.mBrightnessLevel + " to " + brightnessLevel
                    + "(isBacklightOn = " + mIsBacklightOn + ")");
        }
        state.mBrightnessLevel = brightnessLevel;
        state.setBrightnessLevel(brightnessLevel);

        synchronized (mKeyboardBacklightListenerRecords) {
            for (int i = 0; i < mKeyboardBacklightListenerRecords.size(); i++) {
@@ -397,6 +391,10 @@ final class KeyboardBacklightController implements
                        deviceId, callbackState, isTriggeredByKeyPress);
            }
        }

        if (DEBUG) {
            Slog.d(TAG, "Changing state from " + state.mBrightnessLevel + " to " + brightnessLevel);
        }
    }

    private void onKeyboardBacklightListenerDied(int pid) {
@@ -436,10 +434,7 @@ final class KeyboardBacklightController implements
    @Override
    public void dump(PrintWriter pw) {
        IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
        ipw.println(
                TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights, isBacklightOn = "
                        + mIsBacklightOn);

        ipw.println(TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights");
        ipw.increaseIndent();
        for (int i = 0; i < mKeyboardBacklights.size(); i++) {
            KeyboardBacklightState state = mKeyboardBacklights.valueAt(i);
@@ -448,6 +443,10 @@ final class KeyboardBacklightController implements
        ipw.decreaseIndent();
    }

    private static boolean isAnimationEnabled() {
        return InputFeatureFlagProvider.isKeyboardBacklightAnimationEnabled();
    }

    // A record of a registered Keyboard backlight listener from one process.
    private class KeyboardBacklightListenerRecord implements IBinder.DeathRecipient {
        public final int mPid;
@@ -478,14 +477,55 @@ final class KeyboardBacklightController implements
        }
    }

    private static class KeyboardBacklightState {
    private class KeyboardBacklightState {
        private final int mDeviceId;
        private final Light mLight;
        private int mBrightnessLevel;
        private ValueAnimator mAnimator;

        KeyboardBacklightState(Light light) {
        KeyboardBacklightState(int deviceId, Light light) {
            mDeviceId = deviceId;
            mLight = light;
        }

        private void onBacklightStateChanged() {
            setBacklightValue(mIsBacklightOn ? BRIGHTNESS_VALUE_FOR_LEVEL[mBrightnessLevel] : 0);
        }
        private void setBrightnessLevel(int brightnessLevel) {
            if (mIsBacklightOn) {
                setBacklightValue(BRIGHTNESS_VALUE_FOR_LEVEL[brightnessLevel]);
            }
            mBrightnessLevel = brightnessLevel;
        }

        private void cancelAnimation() {
            if (mAnimator != null && mAnimator.isRunning()) {
                mAnimator.cancel();
            }
        }

        private void setBacklightValue(int toValue) {
            int fromValue = Color.alpha(mNative.getLightColor(mDeviceId, mLight.getId()));
            if (fromValue == toValue) {
                return;
            }
            if (isAnimationEnabled()) {
                startAnimation(fromValue, toValue);
            } else {
                mNative.setLightColor(mDeviceId, mLight.getId(), Color.argb(toValue, 0, 0, 0));
            }
        }

        private void startAnimation(int fromValue, int toValue) {
            // Cancel any ongoing animation before starting a new one
            cancelAnimation();
            mAnimator = mAnimatorFactory.makeIntAnimator(fromValue, toValue);
            mAnimator.addUpdateListener(
                    (animation) -> mNative.setLightColor(mDeviceId, mLight.getId(),
                            Color.argb((int) animation.getAnimatedValue(), 0, 0, 0)));
            mAnimator.setDuration(TRANSITION_ANIMATION_DURATION_MILLIS).start();
        }

        @Override
        public String toString() {
            return "KeyboardBacklightState{Light=" + mLight.getId()
@@ -493,4 +533,9 @@ final class KeyboardBacklightController implements
                    + "}";
        }
    }

    @VisibleForTesting
    interface AnimatorFactory {
        ValueAnimator makeIntAnimator(int from, int to);
    }
}
+265 −199
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.server.input

import android.animation.ValueAnimator
import android.content.Context
import android.content.ContextWrapper
import android.graphics.Color
@@ -29,6 +30,7 @@ import android.os.UEventObserver
import android.os.test.TestLooper
import android.platform.test.annotations.Presubmit
import android.view.InputDevice
import androidx.test.annotation.UiThreadTest
import androidx.test.core.app.ApplicationProvider
import com.android.server.input.KeyboardBacklightController.BRIGHTNESS_VALUE_FOR_LEVEL
import com.android.server.input.KeyboardBacklightController.USER_INACTIVITY_THRESHOLD_MILLIS
@@ -96,9 +98,11 @@ class KeyboardBacklightControllerTests {
    private lateinit var context: Context
    private lateinit var dataStore: PersistentDataStore
    private lateinit var testLooper: TestLooper
    private val totalLevels = BRIGHTNESS_VALUE_FOR_LEVEL.size
    private var lightColorMap: HashMap<Int, Int> = HashMap()
    private var lastBacklightState: KeyboardBacklightState? = null
    private var sysfsNodeChanges = 0
    private var lastAnimationValues = IntArray(2)

    @Before
    fun setup() {
@@ -115,8 +119,8 @@ class KeyboardBacklightControllerTests {
            override fun finishWrite(fos: FileOutputStream?, success: Boolean) {}
        })
        testLooper = TestLooper()
        keyboardBacklightController =
            KeyboardBacklightController(context, native, dataStore, testLooper.looper)
        keyboardBacklightController = KeyboardBacklightController(context, native, dataStore,
                testLooper.looper, FakeAnimatorFactory())
        InputManagerGlobal.resetInstance(iInputManager)
        val inputManager = InputManager(context)
        `when`(context.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(inputManager)
@@ -125,6 +129,10 @@ class KeyboardBacklightControllerTests {
            val args = it.arguments
            lightColorMap.put(args[1] as Int, args[2] as Int)
        }
        `when`(native.getLightColor(anyInt(), anyInt())).thenAnswer {
            val args = it.arguments
            lightColorMap.getOrDefault(args[1] as Int, 0)
        }
        lightColorMap.clear()
        `when`(native.sysfsNodeChanged(any())).then {
            sysfsNodeChanges++
@@ -138,13 +146,14 @@ class KeyboardBacklightControllerTests {

    @Test
    fun testKeyboardBacklightIncrementDecrement() {
        BacklightAnimationFlag(false).use {
            val keyboardWithBacklight = createKeyboard(DEVICE_ID)
            val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
            `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
            `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
            keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)

        for (level in 1 until BRIGHTNESS_VALUE_FOR_LEVEL.size) {
            for (level in 1 until totalLevels) {
                incrementKeyboardBacklight(DEVICE_ID)
                assertEquals(
                    "Light value for level $level mismatched",
@@ -177,7 +186,7 @@ class KeyboardBacklightControllerTests {
                ).asInt
            )

        for (level in BRIGHTNESS_VALUE_FOR_LEVEL.size - 2 downTo 0) {
            for (level in totalLevels - 2 downTo 0) {
                decrementKeyboardBacklight(DEVICE_ID)
                assertEquals(
                    "Light value for level $level mismatched",
@@ -210,9 +219,11 @@ class KeyboardBacklightControllerTests {
                ).asInt
            )
        }
    }

    @Test
    fun testKeyboardWithoutBacklight() {
        BacklightAnimationFlag(false).use {
            val keyboardWithoutBacklight = createKeyboard(DEVICE_ID)
            val keyboardInputLight = createLight(LIGHT_ID, Light.LIGHT_TYPE_INPUT)
            `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithoutBacklight)
@@ -222,9 +233,11 @@ class KeyboardBacklightControllerTests {
            incrementKeyboardBacklight(DEVICE_ID)
            assertTrue("Non Keyboard backlights should not change", lightColorMap.isEmpty())
        }
    }

    @Test
    fun testKeyboardWithMultipleLight() {
        BacklightAnimationFlag(false).use {
            val keyboardWithBacklight = createKeyboard(DEVICE_ID)
            val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
            val keyboardInputLight = createLight(SECOND_LIGHT_ID, Light.LIGHT_TYPE_INPUT)
@@ -242,15 +255,17 @@ class KeyboardBacklightControllerTests {
            assertNotNull("Keyboard backlight should change", lightColorMap[LIGHT_ID])
            assertNull("Input lights should not change", lightColorMap[SECOND_LIGHT_ID])
        }
    }

    @Test
    fun testRestoreBacklightOnInputDeviceAdded() {
        BacklightAnimationFlag(false).use {
            val keyboardWithBacklight = createKeyboard(DEVICE_ID)
            val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
            `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
            `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))

        for (level in 1 until BRIGHTNESS_VALUE_FOR_LEVEL.size) {
            for (level in 1 until totalLevels) {
                dataStore.setKeyboardBacklightBrightness(
                    keyboardWithBacklight.descriptor,
                    LIGHT_ID,
@@ -261,17 +276,19 @@ class KeyboardBacklightControllerTests {
                keyboardBacklightController.notifyUserActivity()
                testLooper.dispatchNext()
                assertEquals(
                    "Keyboard backlight level should be restored to the level saved in the data " +
                            "store",
                    "Keyboard backlight level should be restored to the level saved in the " +
                            "data store",
                    Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0),
                    lightColorMap[LIGHT_ID]
                )
                keyboardBacklightController.onInputDeviceRemoved(DEVICE_ID)
            }
        }
    }

    @Test
    fun testRestoreBacklightOnInputDeviceChanged() {
        BacklightAnimationFlag(false).use {
            val keyboardWithBacklight = createKeyboard(DEVICE_ID)
            val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
            `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
@@ -299,9 +316,11 @@ class KeyboardBacklightControllerTests {
                lightColorMap[LIGHT_ID]
            )
        }
    }

    @Test
    fun testKeyboardBacklight_registerUnregisterListener() {
        BacklightAnimationFlag(false).use {
            val keyboardWithBacklight = createKeyboard(DEVICE_ID)
            val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
            `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
@@ -327,8 +346,8 @@ class KeyboardBacklightControllerTests {
                lastBacklightState!!.brightnessLevel
            )
            assertEquals(
            "Backlight state maxBrightnessLevel should be " + (BRIGHTNESS_VALUE_FOR_LEVEL.size - 1),
            (BRIGHTNESS_VALUE_FOR_LEVEL.size - 1),
                "Backlight state maxBrightnessLevel should be " + (totalLevels - 1),
                (totalLevels - 1),
                lastBacklightState!!.maxBrightnessLevel
            )
            assertEquals(
@@ -345,9 +364,11 @@ class KeyboardBacklightControllerTests {

            assertNull("Listener should not receive any updates", lastBacklightState)
        }
    }

    @Test
    fun testKeyboardBacklight_userActivity() {
        BacklightAnimationFlag(false).use {
            val keyboardWithBacklight = createKeyboard(DEVICE_ID)
            val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
            `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
@@ -375,9 +396,11 @@ class KeyboardBacklightControllerTests {
                lightColorMap[LIGHT_ID]
            )
        }
    }

    @Test
    fun testKeyboardBacklight_displayOnOff() {
        BacklightAnimationFlag(false).use {
            val keyboardWithBacklight = createKeyboard(DEVICE_ID)
            val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
            `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
@@ -404,6 +427,7 @@ class KeyboardBacklightControllerTests {
                lightColorMap[LIGHT_ID]
            )
        }
    }

    @Test
    fun testKeyboardBacklightSysfsNodeAdded_AfterInputDeviceAdded() {
@@ -463,6 +487,30 @@ class KeyboardBacklightControllerTests {
        )
    }

    @Test
    @UiThreadTest
    fun testKeyboardBacklightAnimation_onChangeLevels() {
        BacklightAnimationFlag(true).use {
            val keyboardWithBacklight = createKeyboard(DEVICE_ID)
            val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
            `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
            `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
            keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)

            incrementKeyboardBacklight(DEVICE_ID)
            assertEquals(
                "Should start animation from level 0",
                BRIGHTNESS_VALUE_FOR_LEVEL[0],
                lastAnimationValues[0]
            )
            assertEquals(
                "Should start animation to level 1",
                BRIGHTNESS_VALUE_FOR_LEVEL[1],
                lastAnimationValues[1]
            )
        }
    }

    inner class KeyboardBacklightListener : IKeyboardBacklightListener.Stub() {
        override fun onBrightnessChanged(
            deviceId: Int,
@@ -496,4 +544,22 @@ class KeyboardBacklightControllerTests {
        val maxBrightnessLevel: Int,
        val isTriggeredByKeyPress: Boolean
    )

    private inner class BacklightAnimationFlag constructor(enabled: Boolean) : AutoCloseable {
        init {
            InputFeatureFlagProvider.setKeyboardBacklightAnimationEnabled(enabled)
        }

        override fun close() {
            InputFeatureFlagProvider.clearOverrides()
        }
    }

    private inner class FakeAnimatorFactory : KeyboardBacklightController.AnimatorFactory {
        override fun makeIntAnimator(from: Int, to: Int): ValueAnimator {
            lastAnimationValues[0] = from
            lastAnimationValues[1] = to
            return ValueAnimator.ofInt(from, to)
        }
    }
}