1 /* 2 * Copyright (C) 2016 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.display; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.TypeEvaluator; 22 import android.animation.ValueAnimator; 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.app.AlarmManager; 26 import android.content.BroadcastReceiver; 27 import android.content.ContentResolver; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.IntentFilter; 31 import android.database.ContentObserver; 32 import android.net.Uri; 33 import android.opengl.Matrix; 34 import android.os.Handler; 35 import android.os.Looper; 36 import android.os.UserHandle; 37 import android.provider.Settings.Secure; 38 import android.util.MathUtils; 39 import android.util.Slog; 40 import android.view.animation.AnimationUtils; 41 42 import com.android.internal.app.ColorDisplayController; 43 import com.android.server.SystemService; 44 import com.android.server.twilight.TwilightListener; 45 import com.android.server.twilight.TwilightManager; 46 import com.android.server.twilight.TwilightState; 47 48 import java.time.LocalDateTime; 49 import java.time.LocalTime; 50 import java.time.ZoneId; 51 52 import com.android.internal.R; 53 54 import static com.android.server.display.DisplayTransformManager.LEVEL_COLOR_MATRIX_NIGHT_DISPLAY; 55 56 /** 57 * Tints the display at night. 58 */ 59 public final class ColorDisplayService extends SystemService 60 implements ColorDisplayController.Callback { 61 62 private static final String TAG = "ColorDisplayService"; 63 64 /** 65 * The transition time, in milliseconds, for Night Display to turn on/off. 66 */ 67 private static final long TRANSITION_DURATION = 3000L; 68 69 /** 70 * The identity matrix, used if one of the given matrices is {@code null}. 71 */ 72 private static final float[] MATRIX_IDENTITY = new float[16]; 73 static { Matrix.setIdentityM(MATRIX_IDENTITY, 0)74 Matrix.setIdentityM(MATRIX_IDENTITY, 0); 75 } 76 77 /** 78 * Evaluator used to animate color matrix transitions. 79 */ 80 private static final ColorMatrixEvaluator COLOR_MATRIX_EVALUATOR = new ColorMatrixEvaluator(); 81 82 private final Handler mHandler; 83 84 private float[] mMatrixNight = new float[16]; 85 86 private final float[] mColorTempCoefficients = new float[9]; 87 88 private int mCurrentUser = UserHandle.USER_NULL; 89 private ContentObserver mUserSetupObserver; 90 private boolean mBootCompleted; 91 92 private ColorDisplayController mController; 93 private ValueAnimator mColorMatrixAnimator; 94 private Boolean mIsActivated; 95 private AutoMode mAutoMode; 96 ColorDisplayService(Context context)97 public ColorDisplayService(Context context) { 98 super(context); 99 mHandler = new Handler(Looper.getMainLooper()); 100 } 101 102 @Override onStart()103 public void onStart() { 104 // Nothing to publish. 105 } 106 107 @Override onBootPhase(int phase)108 public void onBootPhase(int phase) { 109 if (phase >= PHASE_BOOT_COMPLETED) { 110 mBootCompleted = true; 111 112 // Register listeners now that boot is complete. 113 if (mCurrentUser != UserHandle.USER_NULL && mUserSetupObserver == null) { 114 setUp(); 115 } 116 } 117 } 118 119 @Override onStartUser(int userHandle)120 public void onStartUser(int userHandle) { 121 super.onStartUser(userHandle); 122 123 if (mCurrentUser == UserHandle.USER_NULL) { 124 onUserChanged(userHandle); 125 } 126 } 127 128 @Override onSwitchUser(int userHandle)129 public void onSwitchUser(int userHandle) { 130 super.onSwitchUser(userHandle); 131 132 onUserChanged(userHandle); 133 } 134 135 @Override onStopUser(int userHandle)136 public void onStopUser(int userHandle) { 137 super.onStopUser(userHandle); 138 139 if (mCurrentUser == userHandle) { 140 onUserChanged(UserHandle.USER_NULL); 141 } 142 } 143 onUserChanged(int userHandle)144 private void onUserChanged(int userHandle) { 145 final ContentResolver cr = getContext().getContentResolver(); 146 147 if (mCurrentUser != UserHandle.USER_NULL) { 148 if (mUserSetupObserver != null) { 149 cr.unregisterContentObserver(mUserSetupObserver); 150 mUserSetupObserver = null; 151 } else if (mBootCompleted) { 152 tearDown(); 153 } 154 } 155 156 mCurrentUser = userHandle; 157 158 if (mCurrentUser != UserHandle.USER_NULL) { 159 if (!isUserSetupCompleted(cr, mCurrentUser)) { 160 mUserSetupObserver = new ContentObserver(mHandler) { 161 @Override 162 public void onChange(boolean selfChange, Uri uri) { 163 if (isUserSetupCompleted(cr, mCurrentUser)) { 164 cr.unregisterContentObserver(this); 165 mUserSetupObserver = null; 166 167 if (mBootCompleted) { 168 setUp(); 169 } 170 } 171 } 172 }; 173 cr.registerContentObserver(Secure.getUriFor(Secure.USER_SETUP_COMPLETE), 174 false /* notifyForDescendents */, mUserSetupObserver, mCurrentUser); 175 } else if (mBootCompleted) { 176 setUp(); 177 } 178 } 179 } 180 isUserSetupCompleted(ContentResolver cr, int userHandle)181 private static boolean isUserSetupCompleted(ContentResolver cr, int userHandle) { 182 return Secure.getIntForUser(cr, Secure.USER_SETUP_COMPLETE, 0, userHandle) == 1; 183 } 184 setUp()185 private void setUp() { 186 Slog.d(TAG, "setUp: currentUser=" + mCurrentUser); 187 188 // Create a new controller for the current user and start listening for changes. 189 mController = new ColorDisplayController(getContext(), mCurrentUser); 190 mController.setListener(this); 191 192 // Set the color mode, if valid, and immediately apply the updated tint matrix based on the 193 // existing activated state. This ensures consistency of tint across the color mode change. 194 onDisplayColorModeChanged(mController.getColorMode()); 195 196 // Reset the activated state. 197 mIsActivated = null; 198 199 setCoefficientMatrix(getContext(), DisplayTransformManager.needsLinearColorMatrix()); 200 201 // Prepare color transformation matrix. 202 setMatrix(mController.getColorTemperature(), mMatrixNight); 203 204 // Initialize the current auto mode. 205 onAutoModeChanged(mController.getAutoMode()); 206 207 // Force the initialization current activated state. 208 if (mIsActivated == null) { 209 onActivated(mController.isActivated()); 210 } 211 } 212 tearDown()213 private void tearDown() { 214 Slog.d(TAG, "tearDown: currentUser=" + mCurrentUser); 215 216 if (mController != null) { 217 mController.setListener(null); 218 mController = null; 219 } 220 221 if (mAutoMode != null) { 222 mAutoMode.onStop(); 223 mAutoMode = null; 224 } 225 226 if (mColorMatrixAnimator != null) { 227 mColorMatrixAnimator.end(); 228 mColorMatrixAnimator = null; 229 } 230 } 231 232 @Override onActivated(boolean activated)233 public void onActivated(boolean activated) { 234 if (mIsActivated == null || mIsActivated != activated) { 235 Slog.i(TAG, activated ? "Turning on night display" : "Turning off night display"); 236 237 mIsActivated = activated; 238 239 if (mAutoMode != null) { 240 mAutoMode.onActivated(activated); 241 } 242 243 applyTint(false); 244 } 245 } 246 247 @Override onAutoModeChanged(int autoMode)248 public void onAutoModeChanged(int autoMode) { 249 Slog.d(TAG, "onAutoModeChanged: autoMode=" + autoMode); 250 251 if (mAutoMode != null) { 252 mAutoMode.onStop(); 253 mAutoMode = null; 254 } 255 256 if (autoMode == ColorDisplayController.AUTO_MODE_CUSTOM) { 257 mAutoMode = new CustomAutoMode(); 258 } else if (autoMode == ColorDisplayController.AUTO_MODE_TWILIGHT) { 259 mAutoMode = new TwilightAutoMode(); 260 } 261 262 if (mAutoMode != null) { 263 mAutoMode.onStart(); 264 } 265 } 266 267 @Override onCustomStartTimeChanged(LocalTime startTime)268 public void onCustomStartTimeChanged(LocalTime startTime) { 269 Slog.d(TAG, "onCustomStartTimeChanged: startTime=" + startTime); 270 271 if (mAutoMode != null) { 272 mAutoMode.onCustomStartTimeChanged(startTime); 273 } 274 } 275 276 @Override onCustomEndTimeChanged(LocalTime endTime)277 public void onCustomEndTimeChanged(LocalTime endTime) { 278 Slog.d(TAG, "onCustomEndTimeChanged: endTime=" + endTime); 279 280 if (mAutoMode != null) { 281 mAutoMode.onCustomEndTimeChanged(endTime); 282 } 283 } 284 285 @Override onColorTemperatureChanged(int colorTemperature)286 public void onColorTemperatureChanged(int colorTemperature) { 287 setMatrix(colorTemperature, mMatrixNight); 288 applyTint(true); 289 } 290 291 @Override onDisplayColorModeChanged(int mode)292 public void onDisplayColorModeChanged(int mode) { 293 if (mode == -1) { 294 return; 295 } 296 297 // Cancel the night display tint animator if it's running. 298 if (mColorMatrixAnimator != null) { 299 mColorMatrixAnimator.cancel(); 300 } 301 302 setCoefficientMatrix(getContext(), DisplayTransformManager.needsLinearColorMatrix(mode)); 303 setMatrix(mController.getColorTemperature(), mMatrixNight); 304 305 final DisplayTransformManager dtm = getLocalService(DisplayTransformManager.class); 306 dtm.setColorMode(mode, (mIsActivated != null && mIsActivated) ? mMatrixNight 307 : MATRIX_IDENTITY); 308 } 309 310 @Override onAccessibilityTransformChanged(boolean state)311 public void onAccessibilityTransformChanged(boolean state) { 312 onDisplayColorModeChanged(mController.getColorMode()); 313 } 314 315 /** 316 * Set coefficients based on whether the color matrix is linear or not. 317 */ setCoefficientMatrix(Context context, boolean needsLinear)318 private void setCoefficientMatrix(Context context, boolean needsLinear) { 319 final String[] coefficients = context.getResources().getStringArray(needsLinear 320 ? R.array.config_nightDisplayColorTemperatureCoefficients 321 : R.array.config_nightDisplayColorTemperatureCoefficientsNative); 322 for (int i = 0; i < 9 && i < coefficients.length; i++) { 323 mColorTempCoefficients[i] = Float.parseFloat(coefficients[i]); 324 } 325 } 326 327 /** 328 * Applies current color temperature matrix, or removes it if deactivated. 329 * 330 * @param immediate {@code true} skips transition animation 331 */ applyTint(boolean immediate)332 private void applyTint(boolean immediate) { 333 // Cancel the old animator if still running. 334 if (mColorMatrixAnimator != null) { 335 mColorMatrixAnimator.cancel(); 336 } 337 338 final DisplayTransformManager dtm = getLocalService(DisplayTransformManager.class); 339 final float[] from = dtm.getColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY); 340 final float[] to = mIsActivated ? mMatrixNight : MATRIX_IDENTITY; 341 342 if (immediate) { 343 dtm.setColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY, to); 344 } else { 345 mColorMatrixAnimator = ValueAnimator.ofObject(COLOR_MATRIX_EVALUATOR, 346 from == null ? MATRIX_IDENTITY : from, to); 347 mColorMatrixAnimator.setDuration(TRANSITION_DURATION); 348 mColorMatrixAnimator.setInterpolator(AnimationUtils.loadInterpolator( 349 getContext(), android.R.interpolator.fast_out_slow_in)); 350 mColorMatrixAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 351 @Override 352 public void onAnimationUpdate(ValueAnimator animator) { 353 final float[] value = (float[]) animator.getAnimatedValue(); 354 dtm.setColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY, value); 355 } 356 }); 357 mColorMatrixAnimator.addListener(new AnimatorListenerAdapter() { 358 359 private boolean mIsCancelled; 360 361 @Override 362 public void onAnimationCancel(Animator animator) { 363 mIsCancelled = true; 364 } 365 366 @Override 367 public void onAnimationEnd(Animator animator) { 368 if (!mIsCancelled) { 369 // Ensure final color matrix is set at the end of the animation. If the 370 // animation is cancelled then don't set the final color matrix so the new 371 // animator can pick up from where this one left off. 372 dtm.setColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY, to); 373 } 374 mColorMatrixAnimator = null; 375 } 376 }); 377 mColorMatrixAnimator.start(); 378 } 379 } 380 381 /** 382 * Set the color transformation {@code MATRIX_NIGHT} to the given color temperature. 383 * 384 * @param colorTemperature color temperature in Kelvin 385 * @param outTemp the 4x4 display transformation matrix for that color temperature 386 */ setMatrix(int colorTemperature, float[] outTemp)387 private void setMatrix(int colorTemperature, float[] outTemp) { 388 if (outTemp.length != 16) { 389 Slog.d(TAG, "The display transformation matrix must be 4x4"); 390 return; 391 } 392 393 Matrix.setIdentityM(mMatrixNight, 0); 394 395 final float squareTemperature = colorTemperature * colorTemperature; 396 final float red = squareTemperature * mColorTempCoefficients[0] 397 + colorTemperature * mColorTempCoefficients[1] + mColorTempCoefficients[2]; 398 final float green = squareTemperature * mColorTempCoefficients[3] 399 + colorTemperature * mColorTempCoefficients[4] + mColorTempCoefficients[5]; 400 final float blue = squareTemperature * mColorTempCoefficients[6] 401 + colorTemperature * mColorTempCoefficients[7] + mColorTempCoefficients[8]; 402 outTemp[0] = red; 403 outTemp[5] = green; 404 outTemp[10] = blue; 405 } 406 407 /** 408 * Returns the first date time corresponding to the local time that occurs before the 409 * provided date time. 410 * 411 * @param compareTime the LocalDateTime to compare against 412 * @return the prior LocalDateTime corresponding to this local time 413 */ getDateTimeBefore(LocalTime localTime, LocalDateTime compareTime)414 public static LocalDateTime getDateTimeBefore(LocalTime localTime, LocalDateTime compareTime) { 415 final LocalDateTime ldt = LocalDateTime.of(compareTime.getYear(), compareTime.getMonth(), 416 compareTime.getDayOfMonth(), localTime.getHour(), localTime.getMinute()); 417 418 // Check if the local time has passed, if so return the same time yesterday. 419 return ldt.isAfter(compareTime) ? ldt.minusDays(1) : ldt; 420 } 421 422 /** 423 * Returns the first date time corresponding to this local time that occurs after the 424 * provided date time. 425 * 426 * @param compareTime the LocalDateTime to compare against 427 * @return the next LocalDateTime corresponding to this local time 428 */ getDateTimeAfter(LocalTime localTime, LocalDateTime compareTime)429 public static LocalDateTime getDateTimeAfter(LocalTime localTime, LocalDateTime compareTime) { 430 final LocalDateTime ldt = LocalDateTime.of(compareTime.getYear(), compareTime.getMonth(), 431 compareTime.getDayOfMonth(), localTime.getHour(), localTime.getMinute()); 432 433 // Check if the local time has passed, if so return the same time tomorrow. 434 return ldt.isBefore(compareTime) ? ldt.plusDays(1) : ldt; 435 } 436 437 private abstract class AutoMode implements ColorDisplayController.Callback { onStart()438 public abstract void onStart(); 439 onStop()440 public abstract void onStop(); 441 } 442 443 private class CustomAutoMode extends AutoMode implements AlarmManager.OnAlarmListener { 444 445 private final AlarmManager mAlarmManager; 446 private final BroadcastReceiver mTimeChangedReceiver; 447 448 private LocalTime mStartTime; 449 private LocalTime mEndTime; 450 451 private LocalDateTime mLastActivatedTime; 452 CustomAutoMode()453 CustomAutoMode() { 454 mAlarmManager = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE); 455 mTimeChangedReceiver = new BroadcastReceiver() { 456 @Override 457 public void onReceive(Context context, Intent intent) { 458 updateActivated(); 459 } 460 }; 461 } 462 updateActivated()463 private void updateActivated() { 464 final LocalDateTime now = LocalDateTime.now(); 465 final LocalDateTime start = getDateTimeBefore(mStartTime, now); 466 final LocalDateTime end = getDateTimeAfter(mEndTime, start); 467 boolean activate = now.isBefore(end); 468 469 if (mLastActivatedTime != null) { 470 // Maintain the existing activated state if within the current period. 471 if (mLastActivatedTime.isBefore(now) && mLastActivatedTime.isAfter(start) 472 && (mLastActivatedTime.isAfter(end) || now.isBefore(end))) { 473 activate = mController.isActivated(); 474 } 475 } 476 477 if (mIsActivated == null || mIsActivated != activate) { 478 mController.setActivated(activate); 479 } 480 481 updateNextAlarm(mIsActivated, now); 482 } 483 updateNextAlarm(@ullable Boolean activated, @NonNull LocalDateTime now)484 private void updateNextAlarm(@Nullable Boolean activated, @NonNull LocalDateTime now) { 485 if (activated != null) { 486 final LocalDateTime next = activated ? getDateTimeAfter(mEndTime, now) 487 : getDateTimeAfter(mStartTime, now); 488 final long millis = next.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); 489 mAlarmManager.setExact(AlarmManager.RTC, millis, TAG, this, null); 490 } 491 } 492 493 @Override onStart()494 public void onStart() { 495 final IntentFilter intentFilter = new IntentFilter(Intent.ACTION_TIME_CHANGED); 496 intentFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED); 497 getContext().registerReceiver(mTimeChangedReceiver, intentFilter); 498 499 mStartTime = mController.getCustomStartTime(); 500 mEndTime = mController.getCustomEndTime(); 501 502 mLastActivatedTime = mController.getLastActivatedTime(); 503 504 // Force an update to initialize state. 505 updateActivated(); 506 } 507 508 @Override onStop()509 public void onStop() { 510 getContext().unregisterReceiver(mTimeChangedReceiver); 511 512 mAlarmManager.cancel(this); 513 mLastActivatedTime = null; 514 } 515 516 @Override onActivated(boolean activated)517 public void onActivated(boolean activated) { 518 mLastActivatedTime = mController.getLastActivatedTime(); 519 updateNextAlarm(activated, LocalDateTime.now()); 520 } 521 522 @Override onCustomStartTimeChanged(LocalTime startTime)523 public void onCustomStartTimeChanged(LocalTime startTime) { 524 mStartTime = startTime; 525 mLastActivatedTime = null; 526 updateActivated(); 527 } 528 529 @Override onCustomEndTimeChanged(LocalTime endTime)530 public void onCustomEndTimeChanged(LocalTime endTime) { 531 mEndTime = endTime; 532 mLastActivatedTime = null; 533 updateActivated(); 534 } 535 536 @Override onAlarm()537 public void onAlarm() { 538 Slog.d(TAG, "onAlarm"); 539 updateActivated(); 540 } 541 } 542 543 private class TwilightAutoMode extends AutoMode implements TwilightListener { 544 545 private final TwilightManager mTwilightManager; 546 TwilightAutoMode()547 TwilightAutoMode() { 548 mTwilightManager = getLocalService(TwilightManager.class); 549 } 550 updateActivated(TwilightState state)551 private void updateActivated(TwilightState state) { 552 if (state == null) { 553 // If there isn't a valid TwilightState then just keep the current activated 554 // state. 555 return; 556 } 557 558 boolean activate = state.isNight(); 559 final LocalDateTime lastActivatedTime = mController.getLastActivatedTime(); 560 if (lastActivatedTime != null) { 561 final LocalDateTime now = LocalDateTime.now(); 562 final LocalDateTime sunrise = state.sunrise(); 563 final LocalDateTime sunset = state.sunset(); 564 // Maintain the existing activated state if within the current period. 565 if (lastActivatedTime.isBefore(now) && (lastActivatedTime.isBefore(sunrise) 566 ^ lastActivatedTime.isBefore(sunset))) { 567 activate = mController.isActivated(); 568 } 569 } 570 571 if (mIsActivated == null || mIsActivated != activate) { 572 mController.setActivated(activate); 573 } 574 } 575 576 @Override onStart()577 public void onStart() { 578 mTwilightManager.registerListener(this, mHandler); 579 580 // Force an update to initialize state. 581 updateActivated(mTwilightManager.getLastTwilightState()); 582 } 583 584 @Override onStop()585 public void onStop() { 586 mTwilightManager.unregisterListener(this); 587 } 588 589 @Override onActivated(boolean activated)590 public void onActivated(boolean activated) { 591 } 592 593 @Override onTwilightStateChanged(@ullable TwilightState state)594 public void onTwilightStateChanged(@Nullable TwilightState state) { 595 Slog.d(TAG, "onTwilightStateChanged: isNight=" 596 + (state == null ? null : state.isNight())); 597 updateActivated(state); 598 } 599 } 600 601 /** 602 * Interpolates between two 4x4 color transform matrices (in column-major order). 603 */ 604 private static class ColorMatrixEvaluator implements TypeEvaluator<float[]> { 605 606 /** 607 * Result matrix returned by {@link #evaluate(float, float[], float[])}. 608 */ 609 private final float[] mResultMatrix = new float[16]; 610 611 @Override evaluate(float fraction, float[] startValue, float[] endValue)612 public float[] evaluate(float fraction, float[] startValue, float[] endValue) { 613 for (int i = 0; i < mResultMatrix.length; i++) { 614 mResultMatrix[i] = MathUtils.lerp(startValue[i], endValue[i], fraction); 615 } 616 return mResultMatrix; 617 } 618 } 619 } 620