Loading packages/SystemUI/res/values/config.xml +1 −1 Original line number Diff line number Diff line Loading @@ -107,7 +107,7 @@ <!-- Tiles native to System UI. Order should match "quick_settings_tiles_default" --> <string name="quick_settings_tiles_stock" translatable="false"> wifi,cell,battery,dnd,flashlight,rotation,bt,airplane,location,hotspot,inversion,saver,dark,work,cast,night,screenrecord,reverse,reduce_brightness,cameratoggle,mictoggle wifi,cell,battery,dnd,flashlight,rotation,bt,airplane,location,hotspot,inversion,saver,dark,work,cast,night,screenrecord,reverse,reduce_brightness,cameratoggle,mictoggle,controls </string> <!-- The tiles to display in QuickSettings --> Loading packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialog.kt +2 −1 Original line number Diff line number Diff line Loading @@ -28,11 +28,12 @@ import android.view.WindowManager import com.android.systemui.Interpolators import com.android.systemui.R import com.android.systemui.broadcast.BroadcastDispatcher import javax.inject.Inject /** * Show the controls space inside a dialog, as from the lock screen. */ class ControlsDialog( class ControlsDialog @Inject constructor( thisContext: Context, val broadcastDispatcher: BroadcastDispatcher ) : Dialog(thisContext, R.style.Theme_SystemUI_Dialog_Control_LockScreen) { Loading packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java +7 −1 Original line number Diff line number Diff line Loading @@ -37,6 +37,7 @@ import com.android.systemui.qs.tiles.CastTile; import com.android.systemui.qs.tiles.CellularTile; import com.android.systemui.qs.tiles.ColorInversionTile; import com.android.systemui.qs.tiles.DataSaverTile; import com.android.systemui.qs.tiles.DeviceControlsTile; import com.android.systemui.qs.tiles.DndTile; import com.android.systemui.qs.tiles.FlashlightTile; import com.android.systemui.qs.tiles.HotspotTile; Loading Loading @@ -89,6 +90,7 @@ public class QSFactoryImpl implements QSFactory { private final Provider<ReduceBrightColorsTile> mReduceBrightColorsTileProvider; private final Provider<CameraToggleTile> mCameraToggleTileProvider; private final Provider<MicrophoneToggleTile> mMicrophoneToggleTileProvider; private final Provider<DeviceControlsTile> mDeviceControlsTileProvider; private final Lazy<QSHost> mQsHostLazy; private final Provider<CustomTile.Builder> mCustomTileBuilderProvider; Loading Loading @@ -123,7 +125,8 @@ public class QSFactoryImpl implements QSFactory { Provider<ScreenRecordTile> screenRecordTileProvider, Provider<ReduceBrightColorsTile> reduceBrightColorsTileProvider, Provider<CameraToggleTile> cameraToggleTileProvider, Provider<MicrophoneToggleTile> microphoneToggleTileProvider) { Provider<MicrophoneToggleTile> microphoneToggleTileProvider, Provider<DeviceControlsTile> deviceControlsTileProvider) { mQsHostLazy = qsHostLazy; mCustomTileBuilderProvider = customTileBuilderProvider; Loading Loading @@ -153,6 +156,7 @@ public class QSFactoryImpl implements QSFactory { mReduceBrightColorsTileProvider = reduceBrightColorsTileProvider; mCameraToggleTileProvider = cameraToggleTileProvider; mMicrophoneToggleTileProvider = microphoneToggleTileProvider; mDeviceControlsTileProvider = deviceControlsTileProvider; } public QSTile createTile(String tileSpec) { Loading Loading @@ -212,6 +216,8 @@ public class QSFactoryImpl implements QSFactory { return mCameraToggleTileProvider.get(); case "mictoggle": return mMicrophoneToggleTileProvider.get(); case "controls": return mDeviceControlsTileProvider.get(); } // Custom tiles Loading packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt 0 → 100644 +157 −0 Original line number Diff line number Diff line /* * Copyright (C) 2021 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.tiles import android.content.Intent import android.os.Handler import android.os.Looper import android.provider.Settings import android.service.quicksettings.Tile import com.android.internal.logging.MetricsLogger import com.android.systemui.R import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.dagger.ControlsComponent import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.ui.ControlsDialog import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.qs.QSTile import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.qs.QSHost import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.tileimpl.QSTileImpl import com.android.systemui.statusbar.FeatureFlags import com.android.systemui.util.settings.GlobalSettings import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import javax.inject.Provider class DeviceControlsTile @Inject constructor( host: QSHost, @Background backgroundLooper: Looper, @Main mainHandler: Handler, metricsLogger: MetricsLogger, statusBarStateController: StatusBarStateController, activityStarter: ActivityStarter, qsLogger: QSLogger, private val controlsComponent: ControlsComponent, private val featureFlags: FeatureFlags, private val dialogProvider: Provider<ControlsDialog>, globalSettings: GlobalSettings ) : QSTileImpl<QSTile.State>( host, backgroundLooper, mainHandler, metricsLogger, statusBarStateController, activityStarter, qsLogger ) { companion object { const val SETTINGS_FLAG = "controls_lockscreen" } private val controlsLockscreen = globalSettings.getInt(SETTINGS_FLAG, 0) != 0 private var hasControlsApps = AtomicBoolean(false) private val intent = Intent(Settings.ACTION_DEVICE_CONTROLS_SETTINGS) private var controlsDialog: ControlsDialog? = null private val icon = ResourceIcon.get(R.drawable.ic_device_light) private val listingCallback = object : ControlsListingController.ControlsListingCallback { override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) { if (hasControlsApps.compareAndSet(serviceInfos.isEmpty(), serviceInfos.isNotEmpty())) { refreshState() } } } init { controlsComponent.getControlsListingController().ifPresent { it.observe(this, listingCallback) } } override fun isAvailable(): Boolean { return featureFlags.isKeyguardLayoutEnabled && controlsLockscreen && controlsComponent.getControlsUiController().isPresent } override fun newTileState(): QSTile.State { return QSTile.State().also { it.state = Tile.STATE_UNAVAILABLE // Start unavailable matching `hasControlsApps` } } override fun handleDestroy() { dismissDialog() super.handleDestroy() } private fun createDialog() { if (controlsDialog?.isShowing != true) { controlsDialog = dialogProvider.get() } } private fun dismissDialog() { controlsDialog?.dismiss()?.also { controlsDialog = null } } override fun handleClick() { if (state.state != Tile.STATE_UNAVAILABLE) { mUiHandler.post { createDialog() controlsDialog?.show(controlsComponent.getControlsUiController().get()) } } } override fun handleUpdateState(state: QSTile.State, arg: Any?) { state.label = tileLabel state.secondaryLabel = "" state.stateDescription = "" state.contentDescription = state.label state.icon = icon if (hasControlsApps.get()) { state.state = Tile.STATE_ACTIVE if (controlsDialog == null) { mUiHandler.post(this::createDialog) } } else { state.state = Tile.STATE_UNAVAILABLE dismissDialog() } } override fun getMetricsCategory(): Int { return 0 } override fun getLongClickIntent(): Intent { return intent } override fun getTileLabel(): CharSequence { return mContext.getText(R.string.quick_controls_title) } } No newline at end of file packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DeviceControlsTileTest.kt 0 → 100644 +267 −0 Original line number Diff line number Diff line /* * Copyright (C) 2021 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.tiles import android.os.Handler import android.provider.Settings import android.service.quicksettings.Tile import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.lifecycle.LifecycleOwner import androidx.test.filters.SmallTest import com.android.internal.logging.MetricsLogger import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.dagger.ControlsComponent import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.ui.ControlsDialog import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.qs.QSHost import com.android.systemui.qs.logging.QSLogger import com.android.systemui.statusbar.FeatureFlags import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.capture import com.android.systemui.util.settings.FakeSettings import com.android.systemui.util.settings.GlobalSettings import com.google.common.truth.Truth.assertThat 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.`when` import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper(setAsMainLooper = true) class DeviceControlsTileTest : SysuiTestCase() { @Mock private lateinit var qsHost: QSHost @Mock private lateinit var metricsLogger: MetricsLogger @Mock private lateinit var statusBarStateController: StatusBarStateController @Mock private lateinit var activityStarter: ActivityStarter @Mock private lateinit var qsLogger: QSLogger private lateinit var controlsComponent: ControlsComponent @Mock private lateinit var controlsUiController: ControlsUiController @Mock private lateinit var controlsListingController: ControlsListingController @Mock private lateinit var controlsController: ControlsController @Mock private lateinit var featureFlags: FeatureFlags @Mock private lateinit var controlsDialog: ControlsDialog private lateinit var globalSettings: GlobalSettings @Mock private lateinit var serviceInfo: ControlsServiceInfo @Mock private lateinit var uiEventLogger: UiEventLogger @Captor private lateinit var listingCallbackCaptor: ArgumentCaptor<ControlsListingController.ControlsListingCallback> private lateinit var testableLooper: TestableLooper private lateinit var tile: DeviceControlsTile @Before fun setUp() { MockitoAnnotations.initMocks(this) testableLooper = TestableLooper.get(this) `when`(qsHost.context).thenReturn(mContext) `when`(qsHost.uiEventLogger).thenReturn(uiEventLogger) controlsComponent = ControlsComponent( true, { controlsController }, { controlsUiController }, { controlsListingController } ) globalSettings = FakeSettings() globalSettings.putInt(DeviceControlsTile.SETTINGS_FLAG, 1) `when`(featureFlags.isKeyguardLayoutEnabled).thenReturn(true) tile = createTile() } @Test fun testAvailable() { assertThat(tile.isAvailable).isTrue() } @Test fun testNotAvailableFeature() { `when`(featureFlags.isKeyguardLayoutEnabled).thenReturn(false) assertThat(tile.isAvailable).isFalse() } @Test fun testNotAvailableControls() { controlsComponent = ControlsComponent( false, { controlsController }, { controlsUiController }, { controlsListingController } ) tile = createTile() assertThat(tile.isAvailable).isFalse() } @Test fun testNotAvailableFlag() { globalSettings.putInt(DeviceControlsTile.SETTINGS_FLAG, 0) tile = createTile() assertThat(tile.isAvailable).isFalse() } @Test fun testObservingCallback() { verify(controlsListingController).observe( any(LifecycleOwner::class.java), any(ControlsListingController.ControlsListingCallback::class.java) ) } @Test fun testLongClickIntent() { assertThat(tile.longClickIntent.action).isEqualTo(Settings.ACTION_DEVICE_CONTROLS_SETTINGS) } @Test fun testUnavailableByDefault() { assertThat(tile.state.state).isEqualTo(Tile.STATE_UNAVAILABLE) } @Test fun testStateUnavailableIfNoListings() { verify(controlsListingController).observe( any(LifecycleOwner::class.java), capture(listingCallbackCaptor) ) listingCallbackCaptor.value.onServicesUpdated(emptyList()) testableLooper.processAllMessages() assertThat(tile.state.state).isEqualTo(Tile.STATE_UNAVAILABLE) } @Test fun testStateAvailableIfListings() { verify(controlsListingController).observe( any(LifecycleOwner::class.java), capture(listingCallbackCaptor) ) listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) testableLooper.processAllMessages() assertThat(tile.state.state).isEqualTo(Tile.STATE_ACTIVE) } @Test fun testMoveBetweenStates() { verify(controlsListingController).observe( any(LifecycleOwner::class.java), capture(listingCallbackCaptor) ) listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) testableLooper.processAllMessages() listingCallbackCaptor.value.onServicesUpdated(emptyList()) testableLooper.processAllMessages() assertThat(tile.state.state).isEqualTo(Tile.STATE_UNAVAILABLE) } @Test fun testNoDialogWhenUnavailable() { tile.click() testableLooper.processAllMessages() verify(controlsDialog, never()).show(any(ControlsUiController::class.java)) } @Test fun testDialogShowWhenAvailable() { verify(controlsListingController).observe( any(LifecycleOwner::class.java), capture(listingCallbackCaptor) ) listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) testableLooper.processAllMessages() tile.click() testableLooper.processAllMessages() verify(controlsDialog).show(controlsUiController) } @Test fun testDialogDismissedOnDestroy() { verify(controlsListingController).observe( any(LifecycleOwner::class.java), capture(listingCallbackCaptor) ) listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) testableLooper.processAllMessages() tile.click() testableLooper.processAllMessages() tile.destroy() testableLooper.processAllMessages() verify(controlsDialog).dismiss() } private fun createTile(): DeviceControlsTile { return DeviceControlsTile( qsHost, testableLooper.looper, Handler(testableLooper.looper), metricsLogger, statusBarStateController, activityStarter, qsLogger, controlsComponent, featureFlags, { controlsDialog }, globalSettings ) } } No newline at end of file Loading
packages/SystemUI/res/values/config.xml +1 −1 Original line number Diff line number Diff line Loading @@ -107,7 +107,7 @@ <!-- Tiles native to System UI. Order should match "quick_settings_tiles_default" --> <string name="quick_settings_tiles_stock" translatable="false"> wifi,cell,battery,dnd,flashlight,rotation,bt,airplane,location,hotspot,inversion,saver,dark,work,cast,night,screenrecord,reverse,reduce_brightness,cameratoggle,mictoggle wifi,cell,battery,dnd,flashlight,rotation,bt,airplane,location,hotspot,inversion,saver,dark,work,cast,night,screenrecord,reverse,reduce_brightness,cameratoggle,mictoggle,controls </string> <!-- The tiles to display in QuickSettings --> Loading
packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialog.kt +2 −1 Original line number Diff line number Diff line Loading @@ -28,11 +28,12 @@ import android.view.WindowManager import com.android.systemui.Interpolators import com.android.systemui.R import com.android.systemui.broadcast.BroadcastDispatcher import javax.inject.Inject /** * Show the controls space inside a dialog, as from the lock screen. */ class ControlsDialog( class ControlsDialog @Inject constructor( thisContext: Context, val broadcastDispatcher: BroadcastDispatcher ) : Dialog(thisContext, R.style.Theme_SystemUI_Dialog_Control_LockScreen) { Loading
packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java +7 −1 Original line number Diff line number Diff line Loading @@ -37,6 +37,7 @@ import com.android.systemui.qs.tiles.CastTile; import com.android.systemui.qs.tiles.CellularTile; import com.android.systemui.qs.tiles.ColorInversionTile; import com.android.systemui.qs.tiles.DataSaverTile; import com.android.systemui.qs.tiles.DeviceControlsTile; import com.android.systemui.qs.tiles.DndTile; import com.android.systemui.qs.tiles.FlashlightTile; import com.android.systemui.qs.tiles.HotspotTile; Loading Loading @@ -89,6 +90,7 @@ public class QSFactoryImpl implements QSFactory { private final Provider<ReduceBrightColorsTile> mReduceBrightColorsTileProvider; private final Provider<CameraToggleTile> mCameraToggleTileProvider; private final Provider<MicrophoneToggleTile> mMicrophoneToggleTileProvider; private final Provider<DeviceControlsTile> mDeviceControlsTileProvider; private final Lazy<QSHost> mQsHostLazy; private final Provider<CustomTile.Builder> mCustomTileBuilderProvider; Loading Loading @@ -123,7 +125,8 @@ public class QSFactoryImpl implements QSFactory { Provider<ScreenRecordTile> screenRecordTileProvider, Provider<ReduceBrightColorsTile> reduceBrightColorsTileProvider, Provider<CameraToggleTile> cameraToggleTileProvider, Provider<MicrophoneToggleTile> microphoneToggleTileProvider) { Provider<MicrophoneToggleTile> microphoneToggleTileProvider, Provider<DeviceControlsTile> deviceControlsTileProvider) { mQsHostLazy = qsHostLazy; mCustomTileBuilderProvider = customTileBuilderProvider; Loading Loading @@ -153,6 +156,7 @@ public class QSFactoryImpl implements QSFactory { mReduceBrightColorsTileProvider = reduceBrightColorsTileProvider; mCameraToggleTileProvider = cameraToggleTileProvider; mMicrophoneToggleTileProvider = microphoneToggleTileProvider; mDeviceControlsTileProvider = deviceControlsTileProvider; } public QSTile createTile(String tileSpec) { Loading Loading @@ -212,6 +216,8 @@ public class QSFactoryImpl implements QSFactory { return mCameraToggleTileProvider.get(); case "mictoggle": return mMicrophoneToggleTileProvider.get(); case "controls": return mDeviceControlsTileProvider.get(); } // Custom tiles Loading
packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt 0 → 100644 +157 −0 Original line number Diff line number Diff line /* * Copyright (C) 2021 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.tiles import android.content.Intent import android.os.Handler import android.os.Looper import android.provider.Settings import android.service.quicksettings.Tile import com.android.internal.logging.MetricsLogger import com.android.systemui.R import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.dagger.ControlsComponent import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.ui.ControlsDialog import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.qs.QSTile import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.qs.QSHost import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.tileimpl.QSTileImpl import com.android.systemui.statusbar.FeatureFlags import com.android.systemui.util.settings.GlobalSettings import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import javax.inject.Provider class DeviceControlsTile @Inject constructor( host: QSHost, @Background backgroundLooper: Looper, @Main mainHandler: Handler, metricsLogger: MetricsLogger, statusBarStateController: StatusBarStateController, activityStarter: ActivityStarter, qsLogger: QSLogger, private val controlsComponent: ControlsComponent, private val featureFlags: FeatureFlags, private val dialogProvider: Provider<ControlsDialog>, globalSettings: GlobalSettings ) : QSTileImpl<QSTile.State>( host, backgroundLooper, mainHandler, metricsLogger, statusBarStateController, activityStarter, qsLogger ) { companion object { const val SETTINGS_FLAG = "controls_lockscreen" } private val controlsLockscreen = globalSettings.getInt(SETTINGS_FLAG, 0) != 0 private var hasControlsApps = AtomicBoolean(false) private val intent = Intent(Settings.ACTION_DEVICE_CONTROLS_SETTINGS) private var controlsDialog: ControlsDialog? = null private val icon = ResourceIcon.get(R.drawable.ic_device_light) private val listingCallback = object : ControlsListingController.ControlsListingCallback { override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) { if (hasControlsApps.compareAndSet(serviceInfos.isEmpty(), serviceInfos.isNotEmpty())) { refreshState() } } } init { controlsComponent.getControlsListingController().ifPresent { it.observe(this, listingCallback) } } override fun isAvailable(): Boolean { return featureFlags.isKeyguardLayoutEnabled && controlsLockscreen && controlsComponent.getControlsUiController().isPresent } override fun newTileState(): QSTile.State { return QSTile.State().also { it.state = Tile.STATE_UNAVAILABLE // Start unavailable matching `hasControlsApps` } } override fun handleDestroy() { dismissDialog() super.handleDestroy() } private fun createDialog() { if (controlsDialog?.isShowing != true) { controlsDialog = dialogProvider.get() } } private fun dismissDialog() { controlsDialog?.dismiss()?.also { controlsDialog = null } } override fun handleClick() { if (state.state != Tile.STATE_UNAVAILABLE) { mUiHandler.post { createDialog() controlsDialog?.show(controlsComponent.getControlsUiController().get()) } } } override fun handleUpdateState(state: QSTile.State, arg: Any?) { state.label = tileLabel state.secondaryLabel = "" state.stateDescription = "" state.contentDescription = state.label state.icon = icon if (hasControlsApps.get()) { state.state = Tile.STATE_ACTIVE if (controlsDialog == null) { mUiHandler.post(this::createDialog) } } else { state.state = Tile.STATE_UNAVAILABLE dismissDialog() } } override fun getMetricsCategory(): Int { return 0 } override fun getLongClickIntent(): Intent { return intent } override fun getTileLabel(): CharSequence { return mContext.getText(R.string.quick_controls_title) } } No newline at end of file
packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DeviceControlsTileTest.kt 0 → 100644 +267 −0 Original line number Diff line number Diff line /* * Copyright (C) 2021 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.tiles import android.os.Handler import android.provider.Settings import android.service.quicksettings.Tile import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.lifecycle.LifecycleOwner import androidx.test.filters.SmallTest import com.android.internal.logging.MetricsLogger import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.dagger.ControlsComponent import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.ui.ControlsDialog import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.qs.QSHost import com.android.systemui.qs.logging.QSLogger import com.android.systemui.statusbar.FeatureFlags import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.capture import com.android.systemui.util.settings.FakeSettings import com.android.systemui.util.settings.GlobalSettings import com.google.common.truth.Truth.assertThat 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.`when` import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper(setAsMainLooper = true) class DeviceControlsTileTest : SysuiTestCase() { @Mock private lateinit var qsHost: QSHost @Mock private lateinit var metricsLogger: MetricsLogger @Mock private lateinit var statusBarStateController: StatusBarStateController @Mock private lateinit var activityStarter: ActivityStarter @Mock private lateinit var qsLogger: QSLogger private lateinit var controlsComponent: ControlsComponent @Mock private lateinit var controlsUiController: ControlsUiController @Mock private lateinit var controlsListingController: ControlsListingController @Mock private lateinit var controlsController: ControlsController @Mock private lateinit var featureFlags: FeatureFlags @Mock private lateinit var controlsDialog: ControlsDialog private lateinit var globalSettings: GlobalSettings @Mock private lateinit var serviceInfo: ControlsServiceInfo @Mock private lateinit var uiEventLogger: UiEventLogger @Captor private lateinit var listingCallbackCaptor: ArgumentCaptor<ControlsListingController.ControlsListingCallback> private lateinit var testableLooper: TestableLooper private lateinit var tile: DeviceControlsTile @Before fun setUp() { MockitoAnnotations.initMocks(this) testableLooper = TestableLooper.get(this) `when`(qsHost.context).thenReturn(mContext) `when`(qsHost.uiEventLogger).thenReturn(uiEventLogger) controlsComponent = ControlsComponent( true, { controlsController }, { controlsUiController }, { controlsListingController } ) globalSettings = FakeSettings() globalSettings.putInt(DeviceControlsTile.SETTINGS_FLAG, 1) `when`(featureFlags.isKeyguardLayoutEnabled).thenReturn(true) tile = createTile() } @Test fun testAvailable() { assertThat(tile.isAvailable).isTrue() } @Test fun testNotAvailableFeature() { `when`(featureFlags.isKeyguardLayoutEnabled).thenReturn(false) assertThat(tile.isAvailable).isFalse() } @Test fun testNotAvailableControls() { controlsComponent = ControlsComponent( false, { controlsController }, { controlsUiController }, { controlsListingController } ) tile = createTile() assertThat(tile.isAvailable).isFalse() } @Test fun testNotAvailableFlag() { globalSettings.putInt(DeviceControlsTile.SETTINGS_FLAG, 0) tile = createTile() assertThat(tile.isAvailable).isFalse() } @Test fun testObservingCallback() { verify(controlsListingController).observe( any(LifecycleOwner::class.java), any(ControlsListingController.ControlsListingCallback::class.java) ) } @Test fun testLongClickIntent() { assertThat(tile.longClickIntent.action).isEqualTo(Settings.ACTION_DEVICE_CONTROLS_SETTINGS) } @Test fun testUnavailableByDefault() { assertThat(tile.state.state).isEqualTo(Tile.STATE_UNAVAILABLE) } @Test fun testStateUnavailableIfNoListings() { verify(controlsListingController).observe( any(LifecycleOwner::class.java), capture(listingCallbackCaptor) ) listingCallbackCaptor.value.onServicesUpdated(emptyList()) testableLooper.processAllMessages() assertThat(tile.state.state).isEqualTo(Tile.STATE_UNAVAILABLE) } @Test fun testStateAvailableIfListings() { verify(controlsListingController).observe( any(LifecycleOwner::class.java), capture(listingCallbackCaptor) ) listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) testableLooper.processAllMessages() assertThat(tile.state.state).isEqualTo(Tile.STATE_ACTIVE) } @Test fun testMoveBetweenStates() { verify(controlsListingController).observe( any(LifecycleOwner::class.java), capture(listingCallbackCaptor) ) listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) testableLooper.processAllMessages() listingCallbackCaptor.value.onServicesUpdated(emptyList()) testableLooper.processAllMessages() assertThat(tile.state.state).isEqualTo(Tile.STATE_UNAVAILABLE) } @Test fun testNoDialogWhenUnavailable() { tile.click() testableLooper.processAllMessages() verify(controlsDialog, never()).show(any(ControlsUiController::class.java)) } @Test fun testDialogShowWhenAvailable() { verify(controlsListingController).observe( any(LifecycleOwner::class.java), capture(listingCallbackCaptor) ) listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) testableLooper.processAllMessages() tile.click() testableLooper.processAllMessages() verify(controlsDialog).show(controlsUiController) } @Test fun testDialogDismissedOnDestroy() { verify(controlsListingController).observe( any(LifecycleOwner::class.java), capture(listingCallbackCaptor) ) listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) testableLooper.processAllMessages() tile.click() testableLooper.processAllMessages() tile.destroy() testableLooper.processAllMessages() verify(controlsDialog).dismiss() } private fun createTile(): DeviceControlsTile { return DeviceControlsTile( qsHost, testableLooper.looper, Handler(testableLooper.looper), metricsLogger, statusBarStateController, activityStarter, qsLogger, controlsComponent, featureFlags, { controlsDialog }, globalSettings ) } } No newline at end of file