Add inactivity timer to turn off keyboard backlight

- Should turn off backlight after inactivity of 30s
- Should turn on backlight if user does any activity
- If Display is turned off backlight should be turned off
- If Display is turned on backlight should also be turned on
- Can't rely completely on Display turn on/off logic since, apps
can acquire wakelock to keep Display on (e.g. watching a video),
but this should not stop the keyboard backlight from turning off.

Test: atest KeyboardBacklightControllerTests
Bug: 245506418
Change-Id: I024a9c507ee42e42896c501e9bb1cb83ca315e69
diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java
index ca42614..4d03e44 100644
--- a/services/core/java/com/android/server/input/InputManagerInternal.java
+++ b/services/core/java/com/android/server/input/InputManagerInternal.java
@@ -213,4 +213,10 @@
      * @param enabled When true, stylus buttons will not be reported through motion events.
      */
     public abstract void setStylusButtonMotionEventsEnabled(boolean enabled);
+
+    /**
+     * Notify whether any user activity occurred. This includes any input activity on any
+     * display, external peripherals, fingerprint sensor, etc.
+     */
+    public abstract void notifyUserActivity();
 }
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index e86632c..be4373a 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -3271,6 +3271,7 @@
         public void setInteractive(boolean interactive) {
             mNative.setInteractive(interactive);
             mBatteryController.onInteractiveChanged(interactive);
+            mKeyboardBacklightController.onInteractiveChanged(interactive);
         }
 
         @Override
@@ -3358,6 +3359,11 @@
         }
 
         @Override
+        public void notifyUserActivity() {
+            mKeyboardBacklightController.notifyUserActivity();
+        }
+
+        @Override
         public void incrementKeyboardBacklight(int deviceId) {
             mKeyboardBacklightController.incrementKeyboardBacklight(deviceId);
         }
@@ -3489,6 +3495,8 @@
         default void decrementKeyboardBacklight(int deviceId) {}
         default void registerKeyboardBacklightListener(IKeyboardBacklightListener l, int pid) {}
         default void unregisterKeyboardBacklightListener(IKeyboardBacklightListener l, int pid) {}
+        default void onInteractiveChanged(boolean isInteractive) {}
+        default void notifyUserActivity() {}
         default void systemRunning() {}
         default void dump(PrintWriter pw) {}
     }
diff --git a/services/core/java/com/android/server/input/KeyboardBacklightController.java b/services/core/java/com/android/server/input/KeyboardBacklightController.java
index 653a821..e1e3dd9 100644
--- a/services/core/java/com/android/server/input/KeyboardBacklightController.java
+++ b/services/core/java/com/android/server/input/KeyboardBacklightController.java
@@ -17,7 +17,6 @@
 package com.android.server.input;
 
 import android.annotation.BinderThread;
-import android.annotation.ColorInt;
 import android.content.Context;
 import android.graphics.Color;
 import android.hardware.input.IKeyboardBacklightListener;
@@ -29,6 +28,7 @@
 import android.os.Looper;
 import android.os.Message;
 import android.os.RemoteException;
+import android.os.SystemClock;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
 import android.util.Slog;
@@ -39,9 +39,10 @@
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.io.PrintWriter;
+import java.time.Duration;
+import java.util.Arrays;
 import java.util.Objects;
 import java.util.OptionalInt;
