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