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.keyguard;
18 
19 
20 import static com.android.systemui.flags.Flags.KEYGUARD_TALKBACK_FIX;
21 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_BATTERY;
22 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_BIOMETRIC_MESSAGE;
23 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_DISCLOSURE;
24 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_OWNER_INFO;
25 
26 import static org.junit.Assert.assertFalse;
27 import static org.junit.Assert.assertNull;
28 import static org.junit.Assert.assertTrue;
29 import static org.mockito.ArgumentMatchers.any;
30 import static org.mockito.ArgumentMatchers.anyLong;
31 import static org.mockito.Mockito.mock;
32 import static org.mockito.Mockito.never;
33 import static org.mockito.Mockito.reset;
34 import static org.mockito.Mockito.verify;
35 import static org.mockito.Mockito.when;
36 
37 import android.content.res.ColorStateList;
38 import android.graphics.Color;
39 import android.testing.AndroidTestingRunner;
40 import android.testing.TestableLooper.RunWithLooper;
41 
42 import androidx.test.filters.SmallTest;
43 
44 import com.android.keyguard.logging.KeyguardLogger;
45 import com.android.systemui.SysuiTestCase;
46 import com.android.systemui.flags.FakeFeatureFlags;
47 import com.android.systemui.plugins.statusbar.StatusBarStateController;
48 import com.android.systemui.statusbar.phone.KeyguardIndicationTextView;
49 import com.android.systemui.util.concurrency.DelayableExecutor;
50 
51 import org.junit.Before;
52 import org.junit.Test;
53 import org.junit.runner.RunWith;
54 import org.mockito.ArgumentCaptor;
55 import org.mockito.Captor;
56 import org.mockito.Mock;
57 import org.mockito.MockitoAnnotations;
58 
59 @RunWith(AndroidTestingRunner.class)
60 @RunWithLooper
61 @SmallTest
62 public class KeyguardIndicationRotateTextViewControllerTest extends SysuiTestCase {
63 
64     private static final String TEST_MESSAGE = "test message";
65     private static final String TEST_MESSAGE_2 = "test message two";
66     private int mMsgId = 0;
67 
68     @Mock
69     private DelayableExecutor mExecutor;
70     @Mock
71     private KeyguardIndicationTextView mView;
72     @Mock
73     private StatusBarStateController mStatusBarStateController;
74     @Mock
75     private KeyguardLogger mLogger;
76     @Captor
77     private ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateListenerCaptor;
78 
79     private KeyguardIndicationRotateTextViewController mController;
80     private StatusBarStateController.StateListener mStatusBarStateListener;
81 
82     @Before
setUp()83     public void setUp() throws Exception {
84         MockitoAnnotations.initMocks(this);
85         when(mView.getTextColors()).thenReturn(ColorStateList.valueOf(Color.WHITE));
86         FakeFeatureFlags flags = new FakeFeatureFlags();
87         flags.set(KEYGUARD_TALKBACK_FIX, true);
88         mController = new KeyguardIndicationRotateTextViewController(mView, mExecutor,
89                 mStatusBarStateController, mLogger, flags);
90         mController.onViewAttached();
91 
92         verify(mStatusBarStateController).addCallback(mStatusBarStateListenerCaptor.capture());
93         mStatusBarStateListener = mStatusBarStateListenerCaptor.getValue();
94     }
95 
96     @Test
onViewDetached_removesStatusBarStateListener()97     public void onViewDetached_removesStatusBarStateListener() {
98         mController.onViewDetached();
99         verify(mStatusBarStateController).removeCallback(mStatusBarStateListener);
100     }
101 
102     @Test
onViewDetached_removesAllScheduledIndications()103     public void onViewDetached_removesAllScheduledIndications() {
104         // GIVEN show next indication runnable is set
105         final KeyguardIndicationRotateTextViewController.ShowNextIndication mockShowNextIndication =
106                 mock(KeyguardIndicationRotateTextViewController.ShowNextIndication.class);
107         mController.mShowNextIndicationRunnable = mockShowNextIndication;
108 
109         // WHEN the view is detached
110         mController.onViewDetached();
111 
112         // THEN delayed execution is cancelled & runnable set to null
113         verify(mockShowNextIndication).cancelDelayedExecution();
114         assertNull(mController.mShowNextIndicationRunnable);
115     }
116 
117     @Test
destroy_removesStatusBarStateListener()118     public void destroy_removesStatusBarStateListener() {
119         mController.destroy();
120         verify(mStatusBarStateController).removeCallback(mStatusBarStateListener);
121     }
122 
123     @Test
destroy_removesOnAttachStateChangeListener()124     public void destroy_removesOnAttachStateChangeListener() {
125         mController.destroy();
126         verify(mView).removeOnAttachStateChangeListener(any());
127     }
128 
129     @Test
destroy_removesAllScheduledIndications()130     public void destroy_removesAllScheduledIndications() {
131         // GIVEN show next indication runnable is set
132         final KeyguardIndicationRotateTextViewController.ShowNextIndication mockShowNextIndication =
133                 mock(KeyguardIndicationRotateTextViewController.ShowNextIndication.class);
134         mController.mShowNextIndicationRunnable = mockShowNextIndication;
135 
136         // WHEN the controller is destroyed
137         mController.destroy();
138 
139         // THEN delayed execution is cancelled & runnable set to null
140         verify(mockShowNextIndication).cancelDelayedExecution();
141         assertNull(mController.mShowNextIndicationRunnable);
142     }
143 
144     @Test
testInitialState_noIndication()145     public void testInitialState_noIndication() {
146         assertFalse(mController.hasIndications());
147     }
148 
149     @Test
testShowOneIndication()150     public void testShowOneIndication() {
151         // WHEN we add our first indication
152         final KeyguardIndication indication = createIndication();
153         mController.updateIndication(
154                 INDICATION_TYPE_DISCLOSURE, indication, false);
155 
156         // THEN
157         // - we see controller has an indication
158         // - the indication shows immediately since it's the only one
159         // - no next indication is scheduled since there's only one indication
160         assertTrue(mController.hasIndications());
161         verify(mView).switchIndication(indication);
162         verify(mExecutor, never()).executeDelayed(any(), anyLong());
163     }
164 
165     @Test
testShowTwoRotatingMessages()166     public void testShowTwoRotatingMessages() {
167         // GIVEN we already have an indication message
168         mController.updateIndication(
169                 INDICATION_TYPE_OWNER_INFO, createIndication(), false);
170         reset(mView);
171 
172         // WHEN we have a new indication type to display
173         final KeyguardIndication indication2 = createIndication();
174         mController.updateIndication(
175                 INDICATION_TYPE_DISCLOSURE, indication2, false);
176 
177         // THEN
178         // - we don't immediately see the new message until the delay
179         // - next indication is scheduled
180         verify(mView, never()).switchIndication(indication2);
181         verify(mExecutor).executeDelayed(any(), anyLong());
182     }
183 
184     @Test
testUpdateCurrentMessage()185     public void testUpdateCurrentMessage() {
186         // GIVEN we already have an indication message
187         mController.updateIndication(
188                 INDICATION_TYPE_DISCLOSURE, createIndication(), false);
189         reset(mView);
190 
191         // WHEN we have a new message for this indication type to display
192         final KeyguardIndication indication2 = createIndication();
193         mController.updateIndication(
194                 INDICATION_TYPE_DISCLOSURE, indication2, false);
195 
196         // THEN
197         // - new indication is updated immediately
198         // - we don't schedule to show anything later
199         verify(mView).switchIndication(indication2);
200         verify(mExecutor, never()).executeDelayed(any(), anyLong());
201     }
202 
203     @Test
testUpdateRotatingMessageForUndisplayedIndication()204     public void testUpdateRotatingMessageForUndisplayedIndication() {
205         // GIVEN we already have two indication messages
206         mController.updateIndication(
207                 INDICATION_TYPE_OWNER_INFO, createIndication(), false);
208         mController.updateIndication(
209                 INDICATION_TYPE_DISCLOSURE, createIndication(), false);
210         reset(mView);
211         reset(mExecutor);
212 
213         // WHEN we have a new message for an undisplayed indication type
214         final KeyguardIndication indication3 = createIndication();
215         mController.updateIndication(
216                 INDICATION_TYPE_DISCLOSURE, indication3, false);
217 
218         // THEN
219         // - we don't immediately update
220         // - we don't schedule to show anything new
221         verify(mView, never()).switchIndication(indication3);
222         verify(mExecutor, never()).executeDelayed(any(), anyLong());
223     }
224 
225     @Test
testUpdateImmediately()226     public void testUpdateImmediately() {
227         // GIVEN we already have three indication messages
228         mController.updateIndication(
229                 INDICATION_TYPE_OWNER_INFO, createIndication(), false);
230         mController.updateIndication(
231                 INDICATION_TYPE_DISCLOSURE, createIndication(), false);
232         mController.updateIndication(
233                 INDICATION_TYPE_BATTERY, createIndication(), false);
234         reset(mView);
235         reset(mExecutor);
236 
237         // WHEN we have a new message for a currently shown type that we want to show immediately
238         final KeyguardIndication indication4 = createIndication();
239         mController.updateIndication(
240                 INDICATION_TYPE_BATTERY, indication4, true);
241 
242         // THEN
243         // - we immediately update
244         // - we schedule a new delayable to show the next message later
245         verify(mView).switchIndication(indication4);
246         verify(mExecutor).executeDelayed(any(), anyLong());
247 
248         // WHEN an already existing type is updated to show immediately
249         reset(mView);
250         reset(mExecutor);
251         final KeyguardIndication indication5 = createIndication();
252         mController.updateIndication(
253                 INDICATION_TYPE_DISCLOSURE, indication5, true);
254 
255         // THEN
256         // - we immediately update
257         // - we schedule a new delayable to show the next message later
258         verify(mView).switchIndication(indication5);
259         verify(mExecutor).executeDelayed(any(), anyLong());
260     }
261 
262     @Test
testSameMessage_noIndicationUpdate()263     public void testSameMessage_noIndicationUpdate() {
264         // GIVEN we are showing and indication with a test message
265         mController.updateIndication(
266                 INDICATION_TYPE_OWNER_INFO, createIndication(TEST_MESSAGE), true);
267         reset(mView);
268         reset(mExecutor);
269 
270         // WHEN the same type tries to show the same exact message
271         final KeyguardIndication sameIndication = createIndication(TEST_MESSAGE);
272         mController.updateIndication(
273                 INDICATION_TYPE_OWNER_INFO, sameIndication, true);
274 
275         // THEN
276         // - we don't update the indication b/c there's no reason the animate the same text
277         verify(mView, never()).switchIndication(sameIndication);
278     }
279 
280     @Test
testTransientIndication()281     public void testTransientIndication() {
282         // GIVEN we already have two indication messages
283         mController.updateIndication(
284                 INDICATION_TYPE_OWNER_INFO, createIndication(), false);
285         mController.updateIndication(
286                 INDICATION_TYPE_DISCLOSURE, createIndication(), false);
287         reset(mView);
288         reset(mExecutor);
289 
290         // WHEN we have a transient message
291         mController.showTransient(TEST_MESSAGE_2);
292 
293         // THEN
294         // - we immediately update
295         // - we schedule a new delayable to show the next message later
296         verify(mView).switchIndication(any(KeyguardIndication.class));
297         verify(mExecutor).executeDelayed(any(), anyLong());
298     }
299 
300     @Test
testHideIndicationOneMessage()301     public void testHideIndicationOneMessage() {
302         // GIVEN we have one indication message
303         KeyguardIndication indication = createIndication();
304         mController.updateIndication(
305                 INDICATION_TYPE_OWNER_INFO, indication, false);
306         verify(mView).switchIndication(indication);
307         reset(mView);
308 
309         // WHEN we hide the current indication type
310         mController.hideIndication(INDICATION_TYPE_OWNER_INFO);
311 
312         // THEN we immediately update the text to show no text
313         verify(mView).switchIndication(null);
314     }
315 
316     @Test
testHideIndicationTwoMessages()317     public void testHideIndicationTwoMessages() {
318         // GIVEN we have two indication messages
319         final KeyguardIndication indication1 = createIndication();
320         final KeyguardIndication indication2 = createIndication();
321         mController.updateIndication(
322                 INDICATION_TYPE_OWNER_INFO, indication1, false);
323         mController.updateIndication(
324                 INDICATION_TYPE_DISCLOSURE, indication2, false);
325         assertTrue(mController.isNextIndicationScheduled());
326 
327         // WHEN we hide the current indication type
328         mController.hideIndication(INDICATION_TYPE_OWNER_INFO);
329 
330         // THEN we show the next indication and there's no scheduled next indication
331         verify(mView).switchIndication(indication2);
332         assertFalse(mController.isNextIndicationScheduled());
333     }
334 
335     @Test
testStartDozing()336     public void testStartDozing() {
337         // GIVEN a biometric message is showing
338         mController.updateIndication(INDICATION_TYPE_BIOMETRIC_MESSAGE,
339                 createIndication(), true);
340 
341         // WHEN the device is dozing
342         mStatusBarStateListener.onDozingChanged(true);
343 
344         // THEN switch to INDICATION_TYPE_NONE
345         verify(mView).switchIndication(null);
346     }
347 
348     @Test
testStoppedDozing()349     public void testStoppedDozing() {
350         // GIVEN we're dozing & we have an indication message
351         mStatusBarStateListener.onDozingChanged(true);
352         final KeyguardIndication indication = createIndication();
353         mController.updateIndication(
354                 INDICATION_TYPE_DISCLOSURE, indication, false);
355         reset(mView);
356         reset(mExecutor);
357 
358         // WHEN the device is no longer dozing
359         mStatusBarStateListener.onDozingChanged(false);
360 
361         // THEN show the next message
362         verify(mView).switchIndication(indication);
363     }
364 
365     @Test
testIsDozing()366     public void testIsDozing() {
367         // GIVEN the device is dozing
368         mStatusBarStateListener.onDozingChanged(true);
369         reset(mView);
370 
371         // WHEN an indication is updated
372         final KeyguardIndication indication = createIndication();
373         mController.updateIndication(
374                 INDICATION_TYPE_DISCLOSURE, indication, false);
375 
376         // THEN no message is shown since we're dozing
377         verify(mView, never()).switchIndication(any());
378     }
379 
380     /**
381      * Create an indication with a unique message.
382      */
createIndication()383     private KeyguardIndication createIndication() {
384         return createIndication(TEST_MESSAGE + " " + mMsgId++);
385     }
386 
387     /**
388      * Create an indication with the given message.
389      */
createIndication(String msg)390     private KeyguardIndication createIndication(String msg) {
391         return new KeyguardIndication.Builder()
392                 .setMessage(msg)
393                 .setTextColor(ColorStateList.valueOf(Color.WHITE))
394                 .build();
395     }
396 }
397