-import java.util.TreeSet;
 
 /**
  * A thread-safe component of {@link InputManagerService} responsible for managing the keyboard
@@ -59,12 +60,20 @@
     private enum Direction {
         DIRECTION_UP, DIRECTION_DOWN
     }
-    private static final int MSG_INCREMENT_KEYBOARD_BACKLIGHT = 1;
-    private static final int MSG_DECREMENT_KEYBOARD_BACKLIGHT = 2;
+    private static final int MSG_UPDATE_EXISTING_DEVICES = 1;
+    private static final int MSG_INCREMENT_KEYBOARD_BACKLIGHT = 2;
+    private static final int MSG_DECREMENT_KEYBOARD_BACKLIGHT = 3;
+    private static final int MSG_NOTIFY_USER_ACTIVITY = 4;
+    private static final int MSG_NOTIFY_USER_INACTIVITY = 5;
+    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;
+
     @VisibleForTesting
-    static final TreeSet<Integer> BRIGHTNESS_LEVELS = new TreeSet<>();
+    static final long USER_INACTIVITY_THRESHOLD_MILLIS = Duration.ofSeconds(30).toMillis();
+
+    @VisibleForTesting
+    static final int[] BRIGHTNESS_VALUE_FOR_LEVEL = new int[NUM_BRIGHTNESS_CHANGE_STEPS + 1];
 
     private final Context mContext;
     private final NativeInputManagerService mNative;
@@ -72,7 +81,12 @@
     @GuardedBy("mDataStore")
     private final PersistentDataStore mDataStore;
     private final Handler mHandler;
-    private final SparseArray<Light> mKeyboardBacklights = new SparseArray<>();
+    // 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
+    private boolean mIsBacklightOn = false;
+    // Maintains state if currently the device is interactive or not
+    private boolean mIsInteractive = true;
 
     // List of currently registered keyboard backlight listeners
     @GuardedBy("mKeyboardBacklightListenerRecords")
@@ -84,8 +98,8 @@
         // device brightness range to [0-255]
         // Levels are: 0, 25, 51, ..., 255
         for (int i = 0; i <= NUM_BRIGHTNESS_CHANGE_STEPS; i++) {
-            BRIGHTNESS_LEVELS.add(
-                    (int) Math.floor(((float) i * MAX_BRIGHTNESS) / NUM_BRIGHTNESS_CHANGE_STEPS));
+            BRIGHTNESS_VALUE_FOR_LEVEL[i] = (int) Math.floor(
+                    ((float) i * MAX_BRIGHTNESS) / NUM_BRIGHTNESS_CHANGE_STEPS);
         }
     }
 
@@ -102,10 +116,10 @@
         InputManager inputManager = Objects.requireNonNull(
                 mContext.getSystemService(InputManager.class));
         inputManager.registerInputDeviceListener(this, mHandler);
-        // Circle through all the already added input devices
-        for (int deviceId : inputManager.getInputDeviceIds()) {
-            onInputDeviceAdded(deviceId);
-        }
+
+        Message msg = Message.obtain(mHandler, MSG_UPDATE_EXISTING_DEVICES,
+                inputManager.getInputDeviceIds());
+        mHandler.sendMessage(msg);
     }
 
     @Override
@@ -120,37 +134,41 @@
         mHandler.sendMessage(msg);
     }
 
+    @Override
+    public void notifyUserActivity() {
+        Message msg = Message.obtain(mHandler, MSG_NOTIFY_USER_ACTIVITY);
+        mHandler.sendMessage(msg);
+    }
+
+    @Override
+    public void onInteractiveChanged(boolean isInteractive) {
+        Message msg = Message.obtain(mHandler, MSG_INTERACTIVE_STATE_CHANGED, isInteractive);
+        mHandler.sendMessage(msg);
+    }
+
     private void updateKeyboardBacklight(int deviceId, Direction direction) {
         InputDevice inputDevice = getInputDevice(deviceId);
-        Light keyboardBacklight = mKeyboardBacklights.get(deviceId);
-        if (inputDevice == null || keyboardBacklight == null) {
+        KeyboardBacklightState state = mKeyboardBacklights.get(deviceId);
+        if (inputDevice == null || state == null) {
             return;
         }
+        Light keyboardBacklight = state.mLight;
         // Follow preset levels of brightness defined in BRIGHTNESS_LEVELS
-        int currBrightness = BRIGHTNESS_LEVELS.floor(Color.alpha(
-                mNative.getLightColor(deviceId, keyboardBacklight.getId())));
-        int newBrightness;
+        final int currBrightnessLevel = state.mBrightnessLevel;
+        final int newBrightnessLevel;
         if (direction == Direction.DIRECTION_UP) {
-            newBrightness = currBrightness != MAX_BRIGHTNESS ? BRIGHTNESS_LEVELS.higher(
-                    currBrightness) : currBrightness;
+            newBrightnessLevel = Math.min(currBrightnessLevel + 1, NUM_BRIGHTNESS_CHANGE_STEPS);
         } else {
-            newBrightness = currBrightness != 0 ? BRIGHTNESS_LEVELS.lower(currBrightness)
-                    : currBrightness;
+            newBrightnessLevel = Math.max(currBrightnessLevel - 1, 0);
         }
-        @ColorInt int newColor = Color.argb(newBrightness, 0, 0, 0);
-        mNative.setLightColor(deviceId, keyboardBacklight.getId(), newColor);
-        if (DEBUG) {
-            Slog.d(TAG, "Changing brightness from " + currBrightness + " to " + newBrightness);
-        }
-
-        notifyKeyboardBacklightChanged(deviceId, BRIGHTNESS_LEVELS.headSet(newBrightness).size(),
-                true/* isTriggeredByKeyPress */);
+        updateBacklightState(deviceId, keyboardBacklight, newBrightnessLevel,
+                true /* isTriggeredByKeyPress */);
 
         synchronized (mDataStore) {
             try {
                 mDataStore.setKeyboardBacklightBrightness(inputDevice.getDescriptor(),
                         keyboardBacklight.getId(),
-                        newBrightness);
+                        BRIGHTNESS_VALUE_FOR_LEVEL[newBrightnessLevel]);
             } finally {
                 mDataStore.saveIfNeeded();
             }
