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

Commit 53c0bc00 authored by Behnam Heydarshahi's avatar Behnam Heydarshahi
Browse files

Migrate UiModeNightTile

Fixes: 301056110
Flag: LEGACY QS_PIPELINE_NEW_TILES DISABLED
Test: atest SystemUiRoboTests
Test: atest UiModeNightTileDataInteractor UiModeNightTileUserActionInteractor UiModeNightTileMapper

Change-Id: I318fc6983b92b9817c4a8e393d71f3ebb9743f89
parent a868e7c6
Loading
Loading
Loading
Loading
+200 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.impl.uimodenight.domain

import android.app.UiModeManager
import android.content.res.Configuration
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.os.UserHandle
import android.testing.LeakCheck
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
import com.android.systemui.qs.tiles.impl.uimodenight.domain.interactor.UiModeNightTileDataInteractor
import com.android.systemui.qs.tiles.impl.uimodenight.domain.model.UiModeNightTileModel
import com.android.systemui.statusbar.phone.ConfigurationControllerImpl
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.DateFormatUtil
import com.android.systemui.utils.leaks.FakeBatteryController
import com.android.systemui.utils.leaks.FakeLocationController
import com.google.common.truth.Truth.assertThat
import java.time.LocalTime
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.verify

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class UiModeNightTileDataInteractorTest : SysuiTestCase() {
    private val configurationController: ConfigurationController =
        ConfigurationControllerImpl(context)
    private val batteryController = FakeBatteryController(LeakCheck())
    private val locationController = FakeLocationController(LeakCheck())

    private lateinit var underTest: UiModeNightTileDataInteractor

    @Mock private lateinit var uiModeManager: UiModeManager
    @Mock private lateinit var dateFormatUtil: DateFormatUtil

    @Before
    fun setup() {
        uiModeManager = mock<UiModeManager>()
        dateFormatUtil = mock<DateFormatUtil>()

        whenever(uiModeManager.customNightModeStart).thenReturn(LocalTime.MIN)
        whenever(uiModeManager.customNightModeEnd).thenReturn(LocalTime.MAX)

        underTest =
            UiModeNightTileDataInteractor(
                context,
                configurationController,
                uiModeManager,
                batteryController,
                locationController,
                dateFormatUtil
            )
    }

    @Test
    fun collectTileDataReadsUiModeManagerNightMode() = runTest {
        val expectedNightMode = Configuration.UI_MODE_NIGHT_UNDEFINED
        whenever(uiModeManager.nightMode).thenReturn(expectedNightMode)

        val model by
            collectLastValue(
                underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest))
            )
        runCurrent()

        assertThat(model).isNotNull()
        val actualNightMode = model?.uiMode
        assertThat(actualNightMode).isEqualTo(expectedNightMode)
    }

    @Test
    fun collectTileDataReadsUiModeManagerNightModeCustomTypeAndTimes() = runTest {
        collectLastValue(underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest)))

        runCurrent()

        verify(uiModeManager).nightMode
        verify(uiModeManager).nightModeCustomType
        verify(uiModeManager).customNightModeStart
        verify(uiModeManager).customNightModeEnd
    }

    /** Here, available refers to the tile showing up, not the tile being clickable. */
    @Test
    fun isAvailableRegardlessOfPowerSaveModeOn() = runTest {
        batteryController.setPowerSaveMode(true)

        runCurrent()
        val availability by collectLastValue(underTest.availability(TEST_USER))

        assertThat(availability).isTrue()
    }

    @Test
    fun dataMatchesConfigurationController() = runTest {
        setUiMode(UI_MODE_NIGHT_NO)
        val flowValues: List<UiModeNightTileModel> by
            collectValues(underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest)))

        runCurrent()
        setUiMode(UI_MODE_NIGHT_YES)
        runCurrent()
        setUiMode(UI_MODE_NIGHT_NO)
        runCurrent()

        assertThat(flowValues.size).isEqualTo(3)
        assertThat(flowValues.map { it.isNightMode }).containsExactly(false, true, false).inOrder()
    }

    @Test
    fun dataMatchesBatteryController() = runTest {
        batteryController.setPowerSaveMode(false)
        val flowValues: List<UiModeNightTileModel> by
            collectValues(underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest)))

        runCurrent()
        batteryController.setPowerSaveMode(true)
        runCurrent()
        batteryController.setPowerSaveMode(false)
        runCurrent()

        assertThat(flowValues.size).isEqualTo(3)
        assertThat(flowValues.map { it.isPowerSave }).containsExactly(false, true, false).inOrder()
    }

    @Test
    fun dataMatchesLocationController() = runTest {
        locationController.setLocationEnabled(false)
        val flowValues: List<UiModeNightTileModel> by
            collectValues(underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest)))

        runCurrent()
        locationController.setLocationEnabled(true)
        runCurrent()
        locationController.setLocationEnabled(false)
        runCurrent()

        assertThat(flowValues.size).isEqualTo(3)
        assertThat(flowValues.map { it.isLocationEnabled })
            .containsExactly(false, true, false)
            .inOrder()
    }

    @Test
    fun collectTileDataReads24HourFormatFromDateTimeUtil() = runTest {
        collectLastValue(underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest)))
        runCurrent()

        verify(dateFormatUtil).is24HourFormat
    }

    /**
     * Use this method to trigger [ConfigurationController.ConfigurationListener.onUiModeChanged]
     */
    private fun setUiMode(uiMode: Int) {
        val config = context.resources.configuration
        val newConfig = Configuration(config)
        newConfig.uiMode = uiMode

        /** [underTest] will see this config the next time it creates a model */
        context.orCreateTestableResources.overrideConfiguration(newConfig)

        /** Trigger updateUiMode callbacks */
        configurationController.onConfigurationChanged(newConfig)
    }

    private companion object {
        val TEST_USER = UserHandle.of(1)!!
    }
}
+481 −0

