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.qs;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 
21 import static org.junit.Assert.assertEquals;
22 import static org.mockito.ArgumentMatchers.any;
23 import static org.mockito.ArgumentMatchers.anyBoolean;
24 import static org.mockito.ArgumentMatchers.anyFloat;
25 import static org.mockito.ArgumentMatchers.eq;
26 import static org.mockito.Mockito.doAnswer;
27 import static org.mockito.Mockito.never;
28 import static org.mockito.Mockito.reset;
29 import static org.mockito.Mockito.times;
30 import static org.mockito.Mockito.verify;
31 import static org.mockito.Mockito.when;
32 
33 import android.content.res.Configuration;
34 import android.content.res.Resources;
35 import android.testing.AndroidTestingRunner;
36 import android.testing.TestableLooper.RunWithLooper;
37 
38 import androidx.test.filters.SmallTest;
39 
40 import com.android.internal.logging.MetricsLogger;
41 import com.android.internal.logging.UiEventLogger;
42 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
43 import com.android.internal.logging.testing.UiEventLoggerFake;
44 import com.android.systemui.R;
45 import com.android.systemui.SysuiTestCase;
46 import com.android.systemui.dump.DumpManager;
47 import com.android.systemui.media.controls.ui.MediaHost;
48 import com.android.systemui.plugins.qs.QSTile;
49 import com.android.systemui.plugins.qs.QSTileView;
50 import com.android.systemui.qs.customize.QSCustomizerController;
51 import com.android.systemui.qs.logging.QSLogger;
52 import com.android.systemui.qs.tileimpl.QSTileImpl;
53 import com.android.systemui.util.animation.DisappearParameters;
54 
55 import org.junit.Before;
56 import org.junit.Test;
57 import org.junit.runner.RunWith;
58 import org.mockito.Mock;
59 import org.mockito.MockitoAnnotations;
60 
61 import java.io.PrintWriter;
62 import java.io.StringWriter;
63 import java.util.Collections;
64 import java.util.List;
65 
66 @RunWith(AndroidTestingRunner.class)
67 @RunWithLooper
68 @SmallTest
69 public class QSPanelControllerBaseTest extends SysuiTestCase {
70 
71     @Mock
72     private QSPanel mQSPanel;
73     @Mock
74     private QSHost mQSHost;
75     @Mock
76     private QSCustomizerController mQSCustomizerController;
77     @Mock
78     private QSTileRevealController.Factory mQSTileRevealControllerFactory;
79     @Mock
80     private QSTileRevealController mQSTileRevealController;
81     @Mock
82     private MediaHost mMediaHost;
83     @Mock
84     private MetricsLogger mMetricsLogger;
85     private UiEventLoggerFake mUiEventLogger = new UiEventLoggerFake();
86     @Mock
87     private QSLogger mQSLogger;
88     private DumpManager mDumpManager = new DumpManager();
89     @Mock
90     QSTileImpl mQSTile;
91     @Mock
92     QSTile mOtherTile;
93     @Mock
94     QSTileView mQSTileView;
95     @Mock
96     PagedTileLayout mPagedTileLayout;
97     @Mock
98     Resources mResources;
99     @Mock
100     Configuration mConfiguration;
101     @Mock
102     Runnable mHorizontalLayoutListener;
103 
104     private QSPanelControllerBase<QSPanel> mController;
105 
106     /** Implementation needed to ensure we have a reflectively-available class name. */
107     private class TestableQSPanelControllerBase extends QSPanelControllerBase<QSPanel> {
TestableQSPanelControllerBase(QSPanel view, QSHost host, QSCustomizerController qsCustomizerController, MediaHost mediaHost, MetricsLogger metricsLogger, UiEventLogger uiEventLogger, QSLogger qsLogger, DumpManager dumpManager)108         protected TestableQSPanelControllerBase(QSPanel view, QSHost host,
109                 QSCustomizerController qsCustomizerController, MediaHost mediaHost,
110                 MetricsLogger metricsLogger, UiEventLogger uiEventLogger, QSLogger qsLogger,
111                 DumpManager dumpManager) {
112             super(view, host, qsCustomizerController, true, mediaHost, metricsLogger, uiEventLogger,
113                     qsLogger, dumpManager);
114         }
115 
116         @Override
createTileRevealController()117         protected QSTileRevealController createTileRevealController() {
118             return mQSTileRevealController;
119         }
120     }
121 
122     @Before
setup()123     public void setup() throws Exception {
124         MockitoAnnotations.initMocks(this);
125 
126         when(mQSPanel.isAttachedToWindow()).thenReturn(true);
127         when(mQSPanel.getDumpableTag()).thenReturn("QSPanel");
128         when(mQSPanel.openPanelEvent()).thenReturn(QSEvent.QS_PANEL_EXPANDED);
129         when(mQSPanel.closePanelEvent()).thenReturn(QSEvent.QS_PANEL_COLLAPSED);
130         when(mQSPanel.getOrCreateTileLayout()).thenReturn(mPagedTileLayout);
131         when(mQSPanel.getTileLayout()).thenReturn(mPagedTileLayout);
132         when(mQSTile.getTileSpec()).thenReturn("dnd");
133         when(mQSHost.getTiles()).thenReturn(Collections.singleton(mQSTile));
134         when(mQSHost.createTileView(any(), eq(mQSTile), anyBoolean())).thenReturn(mQSTileView);
135         when(mQSTileRevealControllerFactory.create(any(), any()))
136                 .thenReturn(mQSTileRevealController);
137         when(mMediaHost.getDisappearParameters()).thenReturn(new DisappearParameters());
138         when(mQSPanel.getResources()).thenReturn(mResources);
139         when(mResources.getConfiguration()).thenReturn(mConfiguration);
140         doAnswer(invocation -> {
141             when(mQSPanel.isListening()).thenReturn(invocation.getArgument(0));
142             return null;
143         }).when(mQSPanel).setListening(anyBoolean());
144 
145         mController = new TestableQSPanelControllerBase(mQSPanel, mQSHost,
146                 mQSCustomizerController, mMediaHost,
147                 mMetricsLogger, mUiEventLogger, mQSLogger, mDumpManager);
148 
149         mController.init();
150         reset(mQSTileRevealController);
151     }
152 
153     @Test
testSetRevealExpansion_preAttach()154     public void testSetRevealExpansion_preAttach() {
155         mController.onViewDetached();
156 
157         QSPanelControllerBase<QSPanel> controller = new TestableQSPanelControllerBase(mQSPanel,
158                 mQSHost, mQSCustomizerController, mMediaHost,
159                 mMetricsLogger, mUiEventLogger, mQSLogger, mDumpManager) {
160             @Override
161             protected QSTileRevealController createTileRevealController() {
162                 return mQSTileRevealController;
163             }
164         };
165 
166         // Nothing happens until attached
167         controller.setRevealExpansion(0);
168         verify(mQSTileRevealController, never()).setExpansion(anyFloat());
169         controller.setRevealExpansion(0.5f);
170         verify(mQSTileRevealController, never()).setExpansion(anyFloat());
171         controller.setRevealExpansion(1);
172         verify(mQSTileRevealController, never()).setExpansion(anyFloat());
173 
174         controller.init();
175         verify(mQSTileRevealController).setExpansion(1);
176     }
177 
178     @Test
testSetRevealExpansion_postAttach()179     public void testSetRevealExpansion_postAttach() {
180         mController.setRevealExpansion(0);
181         verify(mQSTileRevealController).setExpansion(0);
182         mController.setRevealExpansion(0.5f);
183         verify(mQSTileRevealController).setExpansion(0.5f);
184         mController.setRevealExpansion(1);
185         verify(mQSTileRevealController).setExpansion(1);
186     }
187 
188 
189     @Test
testSetExpanded_Metrics()190     public void testSetExpanded_Metrics() {
191         when(mQSPanel.isExpanded()).thenReturn(false);
192         mController.setExpanded(true);
193         verify(mMetricsLogger).visibility(eq(MetricsEvent.QS_PANEL), eq(true));
194         verify(mQSLogger).logPanelExpanded(true, mQSPanel.getDumpableTag());
195         assertEquals(1, mUiEventLogger.numLogs());
196         assertEquals(QSEvent.QS_PANEL_EXPANDED.getId(), mUiEventLogger.eventId(0));
197         mUiEventLogger.getLogs().clear();
198 
199         when(mQSPanel.isExpanded()).thenReturn(true);
200         mController.setExpanded(false);
201         verify(mMetricsLogger).visibility(eq(MetricsEvent.QS_PANEL), eq(false));
202         verify(mQSLogger).logPanelExpanded(false, mQSPanel.getDumpableTag());
203         assertEquals(1, mUiEventLogger.numLogs());
204         assertEquals(QSEvent.QS_PANEL_COLLAPSED.getId(), mUiEventLogger.eventId(0));
205         mUiEventLogger.getLogs().clear();
206 
207     }
208 
209     @Test
testDump()210     public void testDump() {
211         String mockTileViewString = "Mock Tile View";
212         String mockTileString = "Mock Tile";
213         doAnswer(invocation -> {
214             PrintWriter pw = invocation.getArgument(0);
215             pw.println(mockTileString);
216             return null;
217         }).when(mQSTile).dump(any(PrintWriter.class), any(String[].class));
218         when(mQSTileView.toString()).thenReturn(mockTileViewString);
219 
220         StringWriter w = new StringWriter();
221         PrintWriter pw = new PrintWriter(w);
222         mController.dump(pw, new String[]{});
223         String expected = "TestableQSPanelControllerBase:\n"
224                 + "  Tile records:\n"
225                 + "    " + mockTileString + "\n"
226                 + "    " + mockTileViewString + "\n"
227                 + "  media bounds: null\n"
228                 + "  horizontal layout: false\n"
229                 + "  last orientation: 0\n"
230                 + "  mShouldUseSplitNotificationShade: false\n";
231         assertEquals(expected, w.getBuffer().toString());
232     }
233 
234     @Test
setListening()235     public void setListening() {
236         mController.setListening(true);
237         verify(mQSLogger).logAllTilesChangeListening(true, "QSPanel", "dnd");
238         verify(mPagedTileLayout).setListening(true, mUiEventLogger);
239 
240         mController.setListening(false);
241         verify(mQSLogger).logAllTilesChangeListening(false, "QSPanel", "dnd");
242         verify(mPagedTileLayout).setListening(false, mUiEventLogger);
243     }
244 
245 
246     @Test
testShouldUzeHorizontalLayout_falseForSplitShade()247     public void testShouldUzeHorizontalLayout_falseForSplitShade() {
248         mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE;
249         when(mMediaHost.getVisible()).thenReturn(true);
250 
251         when(mResources.getBoolean(R.bool.config_use_split_notification_shade)).thenReturn(false);
252         when(mQSPanel.getDumpableTag()).thenReturn("QSPanelLandscape");
253         mController = new TestableQSPanelControllerBase(mQSPanel, mQSHost,
254                 mQSCustomizerController, mMediaHost,
255                 mMetricsLogger, mUiEventLogger, mQSLogger, mDumpManager);
256         mController.init();
257 
258         assertThat(mController.shouldUseHorizontalLayout()).isTrue();
259 
260         when(mResources.getBoolean(R.bool.config_use_split_notification_shade)).thenReturn(true);
261         when(mQSPanel.getDumpableTag()).thenReturn("QSPanelPortrait");
262         mController = new TestableQSPanelControllerBase(mQSPanel, mQSHost,
263                 mQSCustomizerController, mMediaHost,
264                 mMetricsLogger, mUiEventLogger, mQSLogger, mDumpManager);
265         mController.init();
266 
267         assertThat(mController.shouldUseHorizontalLayout()).isFalse();
268     }
269 
270     @Test
testChangeConfiguration_shouldUseHorizontalLayout()271     public void testChangeConfiguration_shouldUseHorizontalLayout() {
272         when(mMediaHost.getVisible()).thenReturn(true);
273         mController.setUsingHorizontalLayoutChangeListener(mHorizontalLayoutListener);
274 
275         // When device is rotated to landscape
276         mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE;
277         mController.mOnConfigurationChangedListener.onConfigurationChange(mConfiguration);
278 
279         // Then the layout changes
280         assertThat(mController.shouldUseHorizontalLayout()).isTrue();
281         verify(mHorizontalLayoutListener).run();
282 
283         // When it is rotated back to portrait
284         mConfiguration.orientation = Configuration.ORIENTATION_PORTRAIT;
285         mController.mOnConfigurationChangedListener.onConfigurationChange(mConfiguration);
286 
287         // Then the layout changes back
288         assertThat(mController.shouldUseHorizontalLayout()).isFalse();
289         verify(mHorizontalLayoutListener, times(2)).run();
290     }
291 
292     @Test
testRefreshAllTilesDoesntRefreshListeningTiles()293     public void testRefreshAllTilesDoesntRefreshListeningTiles() {
294         when(mQSHost.getTiles()).thenReturn(List.of(mQSTile, mOtherTile));
295         mController.setTiles();
296 
297         when(mQSTile.isListening()).thenReturn(false);
298         when(mOtherTile.isListening()).thenReturn(true);
299 
300         mController.refreshAllTiles();
301         verify(mQSTile).refreshState();
302         verify(mOtherTile, never()).refreshState();
303     }
304 
305     @Test
configurationChange_onlySplitShadeConfigChanges_horizontalLayoutStatusUpdated()306     public void configurationChange_onlySplitShadeConfigChanges_horizontalLayoutStatusUpdated() {
307         // Preconditions for horizontal layout
308         when(mMediaHost.getVisible()).thenReturn(true);
309         when(mResources.getBoolean(R.bool.config_use_split_notification_shade)).thenReturn(false);
310         mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE;
311         mController.setUsingHorizontalLayoutChangeListener(mHorizontalLayoutListener);
312         mController.mOnConfigurationChangedListener.onConfigurationChange(mConfiguration);
313         assertThat(mController.shouldUseHorizontalLayout()).isTrue();
314         reset(mHorizontalLayoutListener);
315 
316         // Only split shade status changes
317         when(mResources.getBoolean(R.bool.config_use_split_notification_shade)).thenReturn(true);
318         mController.mOnConfigurationChangedListener.onConfigurationChange(mConfiguration);
319 
320         // Horizontal layout is updated accordingly.
321         assertThat(mController.shouldUseHorizontalLayout()).isFalse();
322         verify(mHorizontalLayoutListener).run();
323     }
324 
325     @Test
changeTiles_callbackRemovedOnOldOnes()326     public void changeTiles_callbackRemovedOnOldOnes() {
327         // Start with one tile
328         assertThat(mController.mRecords.size()).isEqualTo(1);
329         QSPanelControllerBase.TileRecord record = mController.mRecords.get(0);
330 
331         assertThat(record.tile).isEqualTo(mQSTile);
332 
333         // Change to a different tile
334         when(mQSHost.getTiles()).thenReturn(List.of(mOtherTile));
335         mController.setTiles();
336 
337         verify(mQSTile).removeCallback(record.callback);
338         verify(mOtherTile, never()).removeCallback(any());
339         verify(mOtherTile, never()).removeCallbacks();
340     }
341 
342     @Test
onViewDetached_removesJustTheAssociatedCallback()343     public void onViewDetached_removesJustTheAssociatedCallback() {
344         QSPanelControllerBase.TileRecord record = mController.mRecords.get(0);
345 
346         mController.onViewDetached();
347         verify(mQSTile).removeCallback(record.callback);
348         verify(mQSTile, never()).removeCallbacks();
349     }
350 }
351