@@ -163,23 +181,83 @@
             brightness = mDataStore.getKeyboardBacklightBrightness(
                     inputDevice.getDescriptor(), keyboardBacklight.getId());
         }
-        if (!brightness.isEmpty()) {
-            mNative.setLightColor(inputDevice.getId(), keyboardBacklight.getId(),
-                    Color.argb(brightness.getAsInt(), 0, 0, 0));
+        if (brightness.isPresent()) {
+            int brightnessValue = Math.max(0, Math.min(MAX_BRIGHTNESS, brightness.getAsInt()));
+            int brightnessLevel = Arrays.binarySearch(BRIGHTNESS_VALUE_FOR_LEVEL, brightnessValue);
+            updateBacklightState(inputDevice.getId(), keyboardBacklight, brightnessLevel,
+                    false /* isTriggeredByKeyPress */);
             if (DEBUG) {
                 Slog.d(TAG, "Restoring brightness level " + brightness.getAsInt());
             }
         }
     }
 
+    private void handleUserActivity() {
+        // Ignore user activity if device is not interactive. When device becomes interactive, we
+        // will send another user activity to turn backlight on.
+        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 */);
+            }
+        }
+        mHandler.removeMessages(MSG_NOTIFY_USER_INACTIVITY);
+        mHandler.sendEmptyMessageAtTime(MSG_NOTIFY_USER_INACTIVITY,
+                SystemClock.uptimeMillis() + USER_INACTIVITY_THRESHOLD_MILLIS);
+    }
+
+    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 */);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    public void handleInteractiveStateChange(boolean isInteractive) {
+        // Interactive state changes should force the keyboard to turn on/off irrespective of
+        // whether time out occurred or not.
+        mIsInteractive = isInteractive;
+        if (isInteractive) {
+            handleUserActivity();
+        } else {
+            handleUserInactivity();
+        }
+    }
+
     private boolean handleMessage(Message msg) {
         switch (msg.what) {
+            case MSG_UPDATE_EXISTING_DEVICES:
+                for (int deviceId : (int[]) msg.obj) {
+                    onInputDeviceAdded(deviceId);
+                }
+                return true;
             case MSG_INCREMENT_KEYBOARD_BACKLIGHT:
                 updateKeyboardBacklight((int) msg.obj, Direction.DIRECTION_UP);
                 return true;
             case MSG_DECREMENT_KEYBOARD_BACKLIGHT:
                 updateKeyboardBacklight((int) msg.obj, Direction.DIRECTION_DOWN);
                 return true;
+            case MSG_NOTIFY_USER_ACTIVITY:
+                handleUserActivity();
+                return true;
+            case MSG_NOTIFY_USER_INACTIVITY:
+                handleUserInactivity();
+                return true;
+            case MSG_INTERACTIVE_STATE_CHANGED:
+                handleInteractiveStateChange((boolean) msg.obj);
+                return true;
         }
         return false;
     }
