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