File added.

Preview size limit exceeded, changes collapsed.

+125 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.impl.uimodenight.domain

import android.app.UiModeManager
import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
import com.android.systemui.qs.tiles.base.actions.intentInputs
import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
import com.android.systemui.qs.tiles.impl.uimodenight.UiModeNightTileModelHelper.createModel
import com.android.systemui.qs.tiles.impl.uimodenight.domain.interactor.UiModeNightTileUserActionInteractor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify

@SmallTest
@RunWith(AndroidJUnit4::class)
class UiModeNightTileUserActionInteractorTest : SysuiTestCase() {

    private val qsTileIntentUserActionHandler = FakeQSTileIntentUserInputHandler()

    private lateinit var underTest: UiModeNightTileUserActionInteractor

    @Mock private lateinit var uiModeManager: UiModeManager

    @Before
    fun setup() {
        uiModeManager = mock<UiModeManager>()
        underTest =
            UiModeNightTileUserActionInteractor(
                EmptyCoroutineContext,
                uiModeManager,
                qsTileIntentUserActionHandler
            )
    }

    @Test
    fun handleClickToEnable() = runTest {
        val stateBeforeClick = false

        underTest.handleInput(QSTileInputTestKtx.click(createModel(stateBeforeClick)))

        verify(uiModeManager).setNightModeActivated(!stateBeforeClick)
    }

    @Test
    fun handleClickToDisable() = runTest {
        val stateBeforeClick = true

        underTest.handleInput(QSTileInputTestKtx.click(createModel(stateBeforeClick)))

        verify(uiModeManager).setNightModeActivated(!stateBeforeClick)
    }

    @Test
    fun clickToEnableDoesNothingWhenInPowerSaveInNightMode() = runTest {
        val isNightMode = true
        val isPowerSave = true

        underTest.handleInput(QSTileInputTestKtx.click(createModel(isNightMode, isPowerSave)))

        verify(uiModeManager, never()).setNightModeActivated(any())
    }

    @Test
    fun clickToEnableDoesNothingWhenInPowerSaveNotInNightMode() = runTest {
        val isNightMode = false
        val isPowerSave = true

        underTest.handleInput(QSTileInputTestKtx.click(createModel(isNightMode, isPowerSave)))

        verify(uiModeManager, never()).setNightModeActivated(any())
    }