@@ -208,12 +286,12 @@
             mKeyboardBacklights.remove(deviceId);
             return;
         }
-        final Light oldBacklight = mKeyboardBacklights.get(deviceId);
-        if (oldBacklight != null && oldBacklight.getId() == keyboardBacklight.getId()) {
+        KeyboardBacklightState state = mKeyboardBacklights.get(deviceId);
+        if (state != null && state.mLight.getId() == keyboardBacklight.getId()) {
             return;
         }
         // The keyboard backlight was added or changed.
-        mKeyboardBacklights.put(deviceId, keyboardBacklight);
+        mKeyboardBacklights.put(deviceId, new KeyboardBacklightState(keyboardBacklight));
         restoreBacklightBrightness(inputDevice, keyboardBacklight);
     }
 
@@ -275,13 +353,29 @@
         }
     }
 
-    private void notifyKeyboardBacklightChanged(int deviceId, int currentBacklightLevel,
+    private void updateBacklightState(int deviceId, Light light, 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;
+
         synchronized (mKeyboardBacklightListenerRecords) {
             for (int i = 0; i < mKeyboardBacklightListenerRecords.size(); i++) {
+                IKeyboardBacklightState callbackState = new IKeyboardBacklightState();
+                callbackState.brightnessLevel = brightnessLevel;
+                callbackState.maxBrightnessLevel = NUM_BRIGHTNESS_CHANGE_STEPS;
                 mKeyboardBacklightListenerRecords.valueAt(i).notifyKeyboardBacklightChanged(
-                        deviceId, new KeyboardBacklightState(currentBacklightLevel),
-                        isTriggeredByKeyPress);
+                        deviceId, callbackState, isTriggeredByKeyPress);
             }
         }
     }
@@ -295,11 +389,14 @@
     @Override
     public void dump(PrintWriter pw) {
         IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
-        ipw.println(TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights");
+        ipw.println(
+                TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights, isBacklightOn = "
+                        + mIsBacklightOn);
+
         ipw.increaseIndent();
         for (int i = 0; i < mKeyboardBacklights.size(); i++) {
-            Light light = mKeyboardBacklights.get(i);
-            ipw.println(i + ": { id: " + light.getId() + ", name: " + light.getName() + " }");
+            KeyboardBacklightState state = mKeyboardBacklights.valueAt(i);
+            ipw.println(i + ": " + state.toString());
         }
         ipw.decreaseIndent();
     }
@@ -334,17 +431,18 @@
         }
     }
 
-    private static class KeyboardBacklightState extends IKeyboardBacklightState {
+    private static class KeyboardBacklightState {
+        private final Light mLight;
+        private int mBrightnessLevel;
 
-        KeyboardBacklightState(int brightnessLevel) {
-            this.brightnessLevel = brightnessLevel;
-            this.maxBrightnessLevel = NUM_BRIGHTNESS_CHANGE_STEPS;
+        KeyboardBacklightState(Light light) {
+            mLight = light;
         }
 
         @Override
         public String toString() {
-            return "KeyboardBacklightState{brightnessLevel=" + brightnessLevel
-                    + ", maxBrightnessLevel=" + maxBrightnessLevel
+            return "KeyboardBacklightState{Light=" + mLight.getId()
+                    + ", BrightnessLevel=" + mBrightnessLevel
                     + "}";
         }
     }
diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java
index 326d709..ed6a46f 100644
--- a/services/core/java/com/android/server/power/Notifier.java
+++ b/services/core/java/com/android/server/power/Notifier.java
@@ -737,6 +737,7 @@
         }
         TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
         tm.notifyUserActivity();
+        mInputManagerInternal.notifyUserActivity();
         mPolicy.userActivity(displayGroupId, event);
         mFaceDownDetector.userActivity(event);
         mScreenUndimDetector.userActivity(displayGroupId);
diff --git a/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt
index b5dad94..1d23e12 100644
--- a/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt
+++ b/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt
@@ -28,7 +28,8 @@
 import android.platform.test.annotations.Presubmit
 import android.view.InputDevice
 import androidx.test.core.app.ApplicationProvider
-import com.android.server.input.KeyboardBacklightController.BRIGHTNESS_LEVELS
+import com.android.server.input.KeyboardBacklightController.BRIGHTNESS_VALUE_FOR_LEVEL
+import com.android.server.input.KeyboardBacklightController.USER_INACTIVITY_THRESHOLD_MILLIS
 import org.junit.After
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNotNull
@@ -78,6 +79,7 @@
         const val DEVICE_ID = 1
         const val LIGHT_ID = 2
         const val SECOND_LIGHT_ID = 3
+        const val MAX_BRIGHTNESS = 255
     }
 
     @get:Rule
