1 /*
2  * Copyright (C) 2020 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.statusbar.notification.stack;
18 
19 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
20 import static com.android.systemui.statusbar.StatusBarState.SHADE;
21 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL;
22 
23 import static org.mockito.ArgumentMatchers.any;
24 import static org.mockito.ArgumentMatchers.anyInt;
25 import static org.mockito.ArgumentMatchers.argThat;
26 import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
27 import static org.mockito.Mockito.doNothing;
28 import static org.mockito.Mockito.mock;
29 import static org.mockito.Mockito.never;
30 import static org.mockito.Mockito.reset;
31 import static org.mockito.Mockito.verify;
32 import static org.mockito.Mockito.when;
33 
34 import android.content.res.Resources;
35 import android.metrics.LogMaker;
36 import android.testing.AndroidTestingRunner;
37 import android.view.LayoutInflater;
38 
39 import androidx.test.filters.SmallTest;
40 
41 import com.android.internal.logging.MetricsLogger;
42 import com.android.internal.logging.UiEventLogger;
43 import com.android.internal.logging.nano.MetricsProto;
44 import com.android.internal.statusbar.IStatusBarService;
45 import com.android.systemui.SysuiTestCase;
46 import com.android.systemui.classifier.FalsingCollectorFake;
47 import com.android.systemui.classifier.FalsingManagerFake;
48 import com.android.systemui.colorextraction.SysuiColorExtractor;
49 import com.android.systemui.flags.FeatureFlags;
50 import com.android.systemui.media.KeyguardMediaController;
51 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
52 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.OnMenuEventListener;
53 import com.android.systemui.plugins.statusbar.StatusBarStateController;
54 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
55 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
56 import com.android.systemui.statusbar.NotificationLockscreenUserManager.UserChangedListener;
57 import com.android.systemui.statusbar.NotificationRemoteInputManager;
58 import com.android.systemui.statusbar.RemoteInputController;
59 import com.android.systemui.statusbar.SysuiStatusBarStateController;
60 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
61 import com.android.systemui.statusbar.notification.ForegroundServiceDismissalFeatureController;
62 import com.android.systemui.statusbar.notification.NotificationEntryManager;
63 import com.android.systemui.statusbar.notification.collection.NotifCollection;
64 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
65 import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy;
66 import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager;
67 import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController;
68 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
69 import com.android.systemui.statusbar.notification.row.ForegroundServiceDungeonView;
70 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
71 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController.NotificationPanelEvent;
72 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
73 import com.android.systemui.statusbar.phone.KeyguardBypassController;
74 import com.android.systemui.statusbar.phone.ScrimController;
75 import com.android.systemui.statusbar.phone.ShadeController;
76 import com.android.systemui.statusbar.phone.StatusBar;
77 import com.android.systemui.statusbar.policy.ConfigurationController;
78 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
79 import com.android.systemui.statusbar.policy.ZenModeController;
80 import com.android.systemui.tuner.TunerService;
81 
82 import org.junit.Before;
83 import org.junit.Test;
84 import org.junit.runner.RunWith;
85 import org.mockito.Answers;
86 import org.mockito.ArgumentCaptor;
87 import org.mockito.ArgumentMatcher;
88 import org.mockito.Captor;
89 import org.mockito.Mock;
90 import org.mockito.MockitoAnnotations;
91 
92 /**
93  * Tests for {@link NotificationStackScrollLayoutController}.
94  */
95 @SmallTest
96 @RunWith(AndroidTestingRunner.class)
97 public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase {
98 
99     @Mock private NotificationGutsManager mNotificationGutsManager;
100     @Mock private HeadsUpManagerPhone mHeadsUpManager;
101     @Mock private NotificationRoundnessManager mNotificationRoundnessManager;
102     @Mock private TunerService mTunerService;
103     @Mock private DeviceProvisionedController mDeviceProvisionedController;
104     @Mock private DynamicPrivacyController mDynamicPrivacyController;
105     @Mock private ConfigurationController mConfigurationController;
106     @Mock private NotificationStackScrollLayout mNotificationStackScrollLayout;
107     @Mock private ZenModeController mZenModeController;
108     @Mock private KeyguardMediaController mKeyguardMediaController;
109     @Mock private SysuiStatusBarStateController mSysuiStatusBarStateController;
110     @Mock private KeyguardBypassController mKeyguardBypassController;
111     @Mock private SysuiColorExtractor mColorExtractor;
112     @Mock private NotificationLockscreenUserManager mNotificationLockscreenUserManager;
113     @Mock private MetricsLogger mMetricsLogger;
114     @Mock private Resources mResources;
115     @Mock(answer = Answers.RETURNS_SELF)
116     private NotificationSwipeHelper.Builder mNotificationSwipeHelperBuilder;
117     @Mock private NotificationSwipeHelper mNotificationSwipeHelper;
118     @Mock private StatusBar mStatusBar;
119     @Mock private ScrimController mScrimController;
120     @Mock private NotificationGroupManagerLegacy mLegacyGroupManager;
121     @Mock private SectionHeaderController mSilentHeaderController;
122     @Mock private FeatureFlags mFeatureFlags;
123     @Mock private NotifPipeline mNotifPipeline;
124     @Mock private NotifCollection mNotifCollection;
125     @Mock private NotificationEntryManager mEntryManager;
126     @Mock private IStatusBarService mIStatusBarService;
127     @Mock private UiEventLogger mUiEventLogger;
128     @Mock private LockscreenShadeTransitionController mLockscreenShadeTransitionController;
129     @Mock private ForegroundServiceDismissalFeatureController mFgFeatureController;
130     @Mock private ForegroundServiceSectionController mFgServicesSectionController;
131     @Mock private ForegroundServiceDungeonView mForegroundServiceDungeonView;
132     @Mock private LayoutInflater mLayoutInflater;
133     @Mock private NotificationRemoteInputManager mRemoteInputManager;
134     @Mock private VisualStabilityManager mVisualStabilityManager;
135     @Mock private ShadeController mShadeController;
136 
137     @Captor
138     private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerArgumentCaptor;
139 
140     private NotificationStackScrollLayoutController mController;
141 
142     @Before
setUp()143     public void setUp() {
144         MockitoAnnotations.initMocks(this);
145 
146         when(mNotificationSwipeHelperBuilder.build()).thenReturn(mNotificationSwipeHelper);
147         when(mFeatureFlags.isNewNotifPipelineRenderingEnabled()).thenReturn(false);
148         when(mFgServicesSectionController.createView(mLayoutInflater))
149                 .thenReturn(mForegroundServiceDungeonView);
150 
151         mController = new NotificationStackScrollLayoutController(
152                 true,
153                 mNotificationGutsManager,
154                 mHeadsUpManager,
155                 mNotificationRoundnessManager,
156                 mTunerService,
157                 mDeviceProvisionedController,
158                 mDynamicPrivacyController,
159                 mConfigurationController,
160                 mSysuiStatusBarStateController,
161                 mKeyguardMediaController,
162                 mKeyguardBypassController,
163                 mZenModeController,
164                 mColorExtractor,
165                 mNotificationLockscreenUserManager,
166                 mMetricsLogger,
167                 new FalsingCollectorFake(),
168                 new FalsingManagerFake(),
169                 mResources,
170                 mNotificationSwipeHelperBuilder,
171                 mStatusBar,
172                 mScrimController,
173                 mLegacyGroupManager,
174                 mLegacyGroupManager,
175                 mSilentHeaderController,
176                 mFeatureFlags,
177                 mNotifPipeline,
178                 mNotifCollection,
179                 mEntryManager,
180                 mLockscreenShadeTransitionController,
181                 mIStatusBarService,
182                 mUiEventLogger,
183                 mFgFeatureController,
184                 mFgServicesSectionController,
185                 mLayoutInflater,
186                 mRemoteInputManager,
187                 mVisualStabilityManager,
188                 mShadeController
189         );
190 
191         when(mNotificationStackScrollLayout.isAttachedToWindow()).thenReturn(true);
192     }
193 
194     @Test
testAttach_viewAlreadyAttached()195     public void testAttach_viewAlreadyAttached() {
196         mController.attach(mNotificationStackScrollLayout);
197 
198         verify(mConfigurationController).addCallback(
199                 any(ConfigurationController.ConfigurationListener.class));
200     }
201     @Test
testAttach_viewAttachedAfterInit()202     public void testAttach_viewAttachedAfterInit() {
203         when(mNotificationStackScrollLayout.isAttachedToWindow()).thenReturn(false);
204 
205         mController.attach(mNotificationStackScrollLayout);
206 
207         verify(mConfigurationController, never()).addCallback(
208                 any(ConfigurationController.ConfigurationListener.class));
209 
210         mController.mOnAttachStateChangeListener.onViewAttachedToWindow(
211                 mNotificationStackScrollLayout);
212 
213         verify(mConfigurationController).addCallback(
214                 any(ConfigurationController.ConfigurationListener.class));
215     }
216 
217     @Test
testOnDensityOrFontScaleChanged_reInflatesFooterViews()218     public void testOnDensityOrFontScaleChanged_reInflatesFooterViews() {
219         mController.attach(mNotificationStackScrollLayout);
220         mController.mConfigurationListener.onDensityOrFontScaleChanged();
221         verify(mNotificationStackScrollLayout).reinflateViews();
222     }
223 
224     @Test
testUpdateEmptyShadeView_notificationsVisible_zenHiding()225     public void testUpdateEmptyShadeView_notificationsVisible_zenHiding() {
226         when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(true);
227         mController.attach(mNotificationStackScrollLayout);
228         verify(mSysuiStatusBarStateController).addCallback(
229                 mStateListenerArgumentCaptor.capture(), anyInt());
230         StatusBarStateController.StateListener stateListener =
231                 mStateListenerArgumentCaptor.getValue();
232 
233         setupShowEmptyShadeViewState(stateListener, true);
234         reset(mNotificationStackScrollLayout);
235         mController.updateShowEmptyShadeView();
236         verify(mNotificationStackScrollLayout).updateEmptyShadeView(
237                 /* visible= */ true,
238                 /* notifVisibleInShade= */ true);
239 
240         setupShowEmptyShadeViewState(stateListener, false);
241         reset(mNotificationStackScrollLayout);
242         mController.updateShowEmptyShadeView();
243         verify(mNotificationStackScrollLayout).updateEmptyShadeView(
244                 /* visible= */ false,
245                 /* notifVisibleInShade= */ true);
246     }
247 
248     @Test
testUpdateEmptyShadeView_notificationsHidden_zenNotHiding()249     public void testUpdateEmptyShadeView_notificationsHidden_zenNotHiding() {
250         when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(false);
251         mController.attach(mNotificationStackScrollLayout);
252         verify(mSysuiStatusBarStateController).addCallback(
253                 mStateListenerArgumentCaptor.capture(), anyInt());
254         StatusBarStateController.StateListener stateListener =
255                 mStateListenerArgumentCaptor.getValue();
256 
257         setupShowEmptyShadeViewState(stateListener, true);
258         reset(mNotificationStackScrollLayout);
259         mController.updateShowEmptyShadeView();
260         verify(mNotificationStackScrollLayout).updateEmptyShadeView(
261                 /* visible= */ true,
262                 /* notifVisibleInShade= */ false);
263 
264         setupShowEmptyShadeViewState(stateListener, false);
265         reset(mNotificationStackScrollLayout);
266         mController.updateShowEmptyShadeView();
267         verify(mNotificationStackScrollLayout).updateEmptyShadeView(
268                 /* visible= */ false,
269                 /* notifVisibleInShade= */ false);
270     }
271 
272     @Test
testUpdateEmptyShadeView_splitShadeMode_alwaysShowEmptyView()273     public void testUpdateEmptyShadeView_splitShadeMode_alwaysShowEmptyView() {
274         when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(false);
275         mController.attach(mNotificationStackScrollLayout);
276         verify(mSysuiStatusBarStateController).addCallback(
277                 mStateListenerArgumentCaptor.capture(), anyInt());
278         StatusBarStateController.StateListener stateListener =
279                 mStateListenerArgumentCaptor.getValue();
280         when(mNotificationStackScrollLayout.isUsingSplitNotificationShade()).thenReturn(true);
281         stateListener.onStateChanged(SHADE);
282         mController.getView().removeAllViews();
283 
284         mController.setQsExpanded(false);
285         reset(mNotificationStackScrollLayout);
286         mController.updateShowEmptyShadeView();
287         verify(mNotificationStackScrollLayout).updateEmptyShadeView(
288                 /* visible= */ true,
289                 /* notifVisibleInShade= */ false);
290 
291         mController.setQsExpanded(true);
292         reset(mNotificationStackScrollLayout);
293         mController.updateShowEmptyShadeView();
294         verify(mNotificationStackScrollLayout).updateEmptyShadeView(
295                 /* visible= */ true,
296                 /* notifVisibleInShade= */ false);
297     }
298 
299     @Test
testOnUserChange_verifySensitiveProfile()300     public void testOnUserChange_verifySensitiveProfile() {
301         when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true);
302 
303         ArgumentCaptor<UserChangedListener> userChangedCaptor = ArgumentCaptor
304                 .forClass(UserChangedListener.class);
305 
306         mController.attach(mNotificationStackScrollLayout);
307         verify(mNotificationLockscreenUserManager)
308                 .addUserChangedListener(userChangedCaptor.capture());
309         reset(mNotificationStackScrollLayout);
310 
311         UserChangedListener changedListener = userChangedCaptor.getValue();
312         changedListener.onUserChanged(0);
313         verify(mNotificationStackScrollLayout).updateSensitiveness(false, true);
314     }
315 
316     @Test
testOnStatePostChange_verifyIfProfileIsPublic()317     public void testOnStatePostChange_verifyIfProfileIsPublic() {
318         when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true);
319 
320         mController.attach(mNotificationStackScrollLayout);
321         verify(mSysuiStatusBarStateController).addCallback(
322                 mStateListenerArgumentCaptor.capture(), anyInt());
323 
324         StatusBarStateController.StateListener stateListener =
325                 mStateListenerArgumentCaptor.getValue();
326 
327         stateListener.onStatePostChange();
328         verify(mNotificationStackScrollLayout).updateSensitiveness(false, true);
329     }
330 
331     @Test
testOnMenuShownLogging()332     public void testOnMenuShownLogging() {
333         ExpandableNotificationRow row = mock(ExpandableNotificationRow.class, RETURNS_DEEP_STUBS);
334         when(row.getEntry().getSbn().getLogMaker()).thenReturn(new LogMaker(
335                 MetricsProto.MetricsEvent.VIEW_UNKNOWN));
336 
337         ArgumentCaptor<OnMenuEventListener> onMenuEventListenerArgumentCaptor =
338                 ArgumentCaptor.forClass(OnMenuEventListener.class);
339 
340         mController.attach(mNotificationStackScrollLayout);
341         verify(mNotificationSwipeHelperBuilder).setOnMenuEventListener(
342                 onMenuEventListenerArgumentCaptor.capture());
343 
344         OnMenuEventListener onMenuEventListener = onMenuEventListenerArgumentCaptor.getValue();
345 
346         onMenuEventListener.onMenuShown(row);
347         verify(row.getEntry().getSbn()).getLogMaker();  // This writes most of the log data
348         verify(mMetricsLogger).write(logMatcher(MetricsProto.MetricsEvent.ACTION_REVEAL_GEAR,
349                 MetricsProto.MetricsEvent.TYPE_ACTION));
350     }
351 
352     @Test
testOnMenuClickedLogging()353     public void testOnMenuClickedLogging() {
354         ExpandableNotificationRow row = mock(ExpandableNotificationRow.class, RETURNS_DEEP_STUBS);
355         when(row.getEntry().getSbn().getLogMaker()).thenReturn(new LogMaker(
356                 MetricsProto.MetricsEvent.VIEW_UNKNOWN));
357 
358         ArgumentCaptor<OnMenuEventListener> onMenuEventListenerArgumentCaptor =
359                 ArgumentCaptor.forClass(OnMenuEventListener.class);
360 
361         mController.attach(mNotificationStackScrollLayout);
362         verify(mNotificationSwipeHelperBuilder).setOnMenuEventListener(
363                 onMenuEventListenerArgumentCaptor.capture());
364 
365         OnMenuEventListener onMenuEventListener = onMenuEventListenerArgumentCaptor.getValue();
366 
367         onMenuEventListener.onMenuClicked(row, 0, 0, mock(
368                 NotificationMenuRowPlugin.MenuItem.class));
369         verify(row.getEntry().getSbn()).getLogMaker();  // This writes most of the log data
370         verify(mMetricsLogger).write(logMatcher(MetricsProto.MetricsEvent.ACTION_TOUCH_GEAR,
371                 MetricsProto.MetricsEvent.TYPE_ACTION));
372     }
373 
374     @Test
testDismissListener()375     public void testDismissListener() {
376         ArgumentCaptor<NotificationStackScrollLayout.DismissListener>
377                 dismissListenerArgumentCaptor = ArgumentCaptor.forClass(
378                 NotificationStackScrollLayout.DismissListener.class);
379 
380         mController.attach(mNotificationStackScrollLayout);
381 
382         verify(mNotificationStackScrollLayout).setDismissListener(
383                 dismissListenerArgumentCaptor.capture());
384         NotificationStackScrollLayout.DismissListener dismissListener =
385                 dismissListenerArgumentCaptor.getValue();
386 
387         dismissListener.onDismiss(ROWS_ALL);
388         verify(mUiEventLogger).log(NotificationPanelEvent.fromSelection(ROWS_ALL));
389     }
390 
391     @Test
testForegroundDismissEnabled()392     public void testForegroundDismissEnabled() {
393         when(mFgFeatureController.isForegroundServiceDismissalEnabled()).thenReturn(true);
394         mController.attach(mNotificationStackScrollLayout);
395         verify(mNotificationStackScrollLayout).initializeForegroundServiceSection(
396                 mForegroundServiceDungeonView);
397     }
398 
399     @Test
testForegroundDismissaDisabled()400     public void testForegroundDismissaDisabled() {
401         when(mFgFeatureController.isForegroundServiceDismissalEnabled()).thenReturn(false);
402         mController.attach(mNotificationStackScrollLayout);
403         verify(mNotificationStackScrollLayout, never()).initializeForegroundServiceSection(
404                 any(ForegroundServiceDungeonView.class));
405     }
406 
407     @Test
testUpdateFooter_remoteInput()408     public void testUpdateFooter_remoteInput() {
409         ArgumentCaptor<RemoteInputController.Callback> callbackCaptor =
410                 ArgumentCaptor.forClass(RemoteInputController.Callback.class);
411         doNothing().when(mRemoteInputManager).addControllerCallback(callbackCaptor.capture());
412         when(mRemoteInputManager.isRemoteInputActive()).thenReturn(false);
413         mController.attach(mNotificationStackScrollLayout);
414         verify(mNotificationStackScrollLayout).setIsRemoteInputActive(false);
415         RemoteInputController.Callback callback = callbackCaptor.getValue();
416         callback.onRemoteInputActive(true);
417         verify(mNotificationStackScrollLayout).setIsRemoteInputActive(true);
418     }
419 
logMatcher(int category, int type)420     private LogMaker logMatcher(int category, int type) {
421         return argThat(new LogMatcher(category, type));
422     }
423 
setupShowEmptyShadeViewState( StatusBarStateController.StateListener statusBarStateListener, boolean toShow)424     private void setupShowEmptyShadeViewState(
425             StatusBarStateController.StateListener statusBarStateListener,
426             boolean toShow) {
427         if (toShow) {
428             statusBarStateListener.onStateChanged(SHADE);
429             mController.setQsExpanded(false);
430             mController.getView().removeAllViews();
431         } else {
432             statusBarStateListener.onStateChanged(KEYGUARD);
433             mController.setQsExpanded(true);
434             mController.getView().addContainerView(mock(ExpandableNotificationRow.class));
435         }
436     }
437 
438     static class LogMatcher implements ArgumentMatcher<LogMaker> {
439         private int mCategory, mType;
440 
LogMatcher(int category, int type)441         LogMatcher(int category, int type) {
442             mCategory = category;
443             mType = type;
444         }
matches(LogMaker l)445         public boolean matches(LogMaker l) {
446             return (l.getCategory() == mCategory)
447                     && (l.getType() == mType);
448         }
449 
toString()450         public String toString() {
451             return String.format("LogMaker(%d, %d)", mCategory, mType);
452         }
453     }
454 }
455