• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.input;
18 
19 import android.animation.ValueAnimator;
20 import android.annotation.BinderThread;
21 import android.annotation.Nullable;
22 import android.content.Context;
23 import android.content.res.Resources;
24 import android.graphics.Color;
25 import android.hardware.input.IKeyboardBacklightListener;
26 import android.hardware.input.IKeyboardBacklightState;
27 import android.hardware.input.InputManager;
28 import android.hardware.lights.Light;
29 import android.os.Handler;
30 import android.os.IBinder;
31 import android.os.Looper;
32 import android.os.Message;
33 import android.os.RemoteException;
34 import android.os.SystemClock;
35 import android.sysprop.InputProperties;
36 import android.text.TextUtils;
37 import android.util.IndentingPrintWriter;
38 import android.util.Log;
39 import android.util.Slog;
40 import android.util.SparseArray;
41 import android.view.InputDevice;
42 
43 import com.android.internal.annotations.GuardedBy;
44 import com.android.internal.annotations.VisibleForTesting;
45 
46 import java.io.PrintWriter;
47 import java.time.Duration;
48 import java.util.Arrays;
49 import java.util.Objects;
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     // To disable Framework controlled keyboard backlight animation run:
66     // adb shell setprop persist.input.keyboard.backlight_animation.enabled false (requires restart)
67     private final boolean mKeyboardBacklightAnimationEnabled;
68 
69     private enum Direction {
70         DIRECTION_UP, DIRECTION_DOWN
71     }
72     private static final int MSG_UPDATE_EXISTING_DEVICES = 1;
73     private static final int MSG_INCREMENT_KEYBOARD_BACKLIGHT = 2;
74     private static final int MSG_DECREMENT_KEYBOARD_BACKLIGHT = 3;
75     private static final int MSG_NOTIFY_USER_ACTIVITY = 4;
76     private static final int MSG_NOTIFY_USER_INACTIVITY = 5;
77     private static final int MSG_INTERACTIVE_STATE_CHANGED = 6;
78     private static final int MAX_BRIGHTNESS = 255;
79     private static final int DEFAULT_NUM_BRIGHTNESS_CHANGE_STEPS = 10;
80     @VisibleForTesting
81     static final int MAX_BRIGHTNESS_CHANGE_STEPS = 10;
82     private static final long TRANSITION_ANIMATION_DURATION_MILLIS =
83             Duration.ofSeconds(1).toMillis();
84 
85     @VisibleForTesting
86     static final int[] DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL =
87             new int[DEFAULT_NUM_BRIGHTNESS_CHANGE_STEPS + 1];
88 
89     private final Context mContext;
90     private final NativeInputManagerService mNative;
91     private final Handler mHandler;
92     private final AnimatorFactory mAnimatorFactory;
93     // Always access on handler thread or need to lock this for synchronization.
94     private final SparseArray<KeyboardBacklightState> mKeyboardBacklights = new SparseArray<>(1);
95     // Maintains state if all backlights should be on or turned off
96     private boolean mIsBacklightOn = false;
97     // Maintains state if currently the device is interactive or not
98     private boolean mIsInteractive = true;
99 
100     // List of currently registered keyboard backlight listeners
101     @GuardedBy("mKeyboardBacklightListenerRecords")
102     private final SparseArray<KeyboardBacklightListenerRecord> mKeyboardBacklightListenerRecords =
103             new SparseArray<>();
104 
105     private final AmbientKeyboardBacklightController mAmbientController;
106     @Nullable
107     private AmbientKeyboardBacklightController.AmbientKeyboardBacklightListener mAmbientListener;
108 
109     private int mAmbientBacklightValue = 0;
110     private final int mUserInactivityThresholdMs;
111 
112     static {
113         // Fixed brightness levels to avoid issues when converting back and forth from the
114         // device brightness range to [0-255]
115         // Levels are: 0, 51, ..., 255
116         for (int i = 0; i <= DEFAULT_NUM_BRIGHTNESS_CHANGE_STEPS; i++) {
117             DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[i] = (int) Math.floor(
118                     ((float) i * MAX_BRIGHTNESS) / DEFAULT_NUM_BRIGHTNESS_CHANGE_STEPS);
119         }
120     }
121 
KeyboardBacklightController(Context context, NativeInputManagerService nativeService, Looper looper)122     KeyboardBacklightController(Context context, NativeInputManagerService nativeService,
123             Looper looper) {
124         this(context, nativeService, looper, ValueAnimator::ofInt);
125     }
126 
127     @VisibleForTesting
KeyboardBacklightController(Context context, NativeInputManagerService nativeService, Looper looper, AnimatorFactory animatorFactory)128     KeyboardBacklightController(Context context, NativeInputManagerService nativeService,
129             Looper looper, AnimatorFactory animatorFactory) {
130         mContext = context;
131         mNative = nativeService;
132         mHandler = new Handler(looper, this::handleMessage);
133         mAnimatorFactory = animatorFactory;
134         mAmbientController = new AmbientKeyboardBacklightController(context, looper);
135         Resources res = mContext.getResources();
136         mUserInactivityThresholdMs = res.getInteger(
137                 com.android.internal.R.integer.config_keyboardBacklightTimeoutMs);
138         mKeyboardBacklightAnimationEnabled =
139                 InputProperties.enable_keyboard_backlight_animation().orElse(false);
140     }
141 
142     @Override
systemRunning()143     public void systemRunning() {
144         InputManager inputManager = Objects.requireNonNull(
145                 mContext.getSystemService(InputManager.class));
146         inputManager.registerInputDeviceListener(this, mHandler);
147 
148         Message msg = Message.obtain(mHandler, MSG_UPDATE_EXISTING_DEVICES,
149                 inputManager.getInputDeviceIds());
150         mHandler.sendMessage(msg);
151 
152         // Start ambient backlight controller
153         mAmbientController.systemRunning();
154     }
155 
156     @Override
incrementKeyboardBacklight(int deviceId)157     public void incrementKeyboardBacklight(int deviceId) {
158         Message msg = Message.obtain(mHandler, MSG_INCREMENT_KEYBOARD_BACKLIGHT, deviceId);
159         mHandler.sendMessage(msg);
160     }
161 
162     @Override
decrementKeyboardBacklight(int deviceId)163     public void decrementKeyboardBacklight(int deviceId) {
164         Message msg = Message.obtain(mHandler, MSG_DECREMENT_KEYBOARD_BACKLIGHT, deviceId);
165         mHandler.sendMessage(msg);
166     }
167 
168     @Override
notifyUserActivity()169     public void notifyUserActivity() {
170         Message msg = Message.obtain(mHandler, MSG_NOTIFY_USER_ACTIVITY);
171         mHandler.sendMessage(msg);
172     }
173 
174     @Override
onInteractiveChanged(boolean isInteractive)175     public void onInteractiveChanged(boolean isInteractive) {
176         Message msg = Message.obtain(mHandler, MSG_INTERACTIVE_STATE_CHANGED, isInteractive);
177         mHandler.sendMessage(msg);
178     }
179 
updateKeyboardBacklight(int deviceId, Direction direction)180     private void updateKeyboardBacklight(int deviceId, Direction direction) {
181         InputDevice inputDevice = getInputDevice(deviceId);
182         KeyboardBacklightState state = mKeyboardBacklights.get(deviceId);
183         if (inputDevice == null || state == null) {
184             return;
185         }
186         // Follow preset levels of brightness defined in BRIGHTNESS_LEVELS
187         final int currBrightnessLevel;
188         if (state.mUseAmbientController) {
189             int index = Arrays.binarySearch(state.mBrightnessValueForLevel, mAmbientBacklightValue);
190             // Set current level to the lower bound of the ambient value in the brightness array.
191             if (index < 0) {
192                 int lowerBound = Math.max(0, -(index + 1) - 1);
193                 currBrightnessLevel =
194                         direction == Direction.DIRECTION_UP ? lowerBound : lowerBound + 1;
195             } else {
196                 currBrightnessLevel = index;
197             }
198         } else {
199             currBrightnessLevel = state.mBrightnessLevel;
200         }
201         final int newBrightnessLevel;
202         if (direction == Direction.DIRECTION_UP) {
203             newBrightnessLevel = Math.min(currBrightnessLevel + 1,
204                     state.getNumBrightnessChangeSteps());
205         } else {
206             newBrightnessLevel = Math.max(currBrightnessLevel - 1, 0);
207         }
208 
209         state.setBrightnessLevel(newBrightnessLevel);
210 
211         // Might need to stop listening to ALS since user has manually selected backlight
212         // level through keyboard up/down button
213         updateAmbientLightListener();
214 
215         if (DEBUG) {
216             Slog.d(TAG,
217                     "Changing state from " + state.mBrightnessLevel + " to " + newBrightnessLevel);
218         }
219 
220         synchronized (mKeyboardBacklightListenerRecords) {
221             for (int i = 0; i < mKeyboardBacklightListenerRecords.size(); i++) {
222                 IKeyboardBacklightState callbackState = new IKeyboardBacklightState();
223                 callbackState.brightnessLevel = newBrightnessLevel;
224                 callbackState.maxBrightnessLevel = state.getNumBrightnessChangeSteps();
225                 mKeyboardBacklightListenerRecords.valueAt(i).notifyKeyboardBacklightChanged(
226                         deviceId, callbackState, true);
227             }
228         }
229     }
230 
handleUserActivity()231     private void handleUserActivity() {
232         // Ignore user activity if device is not interactive. When device becomes interactive, we
233         // will send another user activity to turn backlight on.
234         if (!mIsInteractive) {
235             return;
236         }
237         mIsBacklightOn = true;
238         for (int i = 0; i < mKeyboardBacklights.size(); i++) {
239             KeyboardBacklightState state = mKeyboardBacklights.valueAt(i);
240             state.onBacklightStateChanged();
241         }
242         mHandler.removeMessages(MSG_NOTIFY_USER_INACTIVITY);
243         mHandler.sendEmptyMessageAtTime(MSG_NOTIFY_USER_INACTIVITY,
244                 SystemClock.uptimeMillis() + mUserInactivityThresholdMs);
245     }
246 
handleUserInactivity()247     private void handleUserInactivity() {
248         mIsBacklightOn = false;
249         for (int i = 0; i < mKeyboardBacklights.size(); i++) {
250             KeyboardBacklightState state = mKeyboardBacklights.valueAt(i);
251             state.onBacklightStateChanged();
252         }
253     }
254 
255     @VisibleForTesting
handleInteractiveStateChange(boolean isInteractive)256     public void handleInteractiveStateChange(boolean isInteractive) {
257         // Interactive state changes should force the keyboard to turn on/off irrespective of
258         // whether time out occurred or not.
259         mIsInteractive = isInteractive;
260         if (isInteractive) {
261             handleUserActivity();
262         } else {
263             handleUserInactivity();
264         }
265         updateAmbientLightListener();
266     }
267 
268     @VisibleForTesting
handleAmbientLightValueChanged(int brightnessValue)269     public void handleAmbientLightValueChanged(int brightnessValue) {
270         mAmbientBacklightValue = brightnessValue;
271         for (int i = 0; i < mKeyboardBacklights.size(); i++) {
272             KeyboardBacklightState state = mKeyboardBacklights.valueAt(i);
273             state.onAmbientBacklightValueChanged();
274         }
275     }
276 
handleMessage(Message msg)277     private boolean handleMessage(Message msg) {
278         switch (msg.what) {
279             case MSG_UPDATE_EXISTING_DEVICES:
280                 for (int deviceId : (int[]) msg.obj) {
281                     onInputDeviceAdded(deviceId);
282                 }
283                 return true;
284             case MSG_INCREMENT_KEYBOARD_BACKLIGHT:
285                 updateKeyboardBacklight((int) msg.obj, Direction.DIRECTION_UP);
286                 return true;
287             case MSG_DECREMENT_KEYBOARD_BACKLIGHT:
288                 updateKeyboardBacklight((int) msg.obj, Direction.DIRECTION_DOWN);
289                 return true;
290             case MSG_NOTIFY_USER_ACTIVITY:
291                 handleUserActivity();
292                 return true;
293             case MSG_NOTIFY_USER_INACTIVITY:
294                 handleUserInactivity();
295                 return true;
296             case MSG_INTERACTIVE_STATE_CHANGED:
297                 handleInteractiveStateChange((boolean) msg.obj);
298                 return true;
299         }
300         return false;
301     }
302 
303     @VisibleForTesting
304     @Override
onInputDeviceAdded(int deviceId)305     public void onInputDeviceAdded(int deviceId) {
306         onInputDeviceChanged(deviceId);
307         updateAmbientLightListener();
308     }
309 
310     @VisibleForTesting
311     @Override
onInputDeviceRemoved(int deviceId)312     public void onInputDeviceRemoved(int deviceId) {
313         mKeyboardBacklights.remove(deviceId);
314         updateAmbientLightListener();
315     }
316 
317     @VisibleForTesting
318     @Override
onInputDeviceChanged(int deviceId)319     public void onInputDeviceChanged(int deviceId) {
320         InputDevice inputDevice = getInputDevice(deviceId);
321         if (inputDevice == null) {
322             return;
323         }
324         final Light keyboardBacklight = getKeyboardBacklight(inputDevice);
325         if (keyboardBacklight == null) {
326             mKeyboardBacklights.remove(deviceId);
327             return;
328         }
329         KeyboardBacklightState state = mKeyboardBacklights.get(deviceId);
330         if (state != null && state.mLight.getId() == keyboardBacklight.getId()) {
331             return;
332         }
333         // The keyboard backlight was added or changed.
334         mKeyboardBacklights.put(deviceId, new KeyboardBacklightState(deviceId, keyboardBacklight));
335     }
336 
getInputDevice(int deviceId)337     private InputDevice getInputDevice(int deviceId) {
338         InputManager inputManager = mContext.getSystemService(InputManager.class);
339         return inputManager != null ? inputManager.getInputDevice(deviceId) : null;
340     }
341 
getKeyboardBacklight(InputDevice inputDevice)342     private Light getKeyboardBacklight(InputDevice inputDevice) {
343         // Assuming each keyboard can have only single Light node for Keyboard backlight control
344         // for simplicity.
345         for (Light light : inputDevice.getLightsManager().getLights()) {
346             if (light.getType() == Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT
347                     && light.hasBrightnessControl()) {
348                 return light;
349             }
350         }
351         return null;
352     }
353 
354     /** Register the keyboard backlight listener for a process. */
355     @BinderThread
356     @Override
registerKeyboardBacklightListener(IKeyboardBacklightListener listener, int pid)357     public void registerKeyboardBacklightListener(IKeyboardBacklightListener listener,
358             int pid) {
359         synchronized (mKeyboardBacklightListenerRecords) {
360             if (mKeyboardBacklightListenerRecords.get(pid) != null) {
361                 throw new IllegalStateException("The calling process has already registered "
362                         + "a KeyboardBacklightListener.");
363             }
364             KeyboardBacklightListenerRecord record = new KeyboardBacklightListenerRecord(pid,
365                     listener);
366             try {
367                 listener.asBinder().linkToDeath(record, 0);
368             } catch (RemoteException ex) {
369                 throw new RuntimeException(ex);
370             }
371             mKeyboardBacklightListenerRecords.put(pid, record);
372         }
373     }
374 
375     /** Unregister the keyboard backlight listener for a process. */
376     @BinderThread
377     @Override
unregisterKeyboardBacklightListener(IKeyboardBacklightListener listener, int pid)378     public void unregisterKeyboardBacklightListener(IKeyboardBacklightListener listener,
379             int pid) {
380         synchronized (mKeyboardBacklightListenerRecords) {
381             KeyboardBacklightListenerRecord record = mKeyboardBacklightListenerRecords.get(pid);
382             if (record == null) {
383                 throw new IllegalStateException("The calling process has no registered "
384                         + "KeyboardBacklightListener.");
385             }
386             if (record.mListener.asBinder() != listener.asBinder()) {
387                 throw new IllegalStateException("The calling process has a different registered "
388                         + "KeyboardBacklightListener.");
389             }
390             record.mListener.asBinder().unlinkToDeath(record, 0);
391             mKeyboardBacklightListenerRecords.remove(pid);
392         }
393     }
394 
onKeyboardBacklightListenerDied(int pid)395     private void onKeyboardBacklightListenerDied(int pid) {
396         synchronized (mKeyboardBacklightListenerRecords) {
397             mKeyboardBacklightListenerRecords.remove(pid);
398         }
399     }
400 
updateAmbientLightListener()401     private void updateAmbientLightListener() {
402         boolean needToListenAmbientLightSensor = false;
403         for (int i = 0; i < mKeyboardBacklights.size(); i++) {
404             needToListenAmbientLightSensor |= mKeyboardBacklights.valueAt(i).mUseAmbientController;
405         }
406         needToListenAmbientLightSensor &= mIsInteractive;
407         if (needToListenAmbientLightSensor && mAmbientListener == null) {
408             mAmbientListener = this::handleAmbientLightValueChanged;
409             mAmbientController.registerAmbientBacklightListener(mAmbientListener);
410         }
411         if (!needToListenAmbientLightSensor && mAmbientListener != null) {
412             mAmbientController.unregisterAmbientBacklightListener(mAmbientListener);
413             mAmbientListener = null;
414         }
415     }
416 
isValidBacklightNodePath(String devPath)417     private static boolean isValidBacklightNodePath(String devPath) {
418         if (TextUtils.isEmpty(devPath)) {
419             return false;
420         }
421         int index = devPath.lastIndexOf('/');
422         if (index < 0) {
423             return false;
424         }
425         String backlightNode = devPath.substring(index + 1);
426         devPath = devPath.substring(0, index);
427         if (!devPath.endsWith("leds") || !backlightNode.contains("kbd_backlight")) {
428             return false;
429         }
430         index = devPath.lastIndexOf('/');
431         return index >= 0;
432     }
433 
434     @Override
dump(PrintWriter pw)435     public void dump(PrintWriter pw) {
436         IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
437         ipw.println(TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights");
438         ipw.increaseIndent();
439         for (int i = 0; i < mKeyboardBacklights.size(); i++) {
440             KeyboardBacklightState state = mKeyboardBacklights.valueAt(i);
441             ipw.println(i + ": " + state.toString());
442         }
443         ipw.decreaseIndent();
444     }
445 
446     // A record of a registered Keyboard backlight listener from one process.
447     private class KeyboardBacklightListenerRecord implements IBinder.DeathRecipient {
448         public final int mPid;
449         public final IKeyboardBacklightListener mListener;
450 
KeyboardBacklightListenerRecord(int pid, IKeyboardBacklightListener listener)451         KeyboardBacklightListenerRecord(int pid, IKeyboardBacklightListener listener) {
452             mPid = pid;
453             mListener = listener;
454         }
455 
456         @Override
binderDied()457         public void binderDied() {
458             if (DEBUG) {
459                 Slog.d(TAG, "Keyboard backlight listener for pid " + mPid + " died.");
460             }
461             onKeyboardBacklightListenerDied(mPid);
462         }
463 
notifyKeyboardBacklightChanged(int deviceId, IKeyboardBacklightState state, boolean isTriggeredByKeyPress)464         public void notifyKeyboardBacklightChanged(int deviceId, IKeyboardBacklightState state,
465                 boolean isTriggeredByKeyPress) {
466             try {
467                 mListener.onBrightnessChanged(deviceId, state, isTriggeredByKeyPress);
468             } catch (RemoteException ex) {
469                 Slog.w(TAG, "Failed to notify process " + mPid
470                         + " that keyboard backlight changed, assuming it died.", ex);
471                 binderDied();
472             }
473         }
474     }
475 
476     private class KeyboardBacklightState {
477         private final int mDeviceId;
478         private final Light mLight;
479         private int mBrightnessLevel;
480         private ValueAnimator mAnimator;
481         private final int[] mBrightnessValueForLevel;
482         private boolean mUseAmbientController = true;
483 
KeyboardBacklightState(int deviceId, Light light)484         KeyboardBacklightState(int deviceId, Light light) {
485             mDeviceId = deviceId;
486             mLight = light;
487             mBrightnessValueForLevel = setupBrightnessLevels();
488         }
489 
setupBrightnessLevels()490         private int[] setupBrightnessLevels() {
491             int[] customLevels = mLight.getPreferredBrightnessLevels();
492             if (customLevels == null || customLevels.length == 0) {
493                 return DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL;
494             }
495             TreeSet<Integer> brightnessLevels = new TreeSet<>();
496             brightnessLevels.add(0);
497             for (int level : customLevels) {
498                 if (level > 0 && level < MAX_BRIGHTNESS) {
499                     brightnessLevels.add(level);
500                 }
501             }
502             brightnessLevels.add(MAX_BRIGHTNESS);
503             int brightnessChangeSteps = brightnessLevels.size() - 1;
504             if (brightnessChangeSteps > MAX_BRIGHTNESS_CHANGE_STEPS) {
505                 return DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL;
506             }
507             int[] result = new int[brightnessLevels.size()];
508             int index = 0;
509             for (int val : brightnessLevels) {
510                 result[index++] = val;
511             }
512             return result;
513         }
514 
getNumBrightnessChangeSteps()515         private int getNumBrightnessChangeSteps() {
516             return mBrightnessValueForLevel.length - 1;
517         }
518 
onBacklightStateChanged()519         private void onBacklightStateChanged() {
520             int toValue = mUseAmbientController ? mAmbientBacklightValue
521                     : mBrightnessValueForLevel[mBrightnessLevel];
522             setBacklightValue(mIsBacklightOn ? toValue : 0);
523         }
setBrightnessLevel(int brightnessLevel)524         private void setBrightnessLevel(int brightnessLevel) {
525             // Once we manually set level, disregard ambient light controller
526             mUseAmbientController = false;
527             if (mIsBacklightOn) {
528                 setBacklightValue(mBrightnessValueForLevel[brightnessLevel]);
529             }
530             mBrightnessLevel = brightnessLevel;
531         }
532 
onAmbientBacklightValueChanged()533         private void onAmbientBacklightValueChanged() {
534             if (mIsBacklightOn && mUseAmbientController) {
535                 setBacklightValue(mAmbientBacklightValue);
536             }
537         }
538 
cancelAnimation()539         private void cancelAnimation() {
540             if (mAnimator != null && mAnimator.isRunning()) {
541                 mAnimator.cancel();
542             }
543         }
544 
setBacklightValue(int toValue)545         private void setBacklightValue(int toValue) {
546             int fromValue = Color.alpha(mNative.getLightColor(mDeviceId, mLight.getId()));
547             if (fromValue == toValue) {
548                 return;
549             }
550             if (mKeyboardBacklightAnimationEnabled) {
551                 startAnimation(fromValue, toValue);
552             } else {
553                 mNative.setLightColor(mDeviceId, mLight.getId(), Color.argb(toValue, 0, 0, 0));
554             }
555         }
556 
startAnimation(int fromValue, int toValue)557         private void startAnimation(int fromValue, int toValue) {
558             // Cancel any ongoing animation before starting a new one
559             cancelAnimation();
560             mAnimator = mAnimatorFactory.makeIntAnimator(fromValue, toValue);
561             mAnimator.addUpdateListener(
562                     (animation) -> mNative.setLightColor(mDeviceId, mLight.getId(),
563                             Color.argb((int) animation.getAnimatedValue(), 0, 0, 0)));
564             mAnimator.setDuration(TRANSITION_ANIMATION_DURATION_MILLIS).start();
565         }
566 
567         @Override
toString()568         public String toString() {
569             return "KeyboardBacklightState{Light=" + mLight.getId()
570                     + ", BrightnessLevel=" + mBrightnessLevel
571                     + "}";
572         }
573     }
574 
575     @VisibleForTesting
576     interface AnimatorFactory {
makeIntAnimator(int from, int to)577         ValueAnimator makeIntAnimator(int from, int to);
578     }
579 }
580