1 /*
2  * Copyright (C) 2019 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 android.app.NotificationManager.IMPORTANCE_DEFAULT;
20 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT;
21 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST;
22 
23 import static org.junit.Assert.assertEquals;
24 import static org.junit.Assert.assertFalse;
25 import static org.junit.Assert.assertTrue;
26 import static org.mockito.ArgumentMatchers.any;
27 import static org.mockito.ArgumentMatchers.eq;
28 import static org.mockito.Mockito.never;
29 import static org.mockito.Mockito.reset;
30 import static org.mockito.Mockito.spy;
31 import static org.mockito.Mockito.times;
32 import static org.mockito.Mockito.verify;
33 import static org.mockito.Mockito.when;
34 
35 import android.app.Notification;
36 import android.app.NotificationChannel;
37 import android.app.NotificationManager;
38 import android.testing.AndroidTestingRunner;
39 
40 import androidx.annotation.Nullable;
41 import androidx.test.filters.SmallTest;
42 
43 import com.android.systemui.SysuiTestCase;
44 import com.android.systemui.plugins.statusbar.StatusBarStateController;
45 import com.android.systemui.statusbar.RankingBuilder;
46 import com.android.systemui.statusbar.SbnBuilder;
47 import com.android.systemui.statusbar.notification.collection.ListEntry;
48 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
49 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
50 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
51 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
52 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner;
53 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable;
54 import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider;
55 import com.android.systemui.statusbar.notification.collection.render.NodeController;
56 import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController;
57 
58 import org.junit.Before;
59 import org.junit.Test;
60 import org.junit.runner.RunWith;
61 import org.mockito.ArgumentCaptor;
62 import org.mockito.Captor;
63 import org.mockito.Mock;
64 import org.mockito.MockitoAnnotations;
65 
66 import java.util.ArrayList;
67 import java.util.Arrays;
68 
69 @SmallTest
70 @RunWith(AndroidTestingRunner.class)
71 public class RankingCoordinatorTest extends SysuiTestCase {
72 
73     @Mock private StatusBarStateController mStatusBarStateController;
74     @Mock private HighPriorityProvider mHighPriorityProvider;
75     @Mock private NotifPipeline mNotifPipeline;
76     @Mock private NodeController mAlertingHeaderController;
77     @Mock private NodeController mSilentNodeController;
78     @Mock private SectionHeaderController mSilentHeaderController;
79     @Mock private Pluggable.PluggableListener<NotifFilter> mInvalidationListener;
80 
81     @Captor private ArgumentCaptor<NotifFilter> mNotifFilterCaptor;
82     @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerCaptor;
83 
84     private NotificationEntry mEntry;
85     private NotifFilter mCapturedSuspendedFilter;
86     private NotifFilter mCapturedDozingFilter;
87     private StatusBarStateController.StateListener mStatusBarStateCallback;
88     private RankingCoordinator mRankingCoordinator;
89 
90     private NotifSectioner mAlertingSectioner;
91     private NotifSectioner mSilentSectioner;
92     private NotifSectioner mMinimizedSectioner;
93     private ArrayList<NotifSectioner> mSections = new ArrayList<>(3);
94 
95     @Before
setup()96     public void setup() {
97         MockitoAnnotations.initMocks(this);
98         mRankingCoordinator = new RankingCoordinator(
99                 mStatusBarStateController,
100                 mHighPriorityProvider,
101                 mAlertingHeaderController,
102                 mSilentHeaderController,
103                 mSilentNodeController);
104         mEntry = spy(new NotificationEntryBuilder().build());
105         mEntry.setRanking(getRankingForUnfilteredNotif().build());
106 
107         mRankingCoordinator.attach(mNotifPipeline);
108         verify(mNotifPipeline, times(2)).addPreGroupFilter(mNotifFilterCaptor.capture());
109         mCapturedSuspendedFilter = mNotifFilterCaptor.getAllValues().get(0);
110         mCapturedDozingFilter = mNotifFilterCaptor.getAllValues().get(1);
111         mCapturedDozingFilter.setInvalidationListener(mInvalidationListener);
112 
113         verify(mStatusBarStateController, times(1)).addCallback(mStateListenerCaptor.capture());
114         mStatusBarStateCallback = mStateListenerCaptor.getAllValues().get(0);
115 
116         mAlertingSectioner = mRankingCoordinator.getAlertingSectioner();
117         mSilentSectioner = mRankingCoordinator.getSilentSectioner();
118         mMinimizedSectioner = mRankingCoordinator.getMinimizedSectioner();
119         mSections.addAll(Arrays.asList(mAlertingSectioner, mSilentSectioner, mMinimizedSectioner));
120     }
121 
122     @Test
testSilentHeaderClearableChildrenUpdate()123     public void testSilentHeaderClearableChildrenUpdate() {
124         ListEntry listEntry = new ListEntry(mEntry.getKey(), 0L) {
125             @Nullable
126             @Override
127             public NotificationEntry getRepresentativeEntry() {
128                 return mEntry;
129             }
130         };
131         setRankingAmbient(false);
132         setSbnClearable(true);
133         mSilentSectioner.onEntriesUpdated(Arrays.asList(listEntry));
134         verify(mSilentHeaderController).setClearSectionEnabled(eq(true));
135 
136         setSbnClearable(false);
137         mSilentSectioner.onEntriesUpdated(Arrays.asList(listEntry));
138         verify(mSilentHeaderController).setClearSectionEnabled(eq(false));
139     }
140 
141     @Test
testUnfilteredState()142     public void testUnfilteredState() {
143         // GIVEN no suppressed visual effects + app not suspended
144         mEntry.setRanking(getRankingForUnfilteredNotif().build());
145 
146         // THEN don't filter out the notification
147         assertFalse(mCapturedSuspendedFilter.shouldFilterOut(mEntry, 0));
148     }
149 
150     @Test
filterSuspended()151     public void filterSuspended() {
152         // GIVEN the notification's app is suspended
153         mEntry.setRanking(getRankingForUnfilteredNotif()
154                 .setSuspended(true)
155                 .build());
156 
157         // THEN filter out the notification
158         assertTrue(mCapturedSuspendedFilter.shouldFilterOut(mEntry, 0));
159     }
160 
161     @Test
filterDozingSuppressAmbient()162     public void filterDozingSuppressAmbient() {
163         // GIVEN should suppress ambient
164         mEntry.setRanking(getRankingForUnfilteredNotif()
165                 .setSuppressedVisualEffects(SUPPRESSED_EFFECT_AMBIENT)
166                 .build());
167 
168         // WHEN it's dozing (on ambient display)
169         when(mStatusBarStateController.isDozing()).thenReturn(true);
170 
171         // THEN filter out the notification
172         assertTrue(mCapturedDozingFilter.shouldFilterOut(mEntry, 0));
173 
174         // WHEN it's not dozing (showing the notification list)
175         when(mStatusBarStateController.isDozing()).thenReturn(false);
176 
177         // THEN don't filter out the notification
178         assertFalse(mCapturedDozingFilter.shouldFilterOut(mEntry, 0));
179 
180         // WHEN it's not dozing and doze amount is 1
181         when(mStatusBarStateController.isDozing()).thenReturn(false);
182         when(mStatusBarStateController.getDozeAmount()).thenReturn(1f);
183 
184         // THEN filter out the notification
185         assertTrue(mCapturedDozingFilter.shouldFilterOut(mEntry, 0));
186     }
187 
188     @Test
filterDozingSuppressNotificationList()189     public void filterDozingSuppressNotificationList() {
190         // GIVEN should suppress from the notification list
191         mEntry.setRanking(getRankingForUnfilteredNotif()
192                 .setSuppressedVisualEffects(SUPPRESSED_EFFECT_NOTIFICATION_LIST)
193                 .build());
194 
195         // WHEN it's dozing (on ambient display)
196         when(mStatusBarStateController.isDozing()).thenReturn(true);
197 
198         // THEN don't filter out the notification
199         assertFalse(mCapturedDozingFilter.shouldFilterOut(mEntry, 0));
200 
201         // WHEN it's not dozing (showing the notification list)
202         when(mStatusBarStateController.isDozing()).thenReturn(false);
203 
204         // THEN filter out the notification
205         assertTrue(mCapturedDozingFilter.shouldFilterOut(mEntry, 0));
206     }
207 
208     @Test
testIncludeInSectionAlerting()209     public void testIncludeInSectionAlerting() {
210         // GIVEN the entry is high priority
211         when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(true);
212 
213         // THEN entry is in the alerting section
214         assertTrue(mAlertingSectioner.isInSection(mEntry));
215         assertFalse(mSilentSectioner.isInSection(mEntry));
216     }
217 
218     @Test
testIncludeInSectionSilent()219     public void testIncludeInSectionSilent() {
220         // GIVEN the entry isn't high priority
221         when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(false);
222         setRankingAmbient(false);
223 
224         // THEN entry is in the silent section
225         assertFalse(mAlertingSectioner.isInSection(mEntry));
226         assertTrue(mSilentSectioner.isInSection(mEntry));
227     }
228 
229     @Test
testMinSection()230     public void testMinSection() {
231         when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(false);
232         setRankingAmbient(true);
233         assertInSection(mEntry, mMinimizedSectioner);
234     }
235 
236     @Test
testSilentSection()237     public void testSilentSection() {
238         when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(false);
239         setRankingAmbient(false);
240         assertInSection(mEntry, mSilentSectioner);
241     }
242 
243     @Test
testClearableSilentSection()244     public void testClearableSilentSection() {
245         when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(false);
246         setSbnClearable(true);
247         setRankingAmbient(false);
248         mSilentSectioner.onEntriesUpdated(Arrays.asList(mEntry));
249         verify(mSilentHeaderController).setClearSectionEnabled(eq(true));
250     }
251 
252     @Test
testClearableMinimizedSection()253     public void testClearableMinimizedSection() {
254         when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(false);
255         setSbnClearable(true);
256         setRankingAmbient(true);
257         mMinimizedSectioner.onEntriesUpdated(Arrays.asList(mEntry));
258         verify(mSilentHeaderController).setClearSectionEnabled(eq(true));
259     }
260 
261     @Test
testNotClearableSilentSection()262     public void testNotClearableSilentSection() {
263         setSbnClearable(false);
264         when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(false);
265         setRankingAmbient(false);
266         mSilentSectioner.onEntriesUpdated(Arrays.asList(mEntry));
267         mMinimizedSectioner.onEntriesUpdated(Arrays.asList(mEntry));
268         mAlertingSectioner.onEntriesUpdated(Arrays.asList(mEntry));
269         verify(mSilentHeaderController, times(2)).setClearSectionEnabled(eq(false));
270     }
271 
272     @Test
testNotClearableMinimizedSection()273     public void testNotClearableMinimizedSection() {
274         setSbnClearable(false);
275         when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(false);
276         setRankingAmbient(true);
277         mSilentSectioner.onEntriesUpdated(Arrays.asList(mEntry));
278         mMinimizedSectioner.onEntriesUpdated(Arrays.asList(mEntry));
279         mAlertingSectioner.onEntriesUpdated(Arrays.asList(mEntry));
280         verify(mSilentHeaderController, times(2)).setClearSectionEnabled(eq(false));
281     }
282 
283     @Test
statusBarStateCallbackTest()284     public void statusBarStateCallbackTest() {
285         mStatusBarStateCallback.onDozeAmountChanged(1f, 1f);
286         verify(mInvalidationListener, times(1))
287                 .onPluggableInvalidated(mCapturedDozingFilter, "dozeAmount changed to one");
288         reset(mInvalidationListener);
289 
290         mStatusBarStateCallback.onDozeAmountChanged(1f, 1f);
291         verify(mInvalidationListener, never()).onPluggableInvalidated(any(), any());
292         reset(mInvalidationListener);
293 
294         mStatusBarStateCallback.onDozeAmountChanged(0.6f, 0.6f);
295         verify(mInvalidationListener, times(1))
296                 .onPluggableInvalidated(mCapturedDozingFilter, "dozeAmount changed to not one");
297         reset(mInvalidationListener);
298 
299         mStatusBarStateCallback.onDozeAmountChanged(0f, 0f);
300         verify(mInvalidationListener, never()).onPluggableInvalidated(any(), any());
301         reset(mInvalidationListener);
302     }
303 
assertInSection(NotificationEntry entry, NotifSectioner section)304     private void assertInSection(NotificationEntry entry, NotifSectioner section) {
305         for (NotifSectioner current: mSections) {
306             if (current == section) {
307                 assertTrue(current.isInSection(entry));
308             } else {
309                 assertFalse(current.isInSection(entry));
310             }
311         }
312     }
313 
getRankingForUnfilteredNotif()314     private RankingBuilder getRankingForUnfilteredNotif() {
315         return new RankingBuilder(mEntry.getRanking())
316                 .setChannel(new NotificationChannel("id", null, IMPORTANCE_DEFAULT))
317                 .setSuppressedVisualEffects(0)
318                 .setSuspended(false);
319     }
320 
setSbnClearable(boolean clearable)321     private void setSbnClearable(boolean clearable) {
322         mEntry.setSbn(new SbnBuilder(mEntry.getSbn())
323                 .setFlag(mContext, Notification.FLAG_NO_CLEAR, !clearable)
324                 .build());
325         assertEquals(clearable, mEntry.getSbn().isClearable());
326     }
327 
setRankingAmbient(boolean ambient)328     private void setRankingAmbient(boolean ambient) {
329         mEntry.setRanking(new RankingBuilder(mEntry.getRanking())
330                 .setImportance(ambient
331                         ? NotificationManager.IMPORTANCE_MIN
332                         : IMPORTANCE_DEFAULT)
333                 .build());
334         assertEquals(ambient, mEntry.getRanking().isAmbient());
335     }
336 }
337