@@ -118,10 +120,6 @@
             val args = it.arguments
             lightColorMap.put(args[1] as Int, args[2] as Int)
         }
-        `when`(native.getLightColor(anyInt(), anyInt())).then {
-            val args = it.arguments
-            lightColorMap.getOrDefault(args[1] as Int, -1)
-        }
         lightColorMap.clear()
     }
 
@@ -137,21 +135,17 @@
         `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
         `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
         keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
-        // Initially backlight is at min
-        lightColorMap[LIGHT_ID] = Color.argb(BRIGHTNESS_LEVELS.first(), 0, 0, 0)
 
-        val brightnessLevelsArray = BRIGHTNESS_LEVELS.toTypedArray()
-        for (level in 1 until brightnessLevelsArray.size) {
-            keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID)
-            testLooper.dispatchNext()
+        for (level in 1 until BRIGHTNESS_VALUE_FOR_LEVEL.size) {
+            incrementKeyboardBacklight(DEVICE_ID)
             assertEquals(
                 "Light value for level $level mismatched",
-                Color.argb(brightnessLevelsArray[level], 0, 0, 0),
+                Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0),
                 lightColorMap[LIGHT_ID]
             )
             assertEquals(
                 "Light value for level $level must be correctly stored in the datastore",
-                brightnessLevelsArray[level],
+                BRIGHTNESS_VALUE_FOR_LEVEL[level],
                 dataStore.getKeyboardBacklightBrightness(
                     keyboardWithBacklight.descriptor,
                     LIGHT_ID
@@ -159,72 +153,49 @@
             )
         }
 
-        for (level in brightnessLevelsArray.size - 2 downTo 0) {
-            keyboardBacklightController.decrementKeyboardBacklight(DEVICE_ID)
-            testLooper.dispatchNext()
-            assertEquals(
-                "Light value for level $level mismatched",
-                Color.argb(brightnessLevelsArray[level], 0, 0, 0),
-                lightColorMap[LIGHT_ID]
-            )
-            assertEquals(
-                "Light value for level $level must be correctly stored in the datastore",
-                brightnessLevelsArray[level],
-                dataStore.getKeyboardBacklightBrightness(
-                    keyboardWithBacklight.descriptor,
-                    LIGHT_ID
-                ).asInt
-            )
-        }
-    }
-
-    @Test
-    fun testKeyboardBacklightIncrementAboveMaxLevel() {
-        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)
-        // Initially backlight is at max
-        lightColorMap[LIGHT_ID] = Color.argb(BRIGHTNESS_LEVELS.last(), 0, 0, 0)
-
-        keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID)
-        testLooper.dispatchNext()
+        // Increment above max level
+        incrementKeyboardBacklight(DEVICE_ID)
         assertEquals(
             "Light value for max level mismatched",
-            Color.argb(BRIGHTNESS_LEVELS.last(), 0, 0, 0),
+            Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
             lightColorMap[LIGHT_ID]
         )
         assertEquals(
             "Light value for max level must be correctly stored in the datastore",
-            BRIGHTNESS_LEVELS.last(),
+            MAX_BRIGHTNESS,
             dataStore.getKeyboardBacklightBrightness(
                 keyboardWithBacklight.descriptor,
                 LIGHT_ID
             ).asInt
         )
