blob: 58fb53aae7bb2d59b873c35ab43e8ccce10bffea [file] [log] [blame]
Eliot Courtney2b4c3a02017-11-27 13:27:46 +09001/*
2 * Copyright (C) 2017 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
17package com.android.systemui.statusbar;
18
19import static junit.framework.Assert.assertTrue;
20
21import static org.junit.Assert.assertEquals;
Julia Reynoldsfc640012018-02-21 12:25:27 -050022import static org.mockito.ArgumentMatchers.any;
Ned Burnsb3b4bd32019-06-27 19:36:58 -040023import static org.mockito.ArgumentMatchers.anyInt;
24import static org.mockito.Mockito.clearInvocations;
25import static org.mockito.Mockito.doAnswer;
Jason Monk09f4d372018-12-20 15:30:54 -050026import static org.mockito.Mockito.mock;
Julia Reynoldsfc640012018-02-21 12:25:27 -050027import static org.mockito.Mockito.spy;
28import static org.mockito.Mockito.times;
Eliot Courtney2b4c3a02017-11-27 13:27:46 +090029import static org.mockito.Mockito.verify;
30import static org.mockito.Mockito.when;
31
Ned Burnsb3b4bd32019-06-27 19:36:58 -040032import android.os.Handler;
Eliot Courtney2b4c3a02017-11-27 13:27:46 +090033import android.testing.AndroidTestingRunner;
34import android.testing.TestableLooper;
35import android.view.View;
36import android.view.ViewGroup;
37import android.widget.LinearLayout;
38
Brett Chabot84151d92019-02-27 15:37:59 -080039import androidx.test.filters.SmallTest;
40
Jason Monk297c04e2018-08-23 17:16:59 -040041import com.android.systemui.Dependency;
42import com.android.systemui.InitController;
Eliot Courtney2b4c3a02017-11-27 13:27:46 +090043import com.android.systemui.SysuiTestCase;
Selim Cinekfdf80332019-03-07 17:29:55 -080044import com.android.systemui.bubbles.BubbleData;
Eliot Courtney2b4c3a02017-11-27 13:27:46 +090045import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
Selim Cinek6f0a62a2019-04-09 18:40:12 -070046import com.android.systemui.statusbar.notification.DynamicPrivacyController;
Rohan Shah20790b82018-07-02 17:21:04 -070047import com.android.systemui.statusbar.notification.NotificationEntryManager;
Eliot Courtney2b4c3a02017-11-27 13:27:46 +090048import com.android.systemui.statusbar.notification.VisualStabilityManager;
Ned Burnsf81c4c42019-01-07 14:10:43 -050049import com.android.systemui.statusbar.notification.collection.NotificationData;
50import com.android.systemui.statusbar.notification.collection.NotificationEntry;
Rohan Shah20790b82018-07-02 17:21:04 -070051import com.android.systemui.statusbar.notification.logging.NotificationLogger;
52import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
53import com.android.systemui.statusbar.notification.row.ExpandableView;
54import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
Selim Cinekb0fada62019-06-17 19:03:59 -070055import com.android.systemui.statusbar.phone.KeyguardBypassController;
Eliot Courtney2b4c3a02017-11-27 13:27:46 +090056import com.android.systemui.statusbar.phone.NotificationGroupManager;
Jason Monk297c04e2018-08-23 17:16:59 -040057import com.android.systemui.statusbar.phone.ShadeController;
Jason Monka716bac2018-12-05 15:48:21 -050058import com.android.systemui.util.Assert;
Eliot Courtney2b4c3a02017-11-27 13:27:46 +090059
60import com.google.android.collect.Lists;
61
62import org.junit.Before;
63import org.junit.Test;
64import org.junit.runner.RunWith;
65import org.mockito.Mock;
66import org.mockito.MockitoAnnotations;
67import org.mockito.Spy;
68
69import java.util.List;
70
71@SmallTest
72@RunWith(AndroidTestingRunner.class)
Jason Monka716bac2018-12-05 15:48:21 -050073@TestableLooper.RunWithLooper
Eliot Courtney2b4c3a02017-11-27 13:27:46 +090074public class NotificationViewHierarchyManagerTest extends SysuiTestCase {
75 @Mock private NotificationPresenter mPresenter;
Eliot Courtney8f56b0e2017-12-14 18:54:28 +090076 @Mock private NotificationData mNotificationData;
77 @Spy private FakeListContainer mListContainer = new FakeListContainer();
78
79 // Dependency mocks:
Eliot Courtney2b4c3a02017-11-27 13:27:46 +090080 @Mock private NotificationEntryManager mEntryManager;
81 @Mock private NotificationLockscreenUserManager mLockscreenUserManager;
82 @Mock private NotificationGroupManager mGroupManager;
83 @Mock private VisualStabilityManager mVisualStabilityManager;
Jason Monk297c04e2018-08-23 17:16:59 -040084 @Mock private ShadeController mShadeController;
Eliot Courtney2b4c3a02017-11-27 13:27:46 +090085
Ned Burnsb3b4bd32019-06-27 19:36:58 -040086 private TestableLooper mTestableLooper;
87 private Handler mHandler;
Eliot Courtney2b4c3a02017-11-27 13:27:46 +090088 private NotificationViewHierarchyManager mViewHierarchyManager;
Ned Burns01e38212019-01-03 16:32:52 -050089 private NotificationTestHelper mHelper;
Ned Burnsb3b4bd32019-06-27 19:36:58 -040090 private boolean mMadeReentrantCall = false;
Eliot Courtney2b4c3a02017-11-27 13:27:46 +090091
92 @Before
93 public void setUp() {
94 MockitoAnnotations.initMocks(this);
Ned Burnsb3b4bd32019-06-27 19:36:58 -040095 mTestableLooper = TestableLooper.get(this);
96 Assert.sMainLooper = mTestableLooper.getLooper();
97 mHandler = Handler.createAsync(mTestableLooper.getLooper());
98
Eliot Courtney8f56b0e2017-12-14 18:54:28 +090099 mDependency.injectTestDependency(NotificationEntryManager.class, mEntryManager);
100 mDependency.injectTestDependency(NotificationLockscreenUserManager.class,
101 mLockscreenUserManager);
102 mDependency.injectTestDependency(NotificationGroupManager.class, mGroupManager);
103 mDependency.injectTestDependency(VisualStabilityManager.class, mVisualStabilityManager);
Jason Monk297c04e2018-08-23 17:16:59 -0400104 mDependency.injectTestDependency(ShadeController.class, mShadeController);
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900105
Ned Burns01e38212019-01-03 16:32:52 -0500106 mHelper = new NotificationTestHelper(mContext);
107
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900108 when(mEntryManager.getNotificationData()).thenReturn(mNotificationData);
109
Jason Monk09f4d372018-12-20 15:30:54 -0500110 mViewHierarchyManager = new NotificationViewHierarchyManager(mContext,
Ned Burnsb3b4bd32019-06-27 19:36:58 -0400111 mHandler, mLockscreenUserManager, mGroupManager, mVisualStabilityManager,
Beverly8fdb5332019-02-04 14:29:49 -0500112 mock(StatusBarStateControllerImpl.class), mEntryManager,
Selim Cinekb0fada62019-06-17 19:03:59 -0700113 () -> mShadeController, new BubbleData(mContext),
114 mock(KeyguardBypassController.class),
115 mock(DynamicPrivacyController.class));
Jason Monk297c04e2018-08-23 17:16:59 -0400116 Dependency.get(InitController.class).executePostInitTasks();
117 mViewHierarchyManager.setUpWithPresenter(mPresenter, mListContainer);
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900118 }
119
Ned Burnsf81c4c42019-01-07 14:10:43 -0500120 private NotificationEntry createEntry() throws Exception {
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900121 ExpandableNotificationRow row = mHelper.createRow();
Ned Burnsf81c4c42019-01-07 14:10:43 -0500122 NotificationEntry entry = new NotificationEntry(row.getStatusBarNotification());
Evan Laird94492852018-10-25 13:43:01 -0400123 entry.setRow(row);
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900124 return entry;
125 }
126
127 @Test
128 public void testNotificationsBecomingBundled() throws Exception {
129 // Tests 3 top level notifications becoming a single bundled notification with |entry0| as
130 // the summary.
Ned Burnsf81c4c42019-01-07 14:10:43 -0500131 NotificationEntry entry0 = createEntry();
132 NotificationEntry entry1 = createEntry();
133 NotificationEntry entry2 = createEntry();
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900134
135 // Set up the prior state to look like three top level notifications.
Evan Laird94492852018-10-25 13:43:01 -0400136 mListContainer.addContainerView(entry0.getRow());
137 mListContainer.addContainerView(entry1.getRow());
138 mListContainer.addContainerView(entry2.getRow());
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900139 when(mNotificationData.getActiveNotifications()).thenReturn(
140 Lists.newArrayList(entry0, entry1, entry2));
141
142 // Set up group manager to report that they should be bundled now.
143 when(mGroupManager.isChildInGroupWithSummary(entry0.notification)).thenReturn(false);
144 when(mGroupManager.isChildInGroupWithSummary(entry1.notification)).thenReturn(true);
145 when(mGroupManager.isChildInGroupWithSummary(entry2.notification)).thenReturn(true);
Evan Laird94492852018-10-25 13:43:01 -0400146 when(mGroupManager.getGroupSummary(entry1.notification)).thenReturn(entry0);
147 when(mGroupManager.getGroupSummary(entry2.notification)).thenReturn(entry0);
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900148
149 // Run updateNotifications - the view hierarchy should be reorganized.
150 mViewHierarchyManager.updateNotificationViews();
151
Evan Laird94492852018-10-25 13:43:01 -0400152 verify(mListContainer).notifyGroupChildAdded(entry1.getRow());
153 verify(mListContainer).notifyGroupChildAdded(entry2.getRow());
154 assertTrue(Lists.newArrayList(entry0.getRow()).equals(mListContainer.mRows));
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900155 }
156
157 @Test
158 public void testNotificationsBecomingUnbundled() throws Exception {
159 // Tests a bundled notification becoming three top level notifications.
Ned Burnsf81c4c42019-01-07 14:10:43 -0500160 NotificationEntry entry0 = createEntry();
161 NotificationEntry entry1 = createEntry();
162 NotificationEntry entry2 = createEntry();
Evan Laird94492852018-10-25 13:43:01 -0400163 entry0.getRow().addChildNotification(entry1.getRow());
164 entry0.getRow().addChildNotification(entry2.getRow());
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900165
166 // Set up the prior state to look like one top level notification.
Evan Laird94492852018-10-25 13:43:01 -0400167 mListContainer.addContainerView(entry0.getRow());
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900168 when(mNotificationData.getActiveNotifications()).thenReturn(
169 Lists.newArrayList(entry0, entry1, entry2));
170
171 // Set up group manager to report that they should not be bundled now.
172 when(mGroupManager.isChildInGroupWithSummary(entry0.notification)).thenReturn(false);
173 when(mGroupManager.isChildInGroupWithSummary(entry1.notification)).thenReturn(false);
174 when(mGroupManager.isChildInGroupWithSummary(entry2.notification)).thenReturn(false);
175
176 // Run updateNotifications - the view hierarchy should be reorganized.
177 mViewHierarchyManager.updateNotificationViews();
178
179 verify(mListContainer).notifyGroupChildRemoved(
Evan Laird94492852018-10-25 13:43:01 -0400180 entry1.getRow(), entry0.getRow().getChildrenContainer());
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900181 verify(mListContainer).notifyGroupChildRemoved(
Evan Laird94492852018-10-25 13:43:01 -0400182 entry2.getRow(), entry0.getRow().getChildrenContainer());
183 assertTrue(
184 Lists.newArrayList(entry0.getRow(), entry1.getRow(), entry2.getRow())
185 .equals(mListContainer.mRows));
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900186 }
187
188 @Test
189 public void testNotificationsBecomingSuppressed() throws Exception {
190 // Tests two top level notifications becoming a suppressed summary and a child.
Ned Burnsf81c4c42019-01-07 14:10:43 -0500191 NotificationEntry entry0 = createEntry();
192 NotificationEntry entry1 = createEntry();
Evan Laird94492852018-10-25 13:43:01 -0400193 entry0.getRow().addChildNotification(entry1.getRow());
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900194
195 // Set up the prior state to look like a top level notification.
Evan Laird94492852018-10-25 13:43:01 -0400196 mListContainer.addContainerView(entry0.getRow());
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900197 when(mNotificationData.getActiveNotifications()).thenReturn(
198 Lists.newArrayList(entry0, entry1));
199
200 // Set up group manager to report a suppressed summary now.
201 when(mGroupManager.isChildInGroupWithSummary(entry0.notification)).thenReturn(false);
202 when(mGroupManager.isChildInGroupWithSummary(entry1.notification)).thenReturn(false);
203 when(mGroupManager.isSummaryOfSuppressedGroup(entry0.notification)).thenReturn(true);
204
205 // Run updateNotifications - the view hierarchy should be reorganized.
206 mViewHierarchyManager.updateNotificationViews();
207
208 verify(mListContainer).notifyGroupChildRemoved(
Evan Laird94492852018-10-25 13:43:01 -0400209 entry1.getRow(), entry0.getRow().getChildrenContainer());
210 assertTrue(Lists.newArrayList(entry0.getRow(), entry1.getRow()).equals(mListContainer.mRows));
211 assertEquals(View.GONE, entry0.getRow().getVisibility());
212 assertEquals(View.VISIBLE, entry1.getRow().getVisibility());
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900213 }
214
Julia Reynoldsfc640012018-02-21 12:25:27 -0500215 @Test
216 public void testUpdateNotificationViews_appOps() throws Exception {
Ned Burnsf81c4c42019-01-07 14:10:43 -0500217 NotificationEntry entry0 = createEntry();
Evan Laird94492852018-10-25 13:43:01 -0400218 entry0.setRow(spy(entry0.getRow()));
Julia Reynoldsfc640012018-02-21 12:25:27 -0500219 when(mNotificationData.getActiveNotifications()).thenReturn(
220 Lists.newArrayList(entry0));
Evan Laird94492852018-10-25 13:43:01 -0400221 mListContainer.addContainerView(entry0.getRow());
Julia Reynoldsfc640012018-02-21 12:25:27 -0500222
223 mViewHierarchyManager.updateNotificationViews();
224
Evan Laird94492852018-10-25 13:43:01 -0400225 verify(entry0.getRow(), times(1)).showAppOpsIcons(any());
Julia Reynoldsfc640012018-02-21 12:25:27 -0500226 }
227
Ned Burnsb3b4bd32019-06-27 19:36:58 -0400228 @Test
229 public void testReentrantCallsToOnDynamicPrivacyChangedPostForLater() {
230 // GIVEN a ListContainer that will make a re-entrant call to updateNotificationViews()
231 mMadeReentrantCall = false;
232 doAnswer((invocation) -> {
233 if (!mMadeReentrantCall) {
234 mMadeReentrantCall = true;
235 mViewHierarchyManager.onDynamicPrivacyChanged();
236 }
237 return null;
238 }).when(mListContainer).setMaxDisplayedNotifications(anyInt());
239
240 // WHEN we call updateNotificationViews()
241 mViewHierarchyManager.updateNotificationViews();
242
243 // THEN onNotificationViewUpdateFinished() is only called once
244 verify(mListContainer).onNotificationViewUpdateFinished();
245
246 // WHEN we drain the looper
247 mTestableLooper.processAllMessages();
248
249 // THEN updateNotificationViews() is called a second time (for the reentrant call)
250 verify(mListContainer, times(2)).onNotificationViewUpdateFinished();
251 }
252
253 @Test
254 public void testMultipleReentrantCallsToOnDynamicPrivacyChangedOnlyPostOnce() {
255 // GIVEN a ListContainer that will make many re-entrant calls to updateNotificationViews()
256 mMadeReentrantCall = false;
257 doAnswer((invocation) -> {
258 if (!mMadeReentrantCall) {
259 mMadeReentrantCall = true;
260 mViewHierarchyManager.onDynamicPrivacyChanged();
261 mViewHierarchyManager.onDynamicPrivacyChanged();
262 mViewHierarchyManager.onDynamicPrivacyChanged();
263 mViewHierarchyManager.onDynamicPrivacyChanged();
264 }
265 return null;
266 }).when(mListContainer).setMaxDisplayedNotifications(anyInt());
267
268 // WHEN we call updateNotificationViews() and drain the looper
269 mViewHierarchyManager.updateNotificationViews();
270 verify(mListContainer).onNotificationViewUpdateFinished();
271 clearInvocations(mListContainer);
272 mTestableLooper.processAllMessages();
273
274 // THEN updateNotificationViews() is called only one more time
275 verify(mListContainer).onNotificationViewUpdateFinished();
276 }
277
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900278 private class FakeListContainer implements NotificationListContainer {
279 final LinearLayout mLayout = new LinearLayout(mContext);
280 final List<View> mRows = Lists.newArrayList();
Ned Burnsb3b4bd32019-06-27 19:36:58 -0400281 private boolean mMakeReentrantCallDuringSetMaxDisplayedNotifications;
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900282
283 @Override
284 public void setChildTransferInProgress(boolean childTransferInProgress) {}
285
286 @Override
Dave Mankoffa4d195d2018-11-16 13:33:27 -0500287 public void changeViewPosition(ExpandableView child, int newIndex) {
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900288 mRows.remove(child);
289 mRows.add(newIndex, child);
290 }
291
292 @Override
Dave Mankoffa4d195d2018-11-16 13:33:27 -0500293 public void notifyGroupChildAdded(ExpandableView row) {}
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900294
295 @Override
Dave Mankoffa4d195d2018-11-16 13:33:27 -0500296 public void notifyGroupChildRemoved(ExpandableView row, ViewGroup childrenContainer) {}
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900297
298 @Override
Dave Mankoffa4d195d2018-11-16 13:33:27 -0500299 public void generateAddAnimation(ExpandableView child, boolean fromMoreCard) {}
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900300
301 @Override
302 public void generateChildOrderChangedEvent() {}
303
304 @Override
Aaron Heuckrothcd944dc2018-10-01 16:31:08 -0400305 public void onReset(ExpandableView view) {}
306
307 @Override
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900308 public int getContainerChildCount() {
309 return mRows.size();
310 }
311
312 @Override
313 public View getContainerChildAt(int i) {
314 return mRows.get(i);
315 }
316
317 @Override
318 public void removeContainerView(View v) {
319 mLayout.removeView(v);
320 mRows.remove(v);
321 }
322
323 @Override
324 public void addContainerView(View v) {
325 mLayout.addView(v);
326 mRows.add(v);
327 }
328
329 @Override
Ned Burnsb3b4bd32019-06-27 19:36:58 -0400330 public void setMaxDisplayedNotifications(int maxNotifications) {
331 if (mMakeReentrantCallDuringSetMaxDisplayedNotifications) {
332 mViewHierarchyManager.onDynamicPrivacyChanged();
333 }
334 }
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900335
336 @Override
Ned Burnsf81c4c42019-01-07 14:10:43 -0500337 public ViewGroup getViewParentForNotification(NotificationEntry entry) {
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900338 return null;
339 }
340
341 @Override
342 public void onHeightChanged(ExpandableView view, boolean animate) {}
343
344 @Override
345 public void resetExposedMenuView(boolean animate, boolean force) {}
346
347 @Override
348 public NotificationSwipeActionHelper getSwipeActionHelper() {
349 return null;
350 }
351
352 @Override
Ned Burnsf81c4c42019-01-07 14:10:43 -0500353 public void cleanUpViewStateForEntry(NotificationEntry entry) { }
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900354
355 @Override
Ned Burnsf81c4c42019-01-07 14:10:43 -0500356 public boolean isInVisibleLocation(NotificationEntry entry) {
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900357 return true;
358 }
359
360 @Override
361 public void setChildLocationsChangedListener(
362 NotificationLogger.OnChildLocationsChangedListener listener) {}
363
364 @Override
365 public boolean hasPulsingNotifications() {
366 return false;
367 }
Evan Laird94492852018-10-25 13:43:01 -0400368
Ned Burnsb3b4bd32019-06-27 19:36:58 -0400369 @Override
370 public void onNotificationViewUpdateFinished() { }
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900371 }
372}