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 org.mockito.ArgumentMatchers.any;
20 import static org.mockito.ArgumentMatchers.anyBoolean;
21 import static org.mockito.ArgumentMatchers.anyFloat;
22 import static org.mockito.ArgumentMatchers.anyInt;
23 import static org.mockito.ArgumentMatchers.eq;
24 import static org.mockito.Mockito.never;
25 import static org.mockito.Mockito.verify;
26 import static org.mockito.Mockito.when;
27 
28 import android.content.res.Resources;
29 import android.graphics.Region;
30 import android.os.Handler;
31 import android.testing.AndroidTestingRunner;
32 import android.view.AttachedSurfaceControl;
33 import android.view.ViewGroup;
34 import android.view.ViewRootImpl;
35 import android.view.ViewTreeObserver;
36 
37 import androidx.test.filters.SmallTest;
38 
39 import com.android.dream.lowlight.LowLightTransitionCoordinator;
40 import com.android.keyguard.BouncerPanelExpansionCalculator;
41 import com.android.systemui.SysuiTestCase;
42 import com.android.systemui.complication.ComplicationHostViewController;
43 import com.android.systemui.dreams.touch.scrim.BouncerlessScrimController;
44 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor;
45 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback;
46 import com.android.systemui.statusbar.BlurUtils;
47 
48 import org.junit.Before;
49 import org.junit.Test;
50 import org.junit.runner.RunWith;
51 import org.mockito.ArgumentCaptor;
52 import org.mockito.Mock;
53 import org.mockito.MockitoAnnotations;
54 
55 @SmallTest
56 @RunWith(AndroidTestingRunner.class)
57 public class DreamOverlayContainerViewControllerTest extends SysuiTestCase {
58     private static final int MAX_BURN_IN_OFFSET = 20;
59     private static final long BURN_IN_PROTECTION_UPDATE_INTERVAL = 10;
60     private static final long MILLIS_UNTIL_FULL_JITTER = 240 * 1000;
61 
62     @Mock
63     Resources mResources;
64 
65     @Mock
66     ViewTreeObserver mViewTreeObserver;
67 
68     @Mock
69     DreamOverlayStatusBarViewController mDreamOverlayStatusBarViewController;
70 
71     @Mock
72     LowLightTransitionCoordinator mLowLightTransitionCoordinator;
73 
74     @Mock
75     DreamOverlayContainerView mDreamOverlayContainerView;
76 
77     @Mock
78     ComplicationHostViewController mComplicationHostViewController;
79 
80     @Mock
81     AttachedSurfaceControl mAttachedSurfaceControl;
82 
83     @Mock
84     ViewGroup mDreamOverlayContentView;
85 
86     @Mock
87     Handler mHandler;
88 
89     @Mock
90     BlurUtils mBlurUtils;
91 
92     @Mock
93     ViewRootImpl mViewRoot;
94 
95     @Mock
96     PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor;
97 
98     @Mock
99     DreamOverlayAnimationsController mAnimationsController;
100 
101     @Mock
102     BouncerlessScrimController mBouncerlessScrimController;
103 
104     @Mock
105     DreamOverlayStateController mStateController;
106 
107     DreamOverlayContainerViewController mController;
108 
109     @Before
setup()110     public void setup() {
111         MockitoAnnotations.initMocks(this);
112 
113         when(mDreamOverlayContainerView.getResources()).thenReturn(mResources);
114         when(mDreamOverlayContainerView.getViewTreeObserver()).thenReturn(mViewTreeObserver);
115         when(mDreamOverlayContainerView.getViewRootImpl()).thenReturn(mViewRoot);
116         when(mDreamOverlayContainerView.getRootSurfaceControl())
117                 .thenReturn(mAttachedSurfaceControl);
118 
119         mController = new DreamOverlayContainerViewController(
120                 mDreamOverlayContainerView,
121                 mComplicationHostViewController,
122                 mDreamOverlayContentView,
123                 mDreamOverlayStatusBarViewController,
124                 mLowLightTransitionCoordinator,
125                 mBlurUtils,
126                 mHandler,
127                 mResources,
128                 MAX_BURN_IN_OFFSET,
129                 BURN_IN_PROTECTION_UPDATE_INTERVAL,
130                 MILLIS_UNTIL_FULL_JITTER,
131                 mPrimaryBouncerCallbackInteractor,
132                 mAnimationsController,
133                 mStateController,
134                 mBouncerlessScrimController);
135     }
136 
137     @Test
testRootSurfaceControlInsetSetOnAttach()138     public void testRootSurfaceControlInsetSetOnAttach() {
139         mController.onViewAttached();
140         verify(mAttachedSurfaceControl).setTouchableRegion(eq(Region.obtain()));
141     }
142 
143     @Test
testDreamOverlayStatusBarViewControllerInitialized()144     public void testDreamOverlayStatusBarViewControllerInitialized() {
145         mController.init();
146         verify(mDreamOverlayStatusBarViewController).init();
147     }
148 
149     @Test
testBurnInProtectionStartsWhenContentViewAttached()150     public void testBurnInProtectionStartsWhenContentViewAttached() {
151         mController.onViewAttached();
152         verify(mHandler).postDelayed(any(Runnable.class), eq(BURN_IN_PROTECTION_UPDATE_INTERVAL));
153     }
154 
155     @Test
testBurnInProtectionStopsWhenContentViewDetached()156     public void testBurnInProtectionStopsWhenContentViewDetached() {
157         mController.onViewDetached();
158         verify(mHandler).removeCallbacks(any(Runnable.class));
159     }
160 
161     @Test
testBurnInProtectionOffsetsStartAtZero()162     public void testBurnInProtectionOffsetsStartAtZero() {
163         ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
164         mController.onViewAttached();
165         verify(mHandler).postDelayed(
166                 runnableCaptor.capture(), eq(BURN_IN_PROTECTION_UPDATE_INTERVAL));
167         runnableCaptor.getValue().run();
168         verify(mDreamOverlayContainerView).setTranslationX(0.f);
169         verify(mDreamOverlayContainerView).setTranslationY(0.f);
170     }
171 
172     @Test
testBurnInProtectionReschedulesUpdate()173     public void testBurnInProtectionReschedulesUpdate() {
174         ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
175         mController.onViewAttached();
176         verify(mHandler).postDelayed(
177                 runnableCaptor.capture(), eq(BURN_IN_PROTECTION_UPDATE_INTERVAL));
178         runnableCaptor.getValue().run();
179         verify(mHandler).postDelayed(runnableCaptor.getValue(), BURN_IN_PROTECTION_UPDATE_INTERVAL);
180     }
181 
182     @Test
testBouncerAnimation_doesNotApply()183     public void testBouncerAnimation_doesNotApply() {
184         final ArgumentCaptor<PrimaryBouncerExpansionCallback> bouncerExpansionCaptor =
185                 ArgumentCaptor.forClass(PrimaryBouncerExpansionCallback.class);
186         mController.onViewAttached();
187         verify(mPrimaryBouncerCallbackInteractor).addBouncerExpansionCallback(
188                 bouncerExpansionCaptor.capture());
189 
190         bouncerExpansionCaptor.getValue().onExpansionChanged(0.5f);
191         verify(mBlurUtils, never()).applyBlur(eq(mViewRoot), anyInt(), eq(false));
192     }
193 
194     @Test
testBouncerAnimation_updateBlur()195     public void testBouncerAnimation_updateBlur() {
196         final ArgumentCaptor<PrimaryBouncerExpansionCallback> bouncerExpansionCaptor =
197                 ArgumentCaptor.forClass(PrimaryBouncerExpansionCallback.class);
198         mController.onViewAttached();
199         verify(mPrimaryBouncerCallbackInteractor).addBouncerExpansionCallback(
200                 bouncerExpansionCaptor.capture());
201 
202         final float blurRadius = 1337f;
203         when(mBlurUtils.blurRadiusOfRatio(anyFloat())).thenReturn(blurRadius);
204 
205         bouncerExpansionCaptor.getValue().onStartingToShow();
206 
207         final float bouncerHideAmount = 0.05f;
208         final float scaledFraction =
209                 BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(bouncerHideAmount);
210 
211         bouncerExpansionCaptor.getValue().onExpansionChanged(bouncerHideAmount);
212         verify(mBlurUtils).blurRadiusOfRatio(1 - scaledFraction);
213         verify(mBlurUtils).applyBlur(mViewRoot, (int) blurRadius, false);
214     }
215 
216     @Test
testStartDreamEntryAnimationsOnAttachedNonLowLight()217     public void testStartDreamEntryAnimationsOnAttachedNonLowLight() {
218         when(mStateController.isLowLightActive()).thenReturn(false);
219 
220         mController.onViewAttached();
221 
222         verify(mAnimationsController).startEntryAnimations(false);
223         verify(mAnimationsController, never()).cancelAnimations();
224     }
225 
226     @Test
testNeverStartDreamEntryAnimationsOnAttachedForLowLight()227     public void testNeverStartDreamEntryAnimationsOnAttachedForLowLight() {
228         when(mStateController.isLowLightActive()).thenReturn(true);
229 
230         mController.onViewAttached();
231 
232         verify(mAnimationsController, never()).startEntryAnimations(anyBoolean());
233     }
234 
235     @Test
testDownwardEntryAnimationsWhenExitingLowLight()236     public void testDownwardEntryAnimationsWhenExitingLowLight() {
237         ArgumentCaptor<DreamOverlayStateController.Callback> callbackCaptor =
238                 ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class);
239         when(mStateController.isLowLightActive()).thenReturn(false);
240 
241         // Call onInit so that the callback is added.
242         mController.onInit();
243         verify(mStateController).addCallback(callbackCaptor.capture());
244 
245         // Send the signal that low light is exiting
246         callbackCaptor.getValue().onExitLowLight();
247 
248         // View is attached to trigger animations.
249         mController.onViewAttached();
250 
251         // Entry animations should be started then immediately ended to skip to the end.
252         verify(mAnimationsController).startEntryAnimations(true);
253     }
254 
255     @Test
testStartsExitAnimationsBeforeEnteringLowLight()256     public void testStartsExitAnimationsBeforeEnteringLowLight() {
257         mController.onBeforeEnterLowLight();
258 
259         verify(mAnimationsController).startExitAnimations();
260     }
261 
262     @Test
testCancelDreamEntryAnimationsOnDetached()263     public void testCancelDreamEntryAnimationsOnDetached() {
264         mController.onViewAttached();
265         mController.onViewDetached();
266 
267         verify(mAnimationsController).cancelAnimations();
268     }
269 }
270