/* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.qs import android.content.res.Configuration import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import android.testing.TestableLooper.RunWithLooper import android.view.ContextThemeWrapper import android.view.ViewTreeObserver import androidx.test.filters.SmallTest import com.android.internal.logging.MetricsLogger import com.android.internal.logging.testing.UiEventLoggerFake import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.flags.andSceneContainer import com.android.systemui.haptics.qs.QSLongPressEffect import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor import com.android.systemui.media.controls.ui.view.MediaHost import com.android.systemui.media.controls.ui.view.MediaHostState import com.android.systemui.plugins.qs.QSTile import com.android.systemui.qs.customize.QSCustomizerController import com.android.systemui.qs.logging.QSLogger import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController import com.android.systemui.util.leak.RotationUtils import javax.inject.Provider import kotlinx.coroutines.flow.MutableStateFlow import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.any import org.mockito.Mockito.reset import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters @SmallTest @RunWith(ParameterizedAndroidJunit4::class) @RunWithLooper(setAsMainLooper = true) class QuickQSPanelControllerTest(flags: FlagsParameterization) : SysuiTestCase() { @Mock private lateinit var quickQSPanel: QuickQSPanel @Mock private lateinit var qsHost: QSHost @Mock private lateinit var qsCustomizerController: QSCustomizerController @Mock private lateinit var mediaHost: MediaHost @Mock private lateinit var metricsLogger: MetricsLogger @Mock private lateinit var qsLogger: QSLogger @Mock private lateinit var tile: QSTile @Mock private lateinit var tileLayout: TileLayout @Captor private lateinit var captor: ArgumentCaptor @Captor private lateinit var configCaptor: ArgumentCaptor @Mock private lateinit var longPressEffectProvider: Provider @Mock private lateinit var mediaCarouselInteractor: MediaCarouselInteractor @Mock private lateinit var configurationController: ConfigurationController @Mock private lateinit var mockViewTreeObserver: ViewTreeObserver private val usingMediaPlayer: Boolean get() = false private val uiEventLogger = UiEventLoggerFake() private val dumpManager = DumpManager() private var usingCollapsedLandscapeMedia = true private lateinit var controller: TestQuickQSPanelController companion object { @JvmStatic @Parameters(name = "{0}") fun getParams(): List { return FlagsParameterization.allCombinationsOf().andSceneContainer() } } init { mSetFlagsRule.setFlagsParameterization(flags) } @Before fun setUp() { MockitoAnnotations.initMocks(this) whenever(quickQSPanel.tileLayout).thenReturn(tileLayout) whenever(quickQSPanel.isAttachedToWindow).thenReturn(true) whenever(quickQSPanel.dumpableTag).thenReturn("") whenever(quickQSPanel.resources).thenReturn(mContext.resources) whenever(quickQSPanel.context) .thenReturn(ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings)) // When SceneContainerFlag is enabled, QSPanelControllerBase.onInit calls // registerForMediaInteractorChanges. This in turn calls JavaAdapterKt.collectFlow, // which requires specific mocks for MediaCarouselInteractor and the View's // ViewTreeObserver to prevent NullPointerExceptions during test setup. if (SceneContainerFlag.isEnabled) { whenever(mediaCarouselInteractor.hasActiveMedia).thenReturn(MutableStateFlow(false)) whenever(quickQSPanel.viewTreeObserver).thenReturn(mockViewTreeObserver) whenever(mockViewTreeObserver.isAlive).thenReturn(true) } controller = TestQuickQSPanelController( quickQSPanel, qsHost, qsCustomizerController, usingMediaPlayer, mediaHost, { usingCollapsedLandscapeMedia }, metricsLogger, uiEventLogger, qsLogger, dumpManager, longPressEffectProvider, mediaCarouselInteractor, configurationController, ) controller.init() } @After fun tearDown() { controller.onViewDetached() controller.destroy() } @Test fun testTileSublistWithFewerTiles_noCrash() { whenever(quickQSPanel.numQuickTiles).thenReturn(3) whenever(qsHost.tiles).thenReturn(listOf(tile, tile)) controller.setTiles() } @Test fun testTileSublistWithTooManyTiles() { val limit = 3 whenever(quickQSPanel.numQuickTiles).thenReturn(limit) whenever(qsHost.tiles).thenReturn(listOf(tile, tile, tile, tile)) controller.setTiles() verify(quickQSPanel, times(limit)).addTile(any()) } @Test @DisableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND) @DisableSceneContainer fun mediaExpansion_afterConfigChange_inLandscape_collapsedInLandscapeTrue_updatesToCollapsed_old() { verify(quickQSPanel).addOnConfigurationChangedListener(captor.capture()) // verify that media starts in the expanded state by default verify(mediaHost).expansion = MediaHostState.EXPANDED // Rotate device, verify media size updated to collapsed usingCollapsedLandscapeMedia = true controller.setRotation(RotationUtils.ROTATION_LANDSCAPE) captor.allValues.forEach { it.onConfigurationChange(Configuration.EMPTY) } verify(mediaHost).expansion = MediaHostState.COLLAPSED } @Test @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND) @DisableSceneContainer fun mediaExpansion_afterConfigChange_inLandscape_collapsedInLandscapeTrue_updatesToCollapsed() { verify(configurationController).addCallback(configCaptor.capture()) // verify that media starts in the expanded state by default verify(mediaHost).expansion = MediaHostState.EXPANDED // Rotate device, verify media size updated to collapsed usingCollapsedLandscapeMedia = true controller.setRotation(RotationUtils.ROTATION_LANDSCAPE) configCaptor.allValues.forEach { it.onConfigChanged(Configuration.EMPTY) } verify(mediaHost).expansion = MediaHostState.COLLAPSED } @Test @DisableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND) @DisableSceneContainer fun mediaExpansion_afterConfigChange_landscape_collapsedInLandscapeFalse_remainsExpanded_old() { verify(quickQSPanel).addOnConfigurationChangedListener(captor.capture()) reset(mediaHost) usingCollapsedLandscapeMedia = false controller.setRotation(RotationUtils.ROTATION_LANDSCAPE) captor.allValues.forEach { it.onConfigurationChange(Configuration.EMPTY) } verify(mediaHost).expansion = MediaHostState.EXPANDED } @Test @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND) @DisableSceneContainer fun mediaExpansion_afterConfigChange_landscape_collapsedInLandscapeFalse_remainsExpanded() { verify(configurationController).addCallback(configCaptor.capture()) reset(mediaHost) usingCollapsedLandscapeMedia = false controller.setRotation(RotationUtils.ROTATION_LANDSCAPE) configCaptor.allValues.forEach { it.onConfigChanged(Configuration.EMPTY) } verify(mediaHost).expansion = MediaHostState.EXPANDED } class TestQuickQSPanelController( view: QuickQSPanel, qsHost: QSHost, qsCustomizerController: QSCustomizerController, usingMediaPlayer: Boolean, mediaHost: MediaHost, usingCollapsedLandscapeMedia: () -> Boolean, metricsLogger: MetricsLogger, uiEventLogger: UiEventLoggerFake, qsLogger: QSLogger, dumpManager: DumpManager, longPressEffectProvider: Provider, mediaCarouselInteractor: MediaCarouselInteractor, configurationController: ConfigurationController, ) : QuickQSPanelController( view, qsHost, qsCustomizerController, usingMediaPlayer, mediaHost, usingCollapsedLandscapeMedia, metricsLogger, uiEventLogger, qsLogger, dumpManager, ResourcesSplitShadeStateController(), longPressEffectProvider, mediaCarouselInteractor, configurationController, ) { private var rotation = RotationUtils.ROTATION_NONE @Override override fun getRotation(): Int = rotation fun setRotation(newRotation: Int) { rotation = newRotation } } }