1 /* 2 * Copyright (C) 2021 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.systemui.dreams; 18 19 import static com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress; 20 import static com.android.keyguard.BouncerPanelExpansionCalculator.getDreamAlphaScaledExpansion; 21 import static com.android.keyguard.BouncerPanelExpansionCalculator.getDreamYPositionScaledExpansion; 22 import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset; 23 import static com.android.systemui.dreams.complication.ComplicationLayoutParams.POSITION_BOTTOM; 24 import static com.android.systemui.dreams.complication.ComplicationLayoutParams.POSITION_TOP; 25 26 import android.animation.Animator; 27 import android.content.res.Resources; 28 import android.os.Handler; 29 import android.util.MathUtils; 30 import android.view.View; 31 import android.view.ViewGroup; 32 33 import androidx.annotation.NonNull; 34 35 import com.android.dream.lowlight.LowLightTransitionCoordinator; 36 import com.android.systemui.R; 37 import com.android.systemui.animation.Interpolators; 38 import com.android.systemui.dagger.qualifiers.Main; 39 import com.android.systemui.dreams.complication.ComplicationHostViewController; 40 import com.android.systemui.dreams.dagger.DreamOverlayComponent; 41 import com.android.systemui.dreams.dagger.DreamOverlayModule; 42 import com.android.systemui.dreams.touch.scrim.BouncerlessScrimController; 43 import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor; 44 import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback; 45 import com.android.systemui.shade.ShadeExpansionChangeEvent; 46 import com.android.systemui.statusbar.BlurUtils; 47 import com.android.systemui.util.ViewController; 48 import com.android.systemui.util.concurrency.DelayableExecutor; 49 50 import java.util.Arrays; 51 52 import javax.inject.Inject; 53 import javax.inject.Named; 54 55 /** 56 * View controller for {@link DreamOverlayContainerView}. 57 */ 58 @DreamOverlayComponent.DreamOverlayScope 59 public class DreamOverlayContainerViewController extends 60 ViewController<DreamOverlayContainerView> implements 61 LowLightTransitionCoordinator.LowLightEnterListener { 62 private final DreamOverlayStatusBarViewController mStatusBarViewController; 63 private final BlurUtils mBlurUtils; 64 private final DreamOverlayAnimationsController mDreamOverlayAnimationsController; 65 private final DreamOverlayStateController mStateController; 66 private final LowLightTransitionCoordinator mLowLightTransitionCoordinator; 67 68 private final ComplicationHostViewController mComplicationHostViewController; 69 70 // The dream overlay's content view, which is located below the status bar (in z-order) and is 71 // the space into which widgets are placed. 72 private final ViewGroup mDreamOverlayContentView; 73 74 // The maximum translation offset to apply to the overlay container to avoid screen burn-in. 75 private final int mMaxBurnInOffset; 76 77 // The interval in milliseconds between burn-in protection updates. 78 private final long mBurnInProtectionUpdateInterval; 79 80 // Amount of time in milliseconds to linear interpolate toward the final jitter offset. Once 81 // this time is achieved, the normal jitter algorithm applies in full. 82 private final long mMillisUntilFullJitter; 83 84 // Main thread handler used to schedule periodic tasks (e.g. burn-in protection updates). 85 private final Handler mHandler; 86 private final int mDreamOverlayMaxTranslationY; 87 private final PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor; 88 89 private long mJitterStartTimeMillis; 90 91 private boolean mBouncerAnimating; 92 private boolean mWakingUpFromSwipe; 93 94 private final BouncerlessScrimController mBouncerlessScrimController; 95 96 private final BouncerlessScrimController.Callback mBouncerlessExpansionCallback = 97 new BouncerlessScrimController.Callback() { 98 @Override 99 public void onExpansion(ShadeExpansionChangeEvent event) { 100 updateTransitionState(event.getFraction()); 101 } 102 103 @Override 104 public void onWakeup() { 105 mWakingUpFromSwipe = true; 106 } 107 }; 108 109 private final PrimaryBouncerExpansionCallback 110 mBouncerExpansionCallback = 111 new PrimaryBouncerExpansionCallback() { 112 113 @Override 114 public void onStartingToShow() { 115 mBouncerAnimating = true; 116 } 117 118 @Override 119 public void onStartingToHide() { 120 mBouncerAnimating = true; 121 } 122 123 @Override 124 public void onFullyHidden() { 125 mBouncerAnimating = false; 126 } 127 128 @Override 129 public void onFullyShown() { 130 mBouncerAnimating = false; 131 } 132 133 @Override 134 public void onExpansionChanged(float bouncerHideAmount) { 135 if (mBouncerAnimating) { 136 updateTransitionState(bouncerHideAmount); 137 } 138 } 139 140 @Override 141 public void onVisibilityChanged(boolean isVisible) { 142 // The bouncer may be hidden abruptly without triggering onExpansionChanged. 143 // In this case, we should reset the transition state. 144 if (!isVisible) { 145 updateTransitionState(1f); 146 } 147 } 148 }; 149 150 /** 151 * If {@code true}, the dream has just transitioned from the low light dream back to the user 152 * dream and we should play an entry animation where the overlay slides in downwards from the 153 * top instead of the typicla slide in upwards from the bottom. 154 */ 155 private boolean mExitingLowLight; 156 157 private final DreamOverlayStateController.Callback 158 mDreamOverlayStateCallback = 159 new DreamOverlayStateController.Callback() { 160 @Override 161 public void onExitLowLight() { 162 mExitingLowLight = true; 163 } 164 }; 165 166 @Inject DreamOverlayContainerViewController( DreamOverlayContainerView containerView, ComplicationHostViewController complicationHostViewController, @Named(DreamOverlayModule.DREAM_OVERLAY_CONTENT_VIEW) ViewGroup contentView, DreamOverlayStatusBarViewController statusBarViewController, LowLightTransitionCoordinator lowLightTransitionCoordinator, BlurUtils blurUtils, @Main Handler handler, @Main Resources resources, @Named(DreamOverlayModule.MAX_BURN_IN_OFFSET) int maxBurnInOffset, @Named(DreamOverlayModule.BURN_IN_PROTECTION_UPDATE_INTERVAL) long burnInProtectionUpdateInterval, @Named(DreamOverlayModule.MILLIS_UNTIL_FULL_JITTER) long millisUntilFullJitter, PrimaryBouncerCallbackInteractor primaryBouncerCallbackInteractor, DreamOverlayAnimationsController animationsController, DreamOverlayStateController stateController, BouncerlessScrimController bouncerlessScrimController)167 public DreamOverlayContainerViewController( 168 DreamOverlayContainerView containerView, 169 ComplicationHostViewController complicationHostViewController, 170 @Named(DreamOverlayModule.DREAM_OVERLAY_CONTENT_VIEW) ViewGroup contentView, 171 DreamOverlayStatusBarViewController statusBarViewController, 172 LowLightTransitionCoordinator lowLightTransitionCoordinator, 173 BlurUtils blurUtils, 174 @Main Handler handler, 175 @Main Resources resources, 176 @Named(DreamOverlayModule.MAX_BURN_IN_OFFSET) int maxBurnInOffset, 177 @Named(DreamOverlayModule.BURN_IN_PROTECTION_UPDATE_INTERVAL) long 178 burnInProtectionUpdateInterval, 179 @Named(DreamOverlayModule.MILLIS_UNTIL_FULL_JITTER) long millisUntilFullJitter, 180 PrimaryBouncerCallbackInteractor primaryBouncerCallbackInteractor, 181 DreamOverlayAnimationsController animationsController, 182 DreamOverlayStateController stateController, 183 BouncerlessScrimController bouncerlessScrimController) { 184 super(containerView); 185 mDreamOverlayContentView = contentView; 186 mStatusBarViewController = statusBarViewController; 187 mBlurUtils = blurUtils; 188 mDreamOverlayAnimationsController = animationsController; 189 mStateController = stateController; 190 mLowLightTransitionCoordinator = lowLightTransitionCoordinator; 191 192 mBouncerlessScrimController = bouncerlessScrimController; 193 mBouncerlessScrimController.addCallback(mBouncerlessExpansionCallback); 194 195 mComplicationHostViewController = complicationHostViewController; 196 mDreamOverlayMaxTranslationY = resources.getDimensionPixelSize( 197 R.dimen.dream_overlay_y_offset); 198 final View view = mComplicationHostViewController.getView(); 199 200 mDreamOverlayContentView.addView(view, 201 new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 202 ViewGroup.LayoutParams.MATCH_PARENT)); 203 204 mHandler = handler; 205 mMaxBurnInOffset = maxBurnInOffset; 206 mBurnInProtectionUpdateInterval = burnInProtectionUpdateInterval; 207 mMillisUntilFullJitter = millisUntilFullJitter; 208 mPrimaryBouncerCallbackInteractor = primaryBouncerCallbackInteractor; 209 } 210 211 @Override onInit()212 protected void onInit() { 213 mStateController.addCallback(mDreamOverlayStateCallback); 214 mStatusBarViewController.init(); 215 mComplicationHostViewController.init(); 216 mDreamOverlayAnimationsController.init(mView); 217 mLowLightTransitionCoordinator.setLowLightEnterListener(this); 218 } 219 220 @Override onViewAttached()221 protected void onViewAttached() { 222 mWakingUpFromSwipe = false; 223 mJitterStartTimeMillis = System.currentTimeMillis(); 224 mHandler.postDelayed(this::updateBurnInOffsets, mBurnInProtectionUpdateInterval); 225 mPrimaryBouncerCallbackInteractor.addBouncerExpansionCallback(mBouncerExpansionCallback); 226 227 // Start dream entry animations. Skip animations for low light clock. 228 if (!mStateController.isLowLightActive()) { 229 // If this is transitioning from the low light dream to the user dream, the overlay 230 // should translate in downwards instead of upwards. 231 mDreamOverlayAnimationsController.startEntryAnimations(mExitingLowLight); 232 mExitingLowLight = false; 233 } 234 } 235 236 @Override onViewDetached()237 protected void onViewDetached() { 238 mHandler.removeCallbacks(this::updateBurnInOffsets); 239 mPrimaryBouncerCallbackInteractor.removeBouncerExpansionCallback(mBouncerExpansionCallback); 240 241 mDreamOverlayAnimationsController.cancelAnimations(); 242 } 243 getContainerView()244 View getContainerView() { 245 return mView; 246 } 247 updateBurnInOffsets()248 private void updateBurnInOffsets() { 249 // Make sure the offset starts at zero, to avoid a big jump in the overlay when it first 250 // appears. 251 final long millisSinceStart = System.currentTimeMillis() - mJitterStartTimeMillis; 252 final int burnInOffset; 253 if (millisSinceStart < mMillisUntilFullJitter) { 254 float lerpAmount = (float) millisSinceStart / (float) mMillisUntilFullJitter; 255 burnInOffset = Math.round(MathUtils.lerp(0f, mMaxBurnInOffset, lerpAmount)); 256 } else { 257 burnInOffset = mMaxBurnInOffset; 258 } 259 260 // These translation values change slowly, and the set translation methods are idempotent, 261 // so no translation occurs when the values don't change. 262 final int halfBurnInOffset = burnInOffset / 2; 263 final int burnInOffsetX = getBurnInOffset(burnInOffset, true) - halfBurnInOffset; 264 final int burnInOffsetY = getBurnInOffset(burnInOffset, false) - halfBurnInOffset; 265 mView.setTranslationX(burnInOffsetX); 266 mView.setTranslationY(burnInOffsetY); 267 268 mHandler.postDelayed(this::updateBurnInOffsets, mBurnInProtectionUpdateInterval); 269 } 270 updateTransitionState(float bouncerHideAmount)271 private void updateTransitionState(float bouncerHideAmount) { 272 for (int position : Arrays.asList(POSITION_TOP, POSITION_BOTTOM)) { 273 final float alpha = getAlpha(position, bouncerHideAmount); 274 final float translationY = getTranslationY(position, bouncerHideAmount); 275 mComplicationHostViewController.getViewsAtPosition(position).forEach(v -> { 276 v.setAlpha(alpha); 277 v.setTranslationY(translationY); 278 }); 279 } 280 281 mBlurUtils.applyBlur(mView.getViewRootImpl(), 282 (int) mBlurUtils.blurRadiusOfRatio( 283 1 - aboutToShowBouncerProgress(bouncerHideAmount)), false); 284 } 285 getAlpha(int position, float expansion)286 private static float getAlpha(int position, float expansion) { 287 return Interpolators.LINEAR_OUT_SLOW_IN.getInterpolation( 288 position == POSITION_TOP ? getDreamAlphaScaledExpansion(expansion) 289 : aboutToShowBouncerProgress(expansion + 0.03f)); 290 } 291 getTranslationY(int position, float expansion)292 private float getTranslationY(int position, float expansion) { 293 final float fraction = Interpolators.LINEAR_OUT_SLOW_IN.getInterpolation( 294 position == POSITION_TOP ? getDreamYPositionScaledExpansion(expansion) 295 : aboutToShowBouncerProgress(expansion + 0.03f)); 296 return MathUtils.lerp(-mDreamOverlayMaxTranslationY, 0, fraction); 297 } 298 299 /** 300 * Handle the dream waking up and run any necessary animations. 301 * 302 * @param onAnimationEnd Callback to trigger once animations are finished. 303 * @param callbackExecutor Executor to execute the callback on. 304 */ wakeUp(@onNull Runnable onAnimationEnd, @NonNull DelayableExecutor callbackExecutor)305 public void wakeUp(@NonNull Runnable onAnimationEnd, 306 @NonNull DelayableExecutor callbackExecutor) { 307 // When swiping causes wakeup, do not run any animations as the dream should exit as soon 308 // as possible. 309 if (mWakingUpFromSwipe) { 310 onAnimationEnd.run(); 311 return; 312 } 313 314 mDreamOverlayAnimationsController.wakeUp(onAnimationEnd, callbackExecutor); 315 } 316 317 @Override onBeforeEnterLowLight()318 public Animator onBeforeEnterLowLight() { 319 // Return the animator so that the transition coordinator waits for the overlay exit 320 // animations to finish before entering low light, as otherwise the default DreamActivity 321 // animation plays immediately and there's no time for this animation to play. 322 return mDreamOverlayAnimationsController.startExitAnimations(); 323 } 324 } 325