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.collection.coordinator;
18 
19 import static org.junit.Assert.assertFalse;
20 import static org.junit.Assert.assertTrue;
21 import static org.mockito.ArgumentMatchers.any;
22 import static org.mockito.ArgumentMatchers.eq;
23 import static org.mockito.Mockito.never;
24 import static org.mockito.Mockito.verify;
25 import static org.mockito.Mockito.when;
26 
27 import android.testing.AndroidTestingRunner;
28 import android.testing.TestableLooper;
29 
30 import androidx.test.filters.SmallTest;
31 
32 import com.android.systemui.SysuiTestCase;
33 import com.android.systemui.statusbar.NotificationRemoteInputManager;
34 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
35 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
36 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
37 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter;
38 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner;
39 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
40 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender;
41 import com.android.systemui.statusbar.notification.collection.render.NodeController;
42 import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder;
43 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider;
44 import com.android.systemui.statusbar.notification.row.NotifBindPipeline.BindCallback;
45 import com.android.systemui.statusbar.policy.HeadsUpManager;
46 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
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 @TestableLooper.RunWithLooper
58 public class HeadsUpCoordinatorTest extends SysuiTestCase {
59 
60     private HeadsUpCoordinator mCoordinator;
61 
62     // captured listeners and pluggables:
63     private NotifCollectionListener mCollectionListener;
64     private NotifPromoter mNotifPromoter;
65     private NotifLifetimeExtender mNotifLifetimeExtender;
66     private OnHeadsUpChangedListener mOnHeadsUpChangedListener;
67     private NotifSectioner mNotifSectioner;
68 
69     @Mock private NotifPipeline mNotifPipeline;
70     @Mock private HeadsUpManager mHeadsUpManager;
71     @Mock private HeadsUpViewBinder mHeadsUpViewBinder;
72     @Mock private NotificationInterruptStateProvider mNotificationInterruptStateProvider;
73     @Mock private NotificationRemoteInputManager mRemoteInputManager;
74     @Mock private NotifLifetimeExtender.OnEndLifetimeExtensionCallback mEndLifetimeExtension;
75     @Mock private NodeController mHeaderController;
76 
77     private NotificationEntry mEntry;
78 
79     @Before
setUp()80     public void setUp() {
81         MockitoAnnotations.initMocks(this);
82 
83         mCoordinator = new HeadsUpCoordinator(
84                 mHeadsUpManager,
85                 mHeadsUpViewBinder,
86                 mNotificationInterruptStateProvider,
87                 mRemoteInputManager,
88                 mHeaderController);
89 
90         mCoordinator.attach(mNotifPipeline);
91 
92         // capture arguments:
93         ArgumentCaptor<NotifCollectionListener> notifCollectionCaptor =
94                 ArgumentCaptor.forClass(NotifCollectionListener.class);
95         ArgumentCaptor<NotifPromoter> notifPromoterCaptor =
96                 ArgumentCaptor.forClass(NotifPromoter.class);
97         ArgumentCaptor<NotifLifetimeExtender> notifLifetimeExtenderCaptor =
98                 ArgumentCaptor.forClass(NotifLifetimeExtender.class);
99         ArgumentCaptor<OnHeadsUpChangedListener> headsUpChangedListenerCaptor =
100                 ArgumentCaptor.forClass(OnHeadsUpChangedListener.class);
101 
102         verify(mNotifPipeline).addCollectionListener(notifCollectionCaptor.capture());
103         verify(mNotifPipeline).addPromoter(notifPromoterCaptor.capture());
104         verify(mNotifPipeline).addNotificationLifetimeExtender(
105                 notifLifetimeExtenderCaptor.capture());
106         verify(mHeadsUpManager).addListener(headsUpChangedListenerCaptor.capture());
107 
108         mCollectionListener = notifCollectionCaptor.getValue();
109         mNotifPromoter = notifPromoterCaptor.getValue();
110         mNotifLifetimeExtender = notifLifetimeExtenderCaptor.getValue();
111         mOnHeadsUpChangedListener = headsUpChangedListenerCaptor.getValue();
112 
113         mNotifSectioner = mCoordinator.getSectioner();
114         mNotifLifetimeExtender.setCallback(mEndLifetimeExtension);
115         mEntry = new NotificationEntryBuilder().build();
116     }
117 
118     @Test
testPromotesCurrentHUN()119     public void testPromotesCurrentHUN() {
120         // GIVEN the current HUN is set to mEntry
121         setCurrentHUN(mEntry);
122 
123         // THEN only promote the current HUN, mEntry
124         assertTrue(mNotifPromoter.shouldPromoteToTopLevel(mEntry));
125         assertFalse(mNotifPromoter.shouldPromoteToTopLevel(new NotificationEntryBuilder().build()));
126     }
127 
128     @Test
testIncludeInSectionCurrentHUN()129     public void testIncludeInSectionCurrentHUN() {
130         // GIVEN the current HUN is set to mEntry
131         setCurrentHUN(mEntry);
132 
133         // THEN only section the current HUN, mEntry
134         assertTrue(mNotifSectioner.isInSection(mEntry));
135         assertFalse(mNotifSectioner.isInSection(new NotificationEntryBuilder().build()));
136     }
137 
138     @Test
testLifetimeExtendsCurrentHUN()139     public void testLifetimeExtendsCurrentHUN() {
140         // GIVEN there is a HUN, mEntry
141         setCurrentHUN(mEntry);
142 
143         // THEN only the current HUN, mEntry, should be lifetimeExtended
144         assertTrue(mNotifLifetimeExtender.shouldExtendLifetime(mEntry, /* cancellationReason */ 0));
145         assertFalse(mNotifLifetimeExtender.shouldExtendLifetime(
146                 new NotificationEntryBuilder().build(), /* cancellationReason */ 0));
147     }
148 
149     @Test
testLifetimeExtensionEndsOnNewHUN()150     public void testLifetimeExtensionEndsOnNewHUN() {
151         // GIVEN there was a HUN that was lifetime extended
152         setCurrentHUN(mEntry);
153         assertTrue(mNotifLifetimeExtender.shouldExtendLifetime(
154                 mEntry, /* cancellation reason */ 0));
155 
156         // WHEN there's a new HUN
157         NotificationEntry newHUN = new NotificationEntryBuilder().build();
158         setCurrentHUN(newHUN);
159 
160         // THEN the old entry's lifetime extension should be cancelled
161         verify(mEndLifetimeExtension).onEndLifetimeExtension(mNotifLifetimeExtender, mEntry);
162     }
163 
164     @Test
testLifetimeExtensionEndsOnNoHUNs()165     public void testLifetimeExtensionEndsOnNoHUNs() {
166         // GIVEN there was a HUN that was lifetime extended
167         setCurrentHUN(mEntry);
168         assertTrue(mNotifLifetimeExtender.shouldExtendLifetime(
169                 mEntry, /* cancellation reason */ 0));
170 
171         // WHEN there's no longer a HUN
172         setCurrentHUN(null);
173 
174         // THEN the old entry's lifetime extension should be cancelled
175         verify(mEndLifetimeExtension).onEndLifetimeExtension(mNotifLifetimeExtender, mEntry);
176     }
177 
178     @Test
testShowHUNOnInflationFinished()179     public void testShowHUNOnInflationFinished() {
180         // WHEN a notification should HUN and its inflation is finished
181         when(mNotificationInterruptStateProvider.shouldHeadsUp(mEntry)).thenReturn(true);
182 
183         ArgumentCaptor<BindCallback> bindCallbackCaptor =
184                 ArgumentCaptor.forClass(BindCallback.class);
185         mCollectionListener.onEntryAdded(mEntry);
186         verify(mHeadsUpViewBinder).bindHeadsUpView(eq(mEntry), bindCallbackCaptor.capture());
187 
188         bindCallbackCaptor.getValue().onBindFinished(mEntry);
189 
190         // THEN we tell the HeadsUpManager to show the notification
191         verify(mHeadsUpManager).showNotification(mEntry);
192     }
193 
194     @Test
testNoHUNOnInflationFinished()195     public void testNoHUNOnInflationFinished() {
196         // WHEN a notification shouldn't HUN and its inflation is finished
197         when(mNotificationInterruptStateProvider.shouldHeadsUp(mEntry)).thenReturn(false);
198         ArgumentCaptor<BindCallback> bindCallbackCaptor =
199                 ArgumentCaptor.forClass(BindCallback.class);
200         mCollectionListener.onEntryAdded(mEntry);
201 
202         // THEN we never bind the heads up view or tell HeadsUpManager to show the notification
203         verify(mHeadsUpViewBinder, never()).bindHeadsUpView(
204                 eq(mEntry), bindCallbackCaptor.capture());
205         verify(mHeadsUpManager, never()).showNotification(mEntry);
206     }
207 
208     @Test
testOnEntryRemovedRemovesHeadsUpNotification()209     public void testOnEntryRemovedRemovesHeadsUpNotification() {
210         // GIVEN the current HUN is mEntry
211         setCurrentHUN(mEntry);
212 
213         // WHEN mEntry is removed from the notification collection
214         mCollectionListener.onEntryRemoved(mEntry, /* cancellation reason */ 0);
215         when(mRemoteInputManager.isSpinning(any())).thenReturn(false);
216 
217         // THEN heads up manager should remove the entry
218         verify(mHeadsUpManager).removeNotification(mEntry.getKey(), false);
219     }
220 
setCurrentHUN(NotificationEntry entry)221     private void setCurrentHUN(NotificationEntry entry) {
222         when(mHeadsUpManager.getTopEntry()).thenReturn(entry);
223         when(mHeadsUpManager.isAlerting(any())).thenReturn(false);
224         if (entry != null) {
225             when(mHeadsUpManager.isAlerting(entry.getKey())).thenReturn(true);
226         }
227         mOnHeadsUpChangedListener.onHeadsUpStateChanged(entry, entry != null);
228     }
229 }
230