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