    @Test
    fun handleLongClickNightModeEnabled() = runTest {
        val isNightMode = true

        underTest.handleInput(QSTileInputTestKtx.longClick(createModel(isNightMode)))

        Truth.assertThat(qsTileIntentUserActionHandler.handledInputs).hasSize(1)
        val intentInput = qsTileIntentUserActionHandler.intentInputs.last()
        val actualIntentAction = intentInput.intent.action
        val expectedIntentAction = Settings.ACTION_DARK_THEME_SETTINGS
        Truth.assertThat(actualIntentAction).isEqualTo(expectedIntentAction)
    }

    @Test
    fun handleLongClickNightModeDisabled() = runTest {
        val isNightMode = false

        underTest.handleInput(QSTileInputTestKtx.longClick(createModel(isNightMode)))

        Truth.assertThat(qsTileIntentUserActionHandler.handledInputs).hasSize(1)
        val intentInput = qsTileIntentUserActionHandler.intentInputs.last()
        val actualIntentAction = intentInput.intent.action
        val expectedIntentAction = Settings.ACTION_DARK_THEME_SETTINGS
        Truth.assertThat(actualIntentAction).isEqualTo(expectedIntentAction)
    }
}
+128 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.impl.uimodenight.domain

import android.app.UiModeManager
import android.content.res.Resources
import android.text.TextUtils
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
import com.android.systemui.qs.tiles.impl.uimodenight.domain.model.UiModeNightTileModel
import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
import com.android.systemui.qs.tiles.viewmodel.QSTileState
import com.android.systemui.res.R
import java.time.LocalTime
import java.time.format.DateTimeFormatter
import javax.inject.Inject

/** Maps [UiModeNightTileModel] to [QSTileState]. */
class UiModeNightTileMapper @Inject constructor(@Main private val resources: Resources) :
    QSTileDataToStateMapper<UiModeNightTileModel> {
    companion object {
        val formatter12Hour: DateTimeFormatter = DateTimeFormatter.ofPattern("hh:mm a")
        val formatter24Hour: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm")
    }
    override fun map(config: QSTileConfig, data: UiModeNightTileModel): QSTileState =
        with(data) {
            QSTileState.build(resources, config.uiConfig) {
                var shouldSetSecondaryLabel = false

                if (isPowerSave) {
                    secondaryLabel =
                        resources.getString(
                            R.string.quick_settings_dark_mode_secondary_label_battery_saver
                        )
                } else if (uiMode == UiModeManager.MODE_NIGHT_AUTO && isLocationEnabled) {
                    secondaryLabel =
                        resources.getString(
                            if (isNightMode)
                                R.string.quick_settings_dark_mode_secondary_label_until_sunrise
                            else R.string.quick_settings_dark_mode_secondary_label_on_at_sunset
                        )
                } else if (uiMode == UiModeManager.MODE_NIGHT_CUSTOM) {
                    if (nightModeCustomType == UiModeManager.MODE_NIGHT_CUSTOM_TYPE_SCHEDULE) {
                        val time: LocalTime =
                            if (isNightMode) {
                                customNightModeEnd
                            } else {
                                customNightModeStart
                            }

                        val formatter: DateTimeFormatter =
                            if (is24HourFormat) formatter24Hour else formatter12Hour

                        secondaryLabel =
                            resources.getString(
                                if (isNightMode)
                                    R.string.quick_settings_dark_mode_secondary_label_until
                                else R.string.quick_settings_dark_mode_secondary_label_on_at,
                                formatter.format(time)
                            )
                    } else if (
                        nightModeCustomType == UiModeManager.MODE_NIGHT_CUSTOM_TYPE_BEDTIME
                    ) {
                        secondaryLabel =
                            resources.getString(
                                if (isNightMode)
                                    R.string
                                        .quick_settings_dark_mode_secondary_label_until_bedtime_ends
                                else R.string.quick_settings_dark_mode_secondary_label_on_at_bedtime
                            )
                    } else {
                        secondaryLabel = null // undefined type of nightModeCustomType
                        shouldSetSecondaryLabel = true
                    }
                } else {
                    secondaryLabel = null
                    shouldSetSecondaryLabel = true
                }

                contentDescription =
                    if (TextUtils.isEmpty(secondaryLabel)) label
                    else TextUtils.concat(label, ", ", secondaryLabel)
                if (isPowerSave) {
                    activationState = QSTileState.ActivationState.UNAVAILABLE
                    if (shouldSetSecondaryLabel)
                        secondaryLabel = resources.getStringArray(R.array.tile_states_dark)[0]
                } else {
                    activationState =
                        if (isNightMode) QSTileState.ActivationState.ACTIVE
                        else QSTileState.ActivationState.INACTIVE

                    if (shouldSetSecondaryLabel) {
                        secondaryLabel =
                            if (activationState == QSTileState.ActivationState.INACTIVE)
                                resources.getStringArray(R.array.tile_states_dark)[1]
                            else resources.getStringArray(R.array.tile_states_dark)[2]
                    }
                }

                val iconRes =
                    if (activationState == QSTileState.ActivationState.ACTIVE)
                        R.drawable.qs_light_dark_theme_icon_on
                    else R.drawable.qs_light_dark_theme_icon_off
                val iconResource = Icon.Resource(iconRes, null)
                icon = { iconResource }

                supportedActions =
                    if (activationState == QSTileState.ActivationState.UNAVAILABLE)
                        setOf(QSTileState.UserAction.LONG_CLICK)
                    else setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK)
            }
        }
}
+113 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.impl.uimodenight.domain.interactor

