Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7534f364 authored by Fabian Kozynski's avatar Fabian Kozynski Committed by Android (Google) Code Review
Browse files

Merge "Add a prototype device controls tile" into sc-dev

parents ac23fdd1 d437edd8
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -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 -->
+2 −1
Original line number Diff line number Diff line
@@ -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) {
+7 −1
Original line number Diff line number Diff line
@@ -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
+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
+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