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

Commit ed2069c4 authored by Behnam Heydarshahi's avatar Behnam Heydarshahi Committed by Android (Google) Code Review
Browse files

Merge "Migrate UiModeNightTile" into main

parents 961d2b4e 53c0bc00
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