-    }
 
-    @Test
-    fun testKeyboardBacklightDecrementBelowMin() {
-        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)
-        // Initially backlight is at min
-        lightColorMap[LIGHT_ID] = Color.argb(BRIGHTNESS_LEVELS.first(), 0, 0, 0)
+        for (level in BRIGHTNESS_VALUE_FOR_LEVEL.size - 2 downTo 0) {
+            decrementKeyboardBacklight(DEVICE_ID)
+            assertEquals(
+                "Light value for level $level mismatched",
+                Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0),
+                lightColorMap[LIGHT_ID]
+            )
+            assertEquals(
+                "Light value for level $level must be correctly stored in the datastore",
+                BRIGHTNESS_VALUE_FOR_LEVEL[level],
+                dataStore.getKeyboardBacklightBrightness(
+                    keyboardWithBacklight.descriptor,
+                    LIGHT_ID
+                ).asInt
+            )
+        }
 
-        keyboardBacklightController.decrementKeyboardBacklight(DEVICE_ID)
-        testLooper.dispatchNext()
+        // Decrement below min level
+        decrementKeyboardBacklight(DEVICE_ID)
         assertEquals(
             "Light value for min level mismatched",
-            Color.argb(BRIGHTNESS_LEVELS.first(), 0, 0, 0),
+            Color.argb(0, 0, 0, 0),
             lightColorMap[LIGHT_ID]
         )
         assertEquals(
             "Light value for min level must be correctly stored in the datastore",
-            BRIGHTNESS_LEVELS.first(),
+            0,
             dataStore.getKeyboardBacklightBrightness(
                 keyboardWithBacklight.descriptor,
                 LIGHT_ID
@@ -240,7 +211,7 @@
         `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardInputLight))
         keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
 
-        keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID)
+        incrementKeyboardBacklight(DEVICE_ID)
         assertTrue("Non Keyboard backlights should not change", lightColorMap.isEmpty())
     }
 
@@ -258,8 +229,7 @@
         )
         keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
 
-        keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID)
-        testLooper.dispatchNext()
+        incrementKeyboardBacklight(DEVICE_ID)
         assertEquals("Only keyboard backlights should change", 1, lightColorMap.size)
         assertNotNull("Keyboard backlight should change", lightColorMap[LIGHT_ID])
         assertNull("Input lights should not change", lightColorMap[SECOND_LIGHT_ID])
@@ -275,14 +245,15 @@
         dataStore.setKeyboardBacklightBrightness(
             keyboardWithBacklight.descriptor,
             LIGHT_ID,
-            BRIGHTNESS_LEVELS.last()
+            MAX_BRIGHTNESS
         )
-        lightColorMap.clear()
 
         keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+        keyboardBacklightController.notifyUserActivity()
+        testLooper.dispatchNext()
         assertEquals(
             "Keyboard backlight level should be restored to the level saved in the data store",
-            Color.argb(BRIGHTNESS_LEVELS.last(), 0, 0, 0),
+            Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
             lightColorMap[LIGHT_ID]
         )
     }
@@ -295,11 +266,12 @@
         dataStore.setKeyboardBacklightBrightness(
             keyboardWithBacklight.descriptor,
             LIGHT_ID,
-            BRIGHTNESS_LEVELS.last()
+            MAX_BRIGHTNESS
         )
-        lightColorMap.clear()
 
         keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+        keyboardBacklightController.notifyUserActivity()
+        testLooper.dispatchNext()
         assertTrue(
             "Keyboard backlight should not be changed until its added",
             lightColorMap.isEmpty()
@@ -307,22 +279,22 @@
 
         `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
         keyboardBacklightController.onInputDeviceChanged(DEVICE_ID)
+        keyboardBacklightController.notifyUserActivity()
+        testLooper.dispatchNext()
         assertEquals(
             "Keyboard backlight level should be restored to the level saved in the data store",
-            Color.argb(BRIGHTNESS_LEVELS.last(), 0, 0, 0),
+            Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
             lightColorMap[LIGHT_ID]
         )
     }
 
     @Test
-    fun testKeyboardBacklightT_registerUnregisterListener() {
+    fun testKeyboardBacklight_registerUnregisterListener() {
         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)
-        // Initially backlight is at min
-        lightColorMap[LIGHT_ID] = Color.argb(BRIGHTNESS_LEVELS.first(), 0, 0, 0)
 
         // Register backlight listener
         val listener = KeyboardBacklightListener()
@@ -343,8 +315,8 @@
             lastBacklightState!!.brightnessLevel
         )
         assertEquals(
-            "Backlight state maxBrightnessLevel should be " + (BRIGHTNESS_LEVELS.size - 1),
-            (BRIGHTNESS_LEVELS.size - 1),
+            "Backlight state maxBrightnessLevel should be " + (BRIGHTNESS_VALUE_FOR_LEVEL.size - 1),
+            (BRIGHTNESS_VALUE_FOR_LEVEL.size - 1),
             lastBacklightState!!.maxBrightnessLevel
         )
         assertEquals(
@@ -357,12 +329,70 @@
         keyboardBacklightController.unregisterKeyboardBacklightListener(listener, 0)
 
         lastBacklightState = null
-        keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID)
-        testLooper.dispatchNext()
+        incrementKeyboardBacklight(DEVICE_ID)
 
         assertNull("Listener should not receive any updates", lastBacklightState)
     }
 
