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.annotation.BinderThread;
21 import android.annotation.Nullable;
22 import android.content.Context;
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.Handler;
29 import android.os.IBinder;
30 import android.os.Looper;
31 import android.os.Message;
32 import android.os.RemoteException;
33 import android.os.SystemClock;
34 import android.os.UEventObserver;
35 import android.text.TextUtils;
36 import android.util.IndentingPrintWriter;
37 import android.util.Log;
38 import android.util.Slog;
39 import android.util.SparseArray;
40 import android.view.InputDevice;
41 
42 import com.android.internal.annotations.GuardedBy;
43 import com.android.internal.annotations.VisibleForTesting;
44 
45 import java.io.PrintWriter;
46 import java.time.Duration;
47 import java.util.Arrays;
48 import java.util.Objects;
49 import java.util.OptionalInt;
50 import java.util.TreeSet;
51 
52 /**
53  * A thread-safe component of {@link InputManagerService} responsible for managing the keyboard
54  * backlight for supported keyboards.
55  */
56 final class KeyboardBacklightController implements
57         InputManagerService.KeyboardBacklightControllerInterface, InputManager.InputDeviceListener {
58 
59     private static final String TAG = "KbdBacklightController";
60 
61     // To enable these logs, run:
62     // 'adb shell setprop log.tag.KbdBacklightController DEBUG' (requires restart)
63     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
64 
65     private enum Direction {
66         DIRECTION_UP, DIRECTION_DOWN
67     }
68     private static final int MSG_UPDATE_EXISTING_DEVICES = 1;
69     private static final int MSG_INCREMENT_KEYBOARD_BACKLIGHT = 2;
70     private static final int MSG_DECREMENT_KEYBOARD_BACKLIGHT = 3;
71     private static final int MSG_NOTIFY_USER_ACTIVITY = 4;
72     private static final int MSG_NOTIFY_USER_INACTIVITY = 5;
73     private static final int MSG_INTERACTIVE_STATE_CHANGED = 6;
74     private static final int MAX_BRIGHTNESS = 255;
75     private static final int DEFAULT_NUM_BRIGHTNESS_CHANGE_STEPS = 10;
76     @VisibleForTesting
77     static final int MAX_BRIGHTNESS_CHANGE_STEPS = 10;
78     private static final long TRANSITION_ANIMATION_DURATION_MILLIS =
79             Duration.ofSeconds(1).toMillis();
80 
81     private static final String UEVENT_KEYBOARD_BACKLIGHT_TAG = "kbd_backlight";
82 
83     @VisibleForTesting
84     static final long USER_INACTIVITY_THRESHOLD_MILLIS = Duration.ofSeconds(30).toMillis();
85 
86     @VisibleForTesting
87     static final int[] DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL =
88             new int[DEFAULT_NUM_BRIGHTNESS_CHANGE_STEPS + 1];
89 
90     private final Context mContext;
91     private final NativeInputManagerService mNative;
92     // The PersistentDataStore should be locked before use.
93     @GuardedBy("mDataStore")
94     private final PersistentDataStore mDataStore;
95     private final Handler mHandler;
96     private final AnimatorFactory mAnimatorFactory;
97     private final UEventManager mUEventManager;
98     // Always access on handler thread or need to lock this for synchronization.
99     private final SparseArray<KeyboardBacklightState> mKeyboardBacklights = new SparseArray<>(1);
100     // Maintains state if all backlights should be on or turned off
101     private boolean mIsBacklightOn = false;
102     // Maintains state if currently the device is interactive or not
103     private boolean mIsInteractive = true;
104 
105     // List of currently registered keyboard backlight listeners
106     @GuardedBy("mKeyboardBacklightListenerRecords")
107     private final SparseArray<KeyboardBacklightListenerRecord> mKeyboardBacklightListenerRecords =
108             new SparseArray<>();
109 
110     private final AmbientKeyboardBacklightController mAmbientController;
111     @Nullable
112     private AmbientKeyboardBacklightController.AmbientKeyboardBacklightListener mAmbientListener;
113 
114     private int mAmbientBacklightValue = 0;
115 
116     static {
117         // Fixed brightness levels to avoid issues when converting back and forth from the
118         // device brightness range to [0-255]
119         // Levels are: 0, 51, ..., 255
120         for (int i = 0; i <= DEFAULT_NUM_BRIGHTNESS_CHANGE_STEPS; i++) {
121             DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[i] = (int) Math.floor(
122                     ((float) i * MAX_BRIGHTNESS) / DEFAULT_NUM_BRIGHTNESS_CHANGE_STEPS);
123         }
124     }
125 
KeyboardBacklightController(Context context, NativeInputManagerService nativeService, PersistentDataStore dataStore, Looper looper, UEventManager uEventManager)126     KeyboardBacklightController(Context context, NativeInputManagerService nativeService,
127             PersistentDataStore dataStore, Looper looper, UEventManager uEventManager) {
128         this(context, nativeService, dataStore, looper, ValueAnimator::ofInt, uEventManager);
129     }
130 
131     @VisibleForTesting
KeyboardBacklightController(Context context, NativeInputManagerService nativeService, PersistentDataStore dataStore, Looper looper, AnimatorFactory animatorFactory, UEventManager uEventManager)132     KeyboardBacklightController(Context context, NativeInputManagerService nativeService,
133             PersistentDataStore dataStore, Looper looper, AnimatorFactory animatorFactory,
134             UEventManager uEventManager) {
135         mContext = context;
136         mNative = nativeService;
137         mDataStore = dataStore;
138         mHandler = new Handler(looper, this::handleMessage);
139         mAnimatorFactory = animatorFactory;
140         mAmbientController = new AmbientKeyboardBacklightController(context, looper);
141         mUEventManager = uEventManager;
142     }
143 
144     @Override
systemRunning()145     public void systemRunning() {
146         InputManager inputManager = Objects.requireNonNull(
147                 mContext.getSystemService(InputManager.class));
148         inputManager.registerInputDeviceListener(this, mHandler);
149 
150         Message msg = Message.obtain(mHandler, MSG_UPDATE_EXISTING_DEVICES,
151                 inputManager.getInputDeviceIds());
152         mHandler.sendMessage(msg);
153 
154         // Observe UEvents for "kbd_backlight" sysfs nodes.
155         // We want to observe creation of such LED nodes since they might be created after device
156         // FD created and InputDevice creation logic doesn't initialize LED nodes which leads to
157         // backlight not working.
158         mUEventManager.addListener(new UEventManager.UEventListener() {
159             @Override
160             public void onUEvent(UEventObserver.UEvent event) {
161                 onKeyboardBacklightUEvent(event);
162             }
163         }, UEVENT_KEYBOARD_BACKLIGHT_TAG);
164 
165         if (InputFeatureFlagProvider.isAmbientKeyboardBacklightControlEnabled()) {
166             // Start ambient backlight controller
167             mAmbientController.systemRunning();
168         }
169     }
170 
171     @Override
incrementKeyboardBacklight(int deviceId)172     public void incrementKeyboardBacklight(int deviceId) {
173         Message msg = Message.obtain(mHandler, MSG_INCREMENT_KEYBOARD_BACKLIGHT, deviceId);
174         mHandler.sendMessage(msg);
175     }
176 
177     @Override
decrementKeyboardBacklight(int deviceId)178     public void decrementKeyboardBacklight(int deviceId) {
179         Message msg = Message.obtain(mHandler, MSG_DECREMENT_KEYBOARD_BACKLIGHT, deviceId);
180         mHandler.sendMessage(msg);
181     }
182 
183     @Override
notifyUserActivity()184     public void notifyUserActivity() {
185         Message msg = Message.obtain(mHandler, MSG_NOTIFY_USER_ACTIVITY);
186         mHandler.sendMessage(msg);
187     }
188 
189     @Override
onInteractiveChanged(boolean isInteractive)190     public void onInteractiveChanged(boolean isInteractive) {
191         Message msg = Message.obtain(mHandler, MSG_INTERACTIVE_STATE_CHANGED, isInteractive);
192         mHandler.sendMessage(msg);
193     }
194 
updateKeyboardBacklight(int deviceId, Direction direction)195     private void updateKeyboardBacklight(int deviceId, Direction direction) {
196         InputDevice inputDevice = getInputDevice(deviceId);
197         KeyboardBacklightState state = mKeyboardBacklights.get(deviceId);
198         if (inputDevice == null || state == null) {
199             return;
200         }
201         // Follow preset levels of brightness defined in BRIGHTNESS_LEVELS
202         final int currBrightnessLevel;
203         if (state.mUseAmbientController) {
204             int index = Arrays.binarySearch(state.mBrightnessValueForLevel, mAmbientBacklightValue);
205             // Set current level to the lower bound of the ambient value in the brightness array.
206             if (index < 0) {
207                 int lowerBound = Math.max(0, -(index + 1) - 1);
208                 currBrightnessLevel =
209                         direction == Direction.DIRECTION_UP ? lowerBound : lowerBound + 1;
210             } else {
211                 currBrightnessLevel = index;
212             }
213         } else {
214             currBrightnessLevel = state.mBrightnessLevel;
215         }
216         final int newBrightnessLevel;
217         if (direction == Direction.DIRECTION_UP) {
218             newBrightnessLevel = Math.min(currBrightnessLevel + 1,
219                     state.getNumBrightnessChangeSteps());
220         } else {
221             newBrightnessLevel = Math.max(currBrightnessLevel - 1, 0);
222         }
223 
224         state.setBrightnessLevel(newBrightnessLevel);
225 
226         // Might need to stop listening to ALS since user has manually selected backlight
227         // level through keyboard up/down button
228         updateAmbientLightListener();
229 
230         maybeBackupBacklightBrightness(inputDevice, state.mLight,
231                 state.mBrightnessValueForLevel[newBrightnessLevel]);
232 
233         if (DEBUG) {
234             Slog.d(TAG,
235                     "Changing state from " + state.mBrightnessLevel + " to " + newBrightnessLevel);
236         }
237 
238         synchronized (mKeyboardBacklightListenerRecords) {
239             for (int i = 0; i < mKeyboardBacklightListenerRecords.size(); i++) {
240                 IKeyboardBacklightState callbackState = new IKeyboardBacklightState();
241                 callbackState.brightnessLevel = newBrightnessLevel;
242                 callbackState.maxBrightnessLevel = state.getNumBrightnessChangeSteps();
243                 mKeyboardBacklightListenerRecords.valueAt(i).notifyKeyboardBacklightChanged(
244                         deviceId, callbackState, true);
245             }
246         }
247     }
248 
maybeBackupBacklightBrightness(InputDevice inputDevice, Light keyboardBacklight, int brightnessValue)249     private void maybeBackupBacklightBrightness(InputDevice inputDevice, Light keyboardBacklight,
250             int brightnessValue) {
251         // Don't back up or restore when ALS based keyboard backlight is enabled
252         if (InputFeatureFlagProvider.isAmbientKeyboardBacklightControlEnabled()) {
253             return;
254         }
255         synchronized (mDataStore) {
256             try {
257                 mDataStore.setKeyboardBacklightBrightness(inputDevice.getDescriptor(),
258                         keyboardBacklight.getId(),
259                         brightnessValue);
260             } finally {
261                 mDataStore.saveIfNeeded();
262             }
263         }
264     }
265 
maybeRestoreBacklightBrightness(InputDevice inputDevice, Light keyboardBacklight)266     private void maybeRestoreBacklightBrightness(InputDevice inputDevice, Light keyboardBacklight) {
267         // Don't back up or restore when ALS based keyboard backlight is enabled
268         if (InputFeatureFlagProvider.isAmbientKeyboardBacklightControlEnabled()) {
269             return;
270         }
271         KeyboardBacklightState state = mKeyboardBacklights.get(inputDevice.getId());
272         OptionalInt brightness;
273         synchronized (mDataStore) {
274             brightness = mDataStore.getKeyboardBacklightBrightness(
275                     inputDevice.getDescriptor(), keyboardBacklight.getId());
276         }
277         if (state != null && brightness.isPresent()) {
278             int brightnessValue = Math.max(0, Math.min(MAX_BRIGHTNESS, brightness.getAsInt()));
279             int newLevel = Arrays.binarySearch(state.mBrightnessValueForLevel, brightnessValue);
280             if (newLevel < 0) {
281                 newLevel = Math.min(state.getNumBrightnessChangeSteps(), -(newLevel + 1));
282             }
283             state.setBrightnessLevel(newLevel);
284             if (DEBUG) {
285                 Slog.d(TAG, "Restoring brightness level " + brightness.getAsInt());
286             }
287         }
288     }
289 
handleUserActivity()290     private void handleUserActivity() {
291         // Ignore user activity if device is not interactive. When device becomes interactive, we
292         // will send another user activity to turn backlight on.
293         if (!mIsInteractive) {
294             return;
295         }
296         mIsBacklightOn = true;
297         for (int i = 0; i < mKeyboardBacklights.size(); i++) {
298             KeyboardBacklightState state = mKeyboardBacklights.valueAt(i);
299             state.onBacklightStateChanged();
300         }
301         mHandler.removeMessages(MSG_NOTIFY_USER_INACTIVITY);
302         mHandler.sendEmptyMessageAtTime(MSG_NOTIFY_USER_INACTIVITY,
303                 SystemClock.uptimeMillis() + USER_INACTIVITY_THRESHOLD_MILLIS);
304     }
305 
handleUserInactivity()306     private void handleUserInactivity() {
307         mIsBacklightOn = false;
308         for (int i = 0; i < mKeyboardBacklights.size(); i++) {
309             KeyboardBacklightState state = mKeyboardBacklights.valueAt(i);
310             state.onBacklightStateChanged();
311         }
312     }
313 
314     @VisibleForTesting
handleInteractiveStateChange(boolean isInteractive)315     public void handleInteractiveStateChange(boolean isInteractive) {
316         // Interactive state changes should force the keyboard to turn on/off irrespective of
317         // whether time out occurred or not.
318         mIsInteractive = isInteractive;
319         if (isInteractive) {
320             handleUserActivity();
321         } else {
322             handleUserInactivity();
323         }
324         updateAmbientLightListener();
325     }
326 
327     @VisibleForTesting
handleAmbientLightValueChanged(int brightnessValue)328     public void handleAmbientLightValueChanged(int brightnessValue) {
329         mAmbientBacklightValue = brightnessValue;
330         for (int i = 0; i < mKeyboardBacklights.size(); i++) {
331             KeyboardBacklightState state = mKeyboardBacklights.valueAt(i);
332             state.onAmbientBacklightValueChanged();
333         }
334     }
335 
handleMessage(Message msg)336     private boolean handleMessage(Message msg) {
337         switch (msg.what) {
338             case MSG_UPDATE_EXISTING_DEVICES:
339                 for (int deviceId : (int[]) msg.obj) {
340                     onInputDeviceAdded(deviceId);
341                 }
342                 return true;
343             case MSG_INCREMENT_KEYBOARD_BACKLIGHT:
344                 updateKeyboardBacklight((int) msg.obj, Direction.DIRECTION_UP);
345                 return true;
346             case MSG_DECREMENT_KEYBOARD_BACKLIGHT:
347                 updateKeyboardBacklight((int) msg.obj, Direction.DIRECTION_DOWN);
348                 return true;
349             case MSG_NOTIFY_USER_ACTIVITY:
350                 handleUserActivity();
351                 return true;
352             case MSG_NOTIFY_USER_INACTIVITY:
353                 handleUserInactivity();
354                 return true;
355             case MSG_INTERACTIVE_STATE_CHANGED:
356                 handleInteractiveStateChange((boolean) msg.obj);
357                 return true;
358         }
359         return false;
360     }
361 
362     @VisibleForTesting
363     @Override
onInputDeviceAdded(int deviceId)364     public void onInputDeviceAdded(int deviceId) {
365         onInputDeviceChanged(deviceId);
366         updateAmbientLightListener();
367     }
368 
369     @VisibleForTesting
370     @Override
onInputDeviceRemoved(int deviceId)371     public void onInputDeviceRemoved(int deviceId) {
372         mKeyboardBacklights.remove(deviceId);
373         updateAmbientLightListener();
374     }
375 
376     @VisibleForTesting
377     @Override
onInputDeviceChanged(int deviceId)378     public void onInputDeviceChanged(int deviceId) {
379         InputDevice inputDevice = getInputDevice(deviceId);
380         if (inputDevice == null) {
381             return;
382         }
383         final Light keyboardBacklight = getKeyboardBacklight(inputDevice);
384         if (keyboardBacklight == null) {
385             mKeyboardBacklights.remove(deviceId);
386             return;
387         }
388         KeyboardBacklightState state = mKeyboardBacklights.get(deviceId);
389         if (state != null && state.mLight.getId() == keyboardBacklight.getId()) {
390             return;
391         }
392         // The keyboard backlight was added or changed.
393         mKeyboardBacklights.put(deviceId, new KeyboardBacklightState(deviceId, keyboardBacklight));
394         maybeRestoreBacklightBrightness(inputDevice, keyboardBacklight);
395     }
396 
getInputDevice(int deviceId)397     private InputDevice getInputDevice(int deviceId) {
398         InputManager inputManager = mContext.getSystemService(InputManager.class);
399         return inputManager != null ? inputManager.getInputDevice(deviceId) : null;
400     }
401 
getKeyboardBacklight(InputDevice inputDevice)402     private Light getKeyboardBacklight(InputDevice inputDevice) {
403         // Assuming each keyboard can have only single Light node for Keyboard backlight control
404         // for simplicity.
405         for (Light light : inputDevice.getLightsManager().getLights()) {
406             if (light.getType() == Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT
407                     && light.hasBrightnessControl()) {
408                 return light;
409             }
410         }
411         return null;
412     }
413 
414     /** Register the keyboard backlight listener for a process. */
415     @BinderThread
416     @Override
registerKeyboardBacklightListener(IKeyboardBacklightListener listener, int pid)417     public void registerKeyboardBacklightListener(IKeyboardBacklightListener listener,
418             int pid) {
419         synchronized (mKeyboardBacklightListenerRecords) {
420             if (mKeyboardBacklightListenerRecords.get(pid) != null) {
421                 throw new IllegalStateException("The calling process has already registered "
422                         + "a KeyboardBacklightListener.");
423             }
424             KeyboardBacklightListenerRecord record = new KeyboardBacklightListenerRecord(pid,
425                     listener);
426             try {
427                 listener.asBinder().linkToDeath(record, 0);
428             } catch (RemoteException ex) {
429                 throw new RuntimeException(ex);
430             }
431             mKeyboardBacklightListenerRecords.put(pid, record);
432         }
433     }
434 
435     /** Unregister the keyboard backlight listener for a process. */
436     @BinderThread
437     @Override
unregisterKeyboardBacklightListener(IKeyboardBacklightListener listener, int pid)438     public void unregisterKeyboardBacklightListener(IKeyboardBacklightListener listener,
439             int pid) {
440         synchronized (mKeyboardBacklightListenerRecords) {
441             KeyboardBacklightListenerRecord record = mKeyboardBacklightListenerRecords.get(pid);
442             if (record == null) {
443                 throw new IllegalStateException("The calling process has no registered "
444                         + "KeyboardBacklightListener.");
445             }
446             if (record.mListener.asBinder() != listener.asBinder()) {
447                 throw new IllegalStateException("The calling process has a different registered "
448                         + "KeyboardBacklightListener.");
449             }
450             record.mListener.asBinder().unlinkToDeath(record, 0);
451             mKeyboardBacklightListenerRecords.remove(pid);
452         }
453     }
454 
onKeyboardBacklightListenerDied(int pid)455     private void onKeyboardBacklightListenerDied(int pid) {
456         synchronized (mKeyboardBacklightListenerRecords) {
457             mKeyboardBacklightListenerRecords.remove(pid);
458         }
459     }
460 
461     @VisibleForTesting
onKeyboardBacklightUEvent(UEventObserver.UEvent event)462     public void onKeyboardBacklightUEvent(UEventObserver.UEvent event) {
463         if ("ADD".equalsIgnoreCase(event.get("ACTION")) && "LEDS".equalsIgnoreCase(
464                 event.get("SUBSYSTEM"))) {
465             final String devPath = event.get("DEVPATH");
466             if (isValidBacklightNodePath(devPath)) {
467                 mNative.sysfsNodeChanged("/sys" + devPath);
468             }
469         }
470     }
471 
updateAmbientLightListener()472     private void updateAmbientLightListener() {
473         if (!InputFeatureFlagProvider.isAmbientKeyboardBacklightControlEnabled()) {
474             return;
475         }
476         boolean needToListenAmbientLightSensor = false;
477         for (int i = 0; i < mKeyboardBacklights.size(); i++) {
478             needToListenAmbientLightSensor |= mKeyboardBacklights.valueAt(i).mUseAmbientController;
479         }
480         needToListenAmbientLightSensor &= mIsInteractive;
481         if (needToListenAmbientLightSensor && mAmbientListener == null) {
482             mAmbientListener = this::handleAmbientLightValueChanged;
483             mAmbientController.registerAmbientBacklightListener(mAmbientListener);
484         }
485         if (!needToListenAmbientLightSensor && mAmbientListener != null) {
486             mAmbientController.unregisterAmbientBacklightListener(mAmbientListener);
487             mAmbientListener = null;
488         }
489     }
490 
isValidBacklightNodePath(String devPath)491     private static boolean isValidBacklightNodePath(String devPath) {
492         if (TextUtils.isEmpty(devPath)) {
493             return false;
494         }
495         int index = devPath.lastIndexOf('/');
496         if (index < 0) {
497             return false;
498         }
499         String backlightNode = devPath.substring(index + 1);
500         devPath = devPath.substring(0, index);
501         if (!devPath.endsWith("leds") || !backlightNode.contains("kbd_backlight")) {
502             return false;
503         }
504         index = devPath.lastIndexOf('/');
505         return index >= 0;
506     }
507 
508     @Override
dump(PrintWriter pw)509     public void dump(PrintWriter pw) {
510         IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
511         ipw.println(TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights");
512         ipw.increaseIndent();
513         for (int i = 0; i < mKeyboardBacklights.size(); i++) {
514             KeyboardBacklightState state = mKeyboardBacklights.valueAt(i);
515             ipw.println(i + ": " + state.toString());
516         }
517         ipw.decreaseIndent();
518     }
519 
520     // A record of a registered Keyboard backlight listener from one process.
521     private class KeyboardBacklightListenerRecord implements IBinder.DeathRecipient {
522         public final int mPid;
523         public final IKeyboardBacklightListener mListener;
524 
KeyboardBacklightListenerRecord(int pid, IKeyboardBacklightListener listener)525         KeyboardBacklightListenerRecord(int pid, IKeyboardBacklightListener listener) {
526             mPid = pid;
527             mListener = listener;
528         }
529 
530         @Override
binderDied()531         public void binderDied() {
532             if (DEBUG) {
533                 Slog.d(TAG, "Keyboard backlight listener for pid " + mPid + " died.");
534             }
535             onKeyboardBacklightListenerDied(mPid);
536         }
537 
notifyKeyboardBacklightChanged(int deviceId, IKeyboardBacklightState state, boolean isTriggeredByKeyPress)538         public void notifyKeyboardBacklightChanged(int deviceId, IKeyboardBacklightState state,
539                 boolean isTriggeredByKeyPress) {
540             try {
541                 mListener.onBrightnessChanged(deviceId, state, isTriggeredByKeyPress);
542             } catch (RemoteException ex) {
543                 Slog.w(TAG, "Failed to notify process " + mPid
544                         + " that keyboard backlight changed, assuming it died.", ex);
545                 binderDied();
546             }
547         }
548     }
549 
550     private class KeyboardBacklightState {
551         private final int mDeviceId;
552         private final Light mLight;
553         private int mBrightnessLevel;
554         private ValueAnimator mAnimator;
555         private final int[] mBrightnessValueForLevel;
556         private boolean mUseAmbientController =
557                 InputFeatureFlagProvider.isAmbientKeyboardBacklightControlEnabled();
558 
KeyboardBacklightState(int deviceId, Light light)559         KeyboardBacklightState(int deviceId, Light light) {
560             mDeviceId = deviceId;
561             mLight = light;
562             mBrightnessValueForLevel = setupBrightnessLevels();
563         }
564 
setupBrightnessLevels()565         private int[] setupBrightnessLevels() {
566             if (!InputFeatureFlagProvider.isKeyboardBacklightCustomLevelsEnabled()) {
567                 return DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL;
568             }
569             int[] customLevels = mLight.getPreferredBrightnessLevels();
570             if (customLevels == null || customLevels.length == 0) {
571                 return DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL;
572             }
573             TreeSet<Integer> brightnessLevels = new TreeSet<>();
574             brightnessLevels.add(0);
575             for (int level : customLevels) {
576                 if (level > 0 && level < MAX_BRIGHTNESS) {
577                     brightnessLevels.add(level);
578                 }
579             }
580             brightnessLevels.add(MAX_BRIGHTNESS);
581             int brightnessChangeSteps = brightnessLevels.size() - 1;
582             if (brightnessChangeSteps > MAX_BRIGHTNESS_CHANGE_STEPS) {
583                 return DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL;
584             }
585             int[] result = new int[brightnessLevels.size()];
586             int index = 0;
587             for (int val : brightnessLevels) {
588                 result[index++] = val;
589             }
590             return result;
591         }
592 
getNumBrightnessChangeSteps()593         private int getNumBrightnessChangeSteps() {
594             return mBrightnessValueForLevel.length - 1;
595         }
596 
onBacklightStateChanged()597         private void onBacklightStateChanged() {
598             int toValue = mUseAmbientController ? mAmbientBacklightValue
599                     : mBrightnessValueForLevel[mBrightnessLevel];
600             setBacklightValue(mIsBacklightOn ? toValue : 0);
601         }
setBrightnessLevel(int brightnessLevel)602         private void setBrightnessLevel(int brightnessLevel) {
603             // Once we manually set level, disregard ambient light controller
604             mUseAmbientController = false;
605             if (mIsBacklightOn) {
606                 setBacklightValue(mBrightnessValueForLevel[brightnessLevel]);
607             }
608             mBrightnessLevel = brightnessLevel;
609         }
610 
onAmbientBacklightValueChanged()611         private void onAmbientBacklightValueChanged() {
612             if (mIsBacklightOn && mUseAmbientController) {
613                 setBacklightValue(mAmbientBacklightValue);
614             }
615         }
616 
cancelAnimation()617         private void cancelAnimation() {
618             if (mAnimator != null && mAnimator.isRunning()) {
619                 mAnimator.cancel();
620             }
621         }
622 
setBacklightValue(int toValue)623         private void setBacklightValue(int toValue) {
624             int fromValue = Color.alpha(mNative.getLightColor(mDeviceId, mLight.getId()));
625             if (fromValue == toValue) {
626                 return;
627             }
628             if (InputFeatureFlagProvider.isKeyboardBacklightAnimationEnabled()) {
629                 startAnimation(fromValue, toValue);
630             } else {
631                 mNative.setLightColor(mDeviceId, mLight.getId(), Color.argb(toValue, 0, 0, 0));
632             }
633         }
634 
startAnimation(int fromValue, int toValue)635         private void startAnimation(int fromValue, int toValue) {
636             // Cancel any ongoing animation before starting a new one
637             cancelAnimation();
638             mAnimator = mAnimatorFactory.makeIntAnimator(fromValue, toValue);
639             mAnimator.addUpdateListener(
640                     (animation) -> mNative.setLightColor(mDeviceId, mLight.getId(),
641                             Color.argb((int) animation.getAnimatedValue(), 0, 0, 0)));
642             mAnimator.setDuration(TRANSITION_ANIMATION_DURATION_MILLIS).start();
643         }
644 
645         @Override
toString()646         public String toString() {
647             return "KeyboardBacklightState{Light=" + mLight.getId()
648                     + ", BrightnessLevel=" + mBrightnessLevel
649                     + "}";
650         }
651     }
652 
653     @VisibleForTesting
654     interface AnimatorFactory {
makeIntAnimator(int from, int to)655         ValueAnimator makeIntAnimator(int from, int to);
656     }
657 }
658