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