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

Commit 7c186c49 authored by Vaibhav Devmurari's avatar Vaibhav Devmurari Committed by Android (Google) Code Review
Browse files

Merge "Add framework controlled backlight transition animations" into udc-qpr-dev

parents 22f8f555 aaf4eba0
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)
        }
    }
}