+    @Test
+    fun testKeyboardBacklight_userActivity() {
+        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))
+        dataStore.setKeyboardBacklightBrightness(
+            keyboardWithBacklight.descriptor,
+            LIGHT_ID,
+            MAX_BRIGHTNESS
+        )
+
+        keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+        keyboardBacklightController.notifyUserActivity()
+        testLooper.dispatchNext()
+        assertEquals(
+            "Keyboard backlight level should be restored to the level saved in the data store",
+            Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
+            lightColorMap[LIGHT_ID]
+        )
+
+        testLooper.moveTimeForward(USER_INACTIVITY_THRESHOLD_MILLIS + 1000)
+        testLooper.dispatchNext()
+        assertEquals(
+            "Keyboard backlight level should be turned off after inactivity",
+            0,
+            lightColorMap[LIGHT_ID]
+        )
+    }
+
+    @Test
+    fun testKeyboardBacklight_displayOnOff() {
+        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))
+        dataStore.setKeyboardBacklightBrightness(
+            keyboardWithBacklight.descriptor,
+            LIGHT_ID,
+            MAX_BRIGHTNESS
+        )
+
+        keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+        keyboardBacklightController.handleInteractiveStateChange(true /* isDisplayOn */)
+        assertEquals(
+            "Keyboard backlight level should be restored to the level saved in the data " +
+                    "store when display turned on",
+            Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
+            lightColorMap[LIGHT_ID]
+        )
+
+        keyboardBacklightController.handleInteractiveStateChange(false /* isDisplayOn */)
+        assertEquals(
+            "Keyboard backlight level should be turned off after display is turned off",
+            0,
+            lightColorMap[LIGHT_ID]
+        )
+    }
+
     inner class KeyboardBacklightListener : IKeyboardBacklightListener.Stub() {
         override fun onBrightnessChanged(
             deviceId: Int,
@@ -378,6 +408,18 @@
         }
     }
 
+    private fun incrementKeyboardBacklight(deviceId: Int) {
+        keyboardBacklightController.incrementKeyboardBacklight(deviceId)
+        keyboardBacklightController.notifyUserActivity()
+        testLooper.dispatchAll()
+    }
+
+    private fun decrementKeyboardBacklight(deviceId: Int) {
+        keyboardBacklightController.decrementKeyboardBacklight(deviceId)
+        keyboardBacklightController.notifyUserActivity()
+        testLooper.dispatchAll()
+    }
+
     class KeyboardBacklightState(
         val deviceId: Int,
         val brightnessLevel: Int,