import android.app.UiModeManager
import android.content.Context
import android.content.res.Configuration
import android.os.UserHandle
import com.android.systemui.common.coroutine.ConflatedCallbackFlow
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
import com.android.systemui.qs.tiles.impl.uimodenight.domain.model.UiModeNightTileModel
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.LocationController
import com.android.systemui.util.time.DateFormatUtil
import javax.inject.Inject
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf

/** Observes ui mode night state changes providing the [UiModeNightTileModel]. */
class UiModeNightTileDataInteractor
@Inject
constructor(
    @Application private val context: Context,
    private val configurationController: ConfigurationController,
    private val uiModeManager: UiModeManager,
    private val batteryController: BatteryController,
    private val locationController: LocationController,
    private val dateFormatUtil: DateFormatUtil,
) : QSTileDataInteractor<UiModeNightTileModel> {

    override fun tileData(
        user: UserHandle,
        triggers: Flow<DataUpdateTrigger>
    ): Flow<UiModeNightTileModel> =
        ConflatedCallbackFlow.conflatedCallbackFlow {
            // send initial state
            trySend(createModel())

            val configurationCallback =
                object : ConfigurationController.ConfigurationListener {
                    override fun onUiModeChanged() {
                        trySend(createModel())
                    }
                }
            configurationController.addCallback(configurationCallback)

            val batteryCallback =
                object : BatteryController.BatteryStateChangeCallback {
                    override fun onPowerSaveChanged(isPowerSave: Boolean) {
                        trySend(createModel())
                    }
                }
            batteryController.addCallback(batteryCallback)

            val locationCallback =
                object : LocationController.LocationChangeCallback {
                    override fun onLocationSettingsChanged(locationEnabled: Boolean) {
                        trySend(createModel())
                    }
                }
            locationController.addCallback(locationCallback)

            awaitClose {
                configurationController.removeCallback(configurationCallback)
                batteryController.removeCallback(batteryCallback)
                locationController.removeCallback(locationCallback)
            }
        }

    private fun createModel(): UiModeNightTileModel {
        val uiMode = uiModeManager.nightMode
        val nightMode =
            (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
                Configuration.UI_MODE_NIGHT_YES
        val powerSave = batteryController.isPowerSave
        val locationEnabled = locationController.isLocationEnabled
        val nightModeCustomType = uiModeManager.nightModeCustomType
        val use24HourFormat = dateFormatUtil.is24HourFormat
        val customNightModeEnd = uiModeManager.customNightModeEnd
        val customNightModeStart = uiModeManager.customNightModeStart

        return UiModeNightTileModel(
            uiMode,
            nightMode,
            powerSave,
            locationEnabled,
            nightModeCustomType,
            use24HourFormat,
            customNightModeEnd,
            customNightModeStart
        )
    }

    override fun availability(user: UserHandle): Flow<Boolean> = flowOf(true)
}
Loading