Add a prototype device controls tile
The tile is gated by controls_lockscreen setting, the feature flag and the software feature. Test: atest SystemUITests Fixes: 179920615 Change-Id: I2d5715c7520e2604edd30688a82f56935c796a5c
This commit is contained in:
@@ -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 -->
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -153,6 +156,7 @@ public class QSFactoryImpl implements QSFactory {
|
||||
mReduceBrightColorsTileProvider = reduceBrightColorsTileProvider;
|
||||
mCameraToggleTileProvider = cameraToggleTileProvider;
|
||||
mMicrophoneToggleTileProvider = microphoneToggleTileProvider;
|
||||
mDeviceControlsTileProvider = deviceControlsTileProvider;
|
||||
}
|
||||
|
||||
public QSTile createTile(String tileSpec) {
|
||||
@@ -212,6 +216,8 @@ public class QSFactoryImpl implements QSFactory {
|
||||
return mCameraToggleTileProvider.get();
|
||||
case "mictoggle":
|
||||
return mMicrophoneToggleTileProvider.get();
|
||||
case "controls":
|
||||
return mDeviceControlsTileProvider.get();
|
||||
}
|
||||
|
||||
// Custom tiles
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
* 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
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user