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