Migrate NightDisplayTile
Fixes: 301056354
Flag: aconfig com.android.systemui.qs_new_tiles DEVELOPMENT
Flag: aconfig com.android.systemui.qs_new_tiles_future DEVELOPMENT
Test: atest NightDisplayTileDataInteractorTest
NightDisplayTileUserActionInteractorTest NightDisplayTileMapperTest
LocationTileDataInteractorTest NightDisplayRepositoryTest
Change-Id: I03b4640e382fceda3266bc98b5f67b3d3689106e
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
index 3d8159e..9c9ee53 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
@@ -24,7 +24,6 @@
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.util.settings.FakeSettings
-import com.google.common.truth.Truth
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
@@ -66,7 +65,7 @@
runCurrent()
- Truth.assertThat(actualValue).isFalse()
+ assertThat(actualValue).isFalse()
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/NightDisplayRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/NightDisplayRepositoryTest.kt
new file mode 100644
index 0000000..ca824cb
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/NightDisplayRepositoryTest.kt
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2024 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.accessibility.data.repository
+
+import android.hardware.display.ColorDisplayManager
+import android.hardware.display.NightDisplayListener
+import android.os.UserHandle
+import android.provider.Settings
+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.dagger.NightDisplayListenerModule
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.user.utils.UserScopedService
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.fakeGlobalSettings
+import com.android.systemui.util.settings.fakeSettings
+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.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+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.ArgumentMatchers
+import org.mockito.Mockito.verify
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NightDisplayRepositoryTest : SysuiTestCase() {
+ private val kosmos = Kosmos()
+ private val testUser = UserHandle.of(1)!!
+ private val testStartTime = LocalTime.MIDNIGHT
+ private val testEndTime = LocalTime.NOON
+ private val colorDisplayManager =
+ mock<ColorDisplayManager> {
+ whenever(nightDisplayAutoMode).thenReturn(ColorDisplayManager.AUTO_MODE_DISABLED)
+ whenever(isNightDisplayActivated).thenReturn(false)
+ whenever(nightDisplayCustomStartTime).thenReturn(testStartTime)
+ whenever(nightDisplayCustomEndTime).thenReturn(testEndTime)
+ }
+ private val locationController = FakeLocationController(LeakCheck())
+ private val nightDisplayListener = mock<NightDisplayListener>()
+ private val listenerBuilder =
+ mock<NightDisplayListenerModule.Builder> {
+ whenever(setUser(ArgumentMatchers.anyInt())).thenReturn(this)
+ whenever(build()).thenReturn(nightDisplayListener)
+ }
+ private val globalSettings = kosmos.fakeGlobalSettings
+ private val secureSettings = kosmos.fakeSettings
+ private val testDispatcher = StandardTestDispatcher()
+ private val scope = TestScope(testDispatcher)
+ private val userScopedColorDisplayManager =
+ mock<UserScopedService<ColorDisplayManager>> {
+ whenever(forUser(eq(testUser))).thenReturn(colorDisplayManager)
+ }
+
+ private val underTest =
+ NightDisplayRepository(
+ testDispatcher,
+ scope.backgroundScope,
+ globalSettings,
+ secureSettings,
+ listenerBuilder,
+ userScopedColorDisplayManager,
+ locationController,
+ )
+
+ @Before
+ fun setup() {
+ enrollInForcedNightDisplayAutoMode(INITIALLY_FORCE_AUTO_MODE, testUser)
+ }
+
+ @Test
+ fun nightDisplayState_matchesAutoMode() =
+ scope.runTest {
+ enrollInForcedNightDisplayAutoMode(INITIALLY_FORCE_AUTO_MODE, testUser)
+ val callbackCaptor = argumentCaptor<NightDisplayListener.Callback>()
+ val lastState by collectLastValue(underTest.nightDisplayState(testUser))
+
+ runCurrent()
+
+ verify(nightDisplayListener).setCallback(callbackCaptor.capture())
+ val callback = callbackCaptor.value
+
+ assertThat(lastState!!.autoMode).isEqualTo(ColorDisplayManager.AUTO_MODE_DISABLED)
+
+ callback.onAutoModeChanged(ColorDisplayManager.AUTO_MODE_CUSTOM_TIME)
+ assertThat(lastState!!.autoMode).isEqualTo(ColorDisplayManager.AUTO_MODE_CUSTOM_TIME)
+
+ callback.onCustomStartTimeChanged(testStartTime)
+ assertThat(lastState!!.startTime).isEqualTo(testStartTime)
+
+ callback.onCustomEndTimeChanged(testEndTime)
+ assertThat(lastState!!.endTime).isEqualTo(testEndTime)
+
+ callback.onAutoModeChanged(ColorDisplayManager.AUTO_MODE_TWILIGHT)
+
+ assertThat(lastState!!.autoMode).isEqualTo(ColorDisplayManager.AUTO_MODE_TWILIGHT)
+ }
+
+ @Test
+ fun nightDisplayState_matchesIsNightDisplayActivated() =
+ scope.runTest {
+ val callbackCaptor = argumentCaptor<NightDisplayListener.Callback>()
+
+ val lastState by collectLastValue(underTest.nightDisplayState(testUser))
+ runCurrent()
+
+ verify(nightDisplayListener).setCallback(callbackCaptor.capture())
+ val callback = callbackCaptor.value
+ assertThat(lastState!!.isActivated)
+ .isEqualTo(colorDisplayManager.isNightDisplayActivated)
+
+ callback.onActivated(true)
+ assertThat(lastState!!.isActivated).isTrue()
+
+ callback.onActivated(false)
+ assertThat(lastState!!.isActivated).isFalse()
+ }
+
+ @Test
+ fun nightDisplayState_matchesController_initiallyCustomAutoMode() =
+ scope.runTest {
+ whenever(colorDisplayManager.nightDisplayAutoMode)
+ .thenReturn(ColorDisplayManager.AUTO_MODE_CUSTOM_TIME)
+
+ val lastState by collectLastValue(underTest.nightDisplayState(testUser))
+ runCurrent()
+
+ assertThat(lastState!!.autoMode).isEqualTo(ColorDisplayManager.AUTO_MODE_CUSTOM_TIME)
+ }
+
+ @Test
+ fun nightDisplayState_matchesController_initiallyTwilightAutoMode() =
+ scope.runTest {
+ whenever(colorDisplayManager.nightDisplayAutoMode)
+ .thenReturn(ColorDisplayManager.AUTO_MODE_TWILIGHT)
+
+ val lastState by collectLastValue(underTest.nightDisplayState(testUser))
+ runCurrent()
+
+ assertThat(lastState!!.autoMode).isEqualTo(ColorDisplayManager.AUTO_MODE_TWILIGHT)
+ }
+
+ @Test
+ fun nightDisplayState_matchesForceAutoMode() =
+ scope.runTest {
+ enrollInForcedNightDisplayAutoMode(false, testUser)
+ val lastState by collectLastValue(underTest.nightDisplayState(testUser))
+ runCurrent()
+
+ assertThat(lastState!!.shouldForceAutoMode).isEqualTo(false)
+
+ enrollInForcedNightDisplayAutoMode(true, testUser)
+ assertThat(lastState!!.shouldForceAutoMode).isEqualTo(true)
+ }
+
+ private fun enrollInForcedNightDisplayAutoMode(enroll: Boolean, userHandle: UserHandle) {
+ globalSettings.putString(
+ Settings.Global.NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE,
+ if (enroll) NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE
+ else NIGHT_DISPLAY_FORCED_AUTO_MODE_UNAVAILABLE
+ )
+ secureSettings.putIntForUser(
+ Settings.Secure.NIGHT_DISPLAY_AUTO_MODE,
+ if (enroll) NIGHT_DISPLAY_AUTO_MODE_RAW_NOT_SET else NIGHT_DISPLAY_AUTO_MODE_RAW_SET,
+ userHandle.identifier
+ )
+ }
+
+ private companion object {
+ const val INITIALLY_FORCE_AUTO_MODE = false
+ const val NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE = "1"
+ const val NIGHT_DISPLAY_FORCED_AUTO_MODE_UNAVAILABLE = "0"
+ const val NIGHT_DISPLAY_AUTO_MODE_RAW_NOT_SET = -1
+ const val NIGHT_DISPLAY_AUTO_MODE_RAW_SET = 0
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileDataInteractorTest.kt
new file mode 100644
index 0000000..a0aa2d4
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileDataInteractorTest.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2024 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.night.domain.interactor
+
+import android.hardware.display.ColorDisplayManager
+import android.hardware.display.NightDisplayListener
+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.accessibility.data.repository.NightDisplayRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dagger.NightDisplayListenerModule
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.user.utils.UserScopedService
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.fakeGlobalSettings
+import com.android.systemui.util.settings.fakeSettings
+import com.android.systemui.util.time.DateFormatUtil
+import com.android.systemui.utils.leaks.FakeLocationController
+import com.google.common.truth.Truth.assertThat
+import java.time.LocalTime
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NightDisplayTileDataInteractorTest : SysuiTestCase() {
+ private val kosmos = Kosmos()
+ private val testUser = UserHandle.of(1)!!
+ private val testStartTime = LocalTime.MIDNIGHT
+ private val testEndTime = LocalTime.NOON
+ private val colorDisplayManager =
+ mock<ColorDisplayManager> {
+ whenever(nightDisplayAutoMode).thenReturn(ColorDisplayManager.AUTO_MODE_DISABLED)
+ whenever(isNightDisplayActivated).thenReturn(false)
+ whenever(nightDisplayCustomStartTime).thenReturn(testStartTime)
+ whenever(nightDisplayCustomEndTime).thenReturn(testEndTime)
+ }
+ private val locationController = FakeLocationController(LeakCheck())
+ private val nightDisplayListener = mock<NightDisplayListener>()
+ private val listenerBuilder =
+ mock<NightDisplayListenerModule.Builder> {
+ whenever(setUser(anyInt())).thenReturn(this)
+ whenever(build()).thenReturn(nightDisplayListener)
+ }
+ private val globalSettings = kosmos.fakeGlobalSettings
+ private val secureSettings = kosmos.fakeSettings
+ private val dateFormatUtil = mock<DateFormatUtil> { whenever(is24HourFormat).thenReturn(false) }
+ private val testDispatcher = StandardTestDispatcher()
+ private val scope = TestScope(testDispatcher)
+ private val userScopedColorDisplayManager =
+ mock<UserScopedService<ColorDisplayManager>> {
+ whenever(forUser(eq(testUser))).thenReturn(colorDisplayManager)
+ }
+ private val nightDisplayRepository =
+ NightDisplayRepository(
+ testDispatcher,
+ scope.backgroundScope,
+ globalSettings,
+ secureSettings,
+ listenerBuilder,
+ userScopedColorDisplayManager,
+ locationController,
+ )
+
+ private val underTest: NightDisplayTileDataInteractor =
+ NightDisplayTileDataInteractor(context, dateFormatUtil, nightDisplayRepository)
+
+ @Test
+ fun availability_matchesColorDisplayManager() = runTest {
+ val availability by collectLastValue(underTest.availability(testUser))
+
+ val expectedAvailability = ColorDisplayManager.isNightDisplayAvailable(context)
+ assertThat(availability).isEqualTo(expectedAvailability)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractorTest.kt
new file mode 100644
index 0000000..adc8bcb
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractorTest.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2024 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.night.domain.interactor
+
+import android.hardware.display.ColorDisplayManager
+import android.hardware.display.NightDisplayListener
+import android.os.UserHandle
+import android.provider.Settings
+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.accessibility.data.repository.NightDisplayRepository
+import com.android.systemui.dagger.NightDisplayListenerModule
+import com.android.systemui.kosmos.Kosmos
+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.custom.qsTileLogger
+import com.android.systemui.qs.tiles.impl.night.domain.model.NightDisplayTileModel
+import com.android.systemui.user.utils.UserScopedService
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.fakeGlobalSettings
+import com.android.systemui.util.settings.fakeSettings
+import com.android.systemui.utils.leaks.FakeLocationController
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NightDisplayTileUserActionInteractorTest : SysuiTestCase() {
+ private val kosmos = Kosmos()
+ private val qsTileIntentUserActionHandler = FakeQSTileIntentUserInputHandler()
+ private val testUser = UserHandle.of(1)
+ private val colorDisplayManager =
+ mock<ColorDisplayManager> {
+ whenever(nightDisplayAutoMode).thenReturn(ColorDisplayManager.AUTO_MODE_DISABLED)
+ whenever(isNightDisplayActivated).thenReturn(false)
+ }
+ private val locationController = FakeLocationController(LeakCheck())
+ private val nightDisplayListener = mock<NightDisplayListener>()
+ private val listenerBuilder =
+ mock<NightDisplayListenerModule.Builder> {
+ whenever(setUser(ArgumentMatchers.anyInt())).thenReturn(this)
+ whenever(build()).thenReturn(nightDisplayListener)
+ }
+ private val globalSettings = kosmos.fakeGlobalSettings
+ private val secureSettings = kosmos.fakeSettings
+ private val testDispatcher = StandardTestDispatcher()
+ private val scope = TestScope(testDispatcher)
+ private val userScopedColorDisplayManager =
+ mock<UserScopedService<ColorDisplayManager>> {
+ whenever(forUser(eq(testUser))).thenReturn(colorDisplayManager)
+ }
+ private val nightDisplayRepository =
+ NightDisplayRepository(
+ testDispatcher,
+ scope.backgroundScope,
+ globalSettings,
+ secureSettings,
+ listenerBuilder,
+ userScopedColorDisplayManager,
+ locationController,
+ )
+
+ private val underTest =
+ NightDisplayTileUserActionInteractor(
+ nightDisplayRepository,
+ qsTileIntentUserActionHandler,
+ kosmos.qsTileLogger
+ )
+
+ @Test
+ fun handleClick_inactive_activates() =
+ scope.runTest {
+ val startingModel = NightDisplayTileModel.AutoModeOff(false, false)
+
+ underTest.handleInput(QSTileInputTestKtx.click(startingModel, testUser))
+
+ verify(colorDisplayManager).setNightDisplayActivated(true)
+ }
+
+ @Test
+ fun handleClick_active_disables() =
+ scope.runTest {
+ val startingModel = NightDisplayTileModel.AutoModeOff(true, false)
+
+ underTest.handleInput(QSTileInputTestKtx.click(startingModel, testUser))
+
+ verify(colorDisplayManager).setNightDisplayActivated(false)
+ }
+
+ @Test
+ fun handleClick_whenAutoModeTwilight_flipsState() =
+ scope.runTest {
+ val originalState = true
+ val startingModel = NightDisplayTileModel.AutoModeTwilight(originalState, false, false)
+
+ underTest.handleInput(QSTileInputTestKtx.click(startingModel, testUser))
+
+ verify(colorDisplayManager).setNightDisplayActivated(!originalState)
+ }
+
+ @Test
+ fun handleClick_whenAutoModeCustom_flipsState() =
+ scope.runTest {
+ val originalState = true
+ val startingModel =
+ NightDisplayTileModel.AutoModeCustom(originalState, false, null, null, false)
+
+ underTest.handleInput(QSTileInputTestKtx.click(startingModel, testUser))
+
+ verify(colorDisplayManager).setNightDisplayActivated(!originalState)
+ }
+
+ @Test
+ fun handleLongClickWhenEnabled() =
+ scope.runTest {
+ val enabledState = true
+
+ underTest.handleInput(
+ QSTileInputTestKtx.longClick(
+ NightDisplayTileModel.AutoModeOff(enabledState, false),
+ testUser
+ )
+ )
+
+ assertThat(qsTileIntentUserActionHandler.handledInputs).hasSize(1)
+
+ val intentInput = qsTileIntentUserActionHandler.intentInputs.last()
+ val actualIntentAction = intentInput.intent.action
+ val expectedIntentAction = Settings.ACTION_NIGHT_DISPLAY_SETTINGS
+ assertThat(actualIntentAction).isEqualTo(expectedIntentAction)
+ }
+
+ @Test
+ fun handleLongClickWhenDisabled() =
+ scope.runTest {
+ val enabledState = false
+
+ underTest.handleInput(
+ QSTileInputTestKtx.longClick(
+ NightDisplayTileModel.AutoModeOff(enabledState, false),
+ testUser
+ )
+ )
+
+ assertThat(qsTileIntentUserActionHandler.handledInputs).hasSize(1)
+
+ val intentInput = qsTileIntentUserActionHandler.intentInputs.last()
+ val actualIntentAction = intentInput.intent.action
+ val expectedIntentAction = Settings.ACTION_NIGHT_DISPLAY_SETTINGS
+ assertThat(actualIntentAction).isEqualTo(expectedIntentAction)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapperTest.kt
new file mode 100644
index 0000000..5d2e701
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapperTest.kt
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2024 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.night.ui
+
+import android.graphics.drawable.TestStubDrawable
+import android.service.quicksettings.Tile
+import android.text.TextUtils
+import android.widget.Switch
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.tiles.base.logging.QSTileLogger
+import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject
+import com.android.systemui.qs.tiles.impl.night.domain.model.NightDisplayTileModel
+import com.android.systemui.qs.tiles.impl.night.qsNightDisplayTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import com.android.systemui.util.mockito.mock
+import java.time.LocalTime
+import java.time.format.DateTimeFormatter
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NightDisplayTileMapperTest : SysuiTestCase() {
+ private val kosmos = Kosmos()
+ private val config = kosmos.qsNightDisplayTileConfig
+
+ private val testStartTime = LocalTime.MIDNIGHT
+ private val testEndTime = LocalTime.NOON
+
+ private lateinit var mapper: NightDisplayTileMapper
+
+ @Before
+ fun setup() {
+ mapper =
+ NightDisplayTileMapper(
+ context.orCreateTestableResources
+ .apply {
+ addOverride(R.drawable.qs_nightlight_icon_on, TestStubDrawable())
+ addOverride(R.drawable.qs_nightlight_icon_off, TestStubDrawable())
+ }
+ .resources,
+ context.theme,
+ mock<QSTileLogger>(),
+ )
+ }
+
+ @Test
+ fun disabledModel_whenAutoModeOff() {
+ val inputModel = NightDisplayTileModel.AutoModeOff(false, false)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.INACTIVE,
+ context.resources.getStringArray(R.array.tile_states_night)[Tile.STATE_INACTIVE]
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ /** Force enable does not change the mode by itself. */
+ @Test
+ fun disabledModel_whenAutoModeOff_whenForceEnable() {
+ val inputModel = NightDisplayTileModel.AutoModeOff(false, true)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.INACTIVE,
+ context.resources.getStringArray(R.array.tile_states_night)[Tile.STATE_INACTIVE]
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun enabledModel_whenAutoModeOff() {
+ val inputModel = NightDisplayTileModel.AutoModeOff(true, false)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.ACTIVE,
+ context.resources.getStringArray(R.array.tile_states_night)[Tile.STATE_ACTIVE]
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun enabledModel_forceAutoMode_whenAutoModeOff() {
+ val inputModel = NightDisplayTileModel.AutoModeOff(true, true)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.ACTIVE,
+ context.resources.getStringArray(R.array.tile_states_night)[Tile.STATE_ACTIVE]
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun enabledModel_autoModeTwilight_locationOff() {
+ val inputModel = NightDisplayTileModel.AutoModeTwilight(true, false, false)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState = createNightDisplayTileState(QSTileState.ActivationState.ACTIVE, null)
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun enabledModel_autoModeTwilight_locationOn() {
+ val inputModel = NightDisplayTileModel.AutoModeTwilight(true, false, true)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.ACTIVE,
+ context.getString(R.string.quick_settings_night_secondary_label_until_sunrise)
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun disabledModel_autoModeTwilight_locationOn() {
+ val inputModel = NightDisplayTileModel.AutoModeTwilight(false, false, true)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.INACTIVE,
+ context.getString(R.string.quick_settings_night_secondary_label_on_at_sunset)
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun disabledModel_autoModeTwilight_locationOff() {
+ val inputModel = NightDisplayTileModel.AutoModeTwilight(false, false, false)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState = createNightDisplayTileState(QSTileState.ActivationState.INACTIVE, null)
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun disabledModel_autoModeCustom_24Hour() {
+ val inputModel =
+ NightDisplayTileModel.AutoModeCustom(false, false, testStartTime, null, true)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.INACTIVE,
+ context.getString(
+ R.string.quick_settings_night_secondary_label_on_at,
+ formatter24Hour.format(testStartTime)
+ )
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun disabledModel_autoModeCustom_12Hour() {
+ val inputModel =
+ NightDisplayTileModel.AutoModeCustom(false, false, testStartTime, null, false)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.INACTIVE,
+ context.getString(
+ R.string.quick_settings_night_secondary_label_on_at,
+ formatter12Hour.format(testStartTime)
+ )
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ /** Should have the same outcome as [disabledModel_autoModeCustom_12Hour] */
+ @Test
+ fun disabledModel_autoModeCustom_12Hour_isEnrolledForcedAutoMode() {
+ val inputModel =
+ NightDisplayTileModel.AutoModeCustom(false, true, testStartTime, null, false)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.INACTIVE,
+ context.getString(
+ R.string.quick_settings_night_secondary_label_on_at,
+ formatter12Hour.format(testStartTime)
+ )
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun enabledModel_autoModeCustom_24Hour() {
+ val inputModel = NightDisplayTileModel.AutoModeCustom(true, false, null, testEndTime, true)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.ACTIVE,
+ context.getString(
+ R.string.quick_settings_secondary_label_until,
+ formatter24Hour.format(testEndTime)
+ )
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun enabledModel_autoModeCustom_12Hour() {
+ val inputModel = NightDisplayTileModel.AutoModeCustom(true, false, null, testEndTime, false)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.ACTIVE,
+ context.getString(
+ R.string.quick_settings_secondary_label_until,
+ formatter12Hour.format(testEndTime)
+ )
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ /** Should have the same state as [enabledModel_autoModeCustom_24Hour] */
+ @Test
+ fun enabledModel_autoModeCustom_24Hour_forceEnabled() {
+ val inputModel = NightDisplayTileModel.AutoModeCustom(true, true, null, testEndTime, true)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.ACTIVE,
+ context.getString(
+ R.string.quick_settings_secondary_label_until,
+ formatter24Hour.format(testEndTime)
+ )
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ private fun createNightDisplayTileState(
+ activationState: QSTileState.ActivationState,
+ secondaryLabel: String?
+ ): QSTileState {
+ val label = context.getString(R.string.quick_settings_night_display_label)
+
+ val contentDescription =
+ if (TextUtils.isEmpty(secondaryLabel)) label
+ else TextUtils.concat(label, ", ", secondaryLabel)
+ return QSTileState(
+ {
+ Icon.Loaded(
+ context.getDrawable(
+ if (activationState == QSTileState.ActivationState.ACTIVE)
+ R.drawable.qs_nightlight_icon_on
+ else R.drawable.qs_nightlight_icon_off
+ )!!,
+ null
+ )
+ },
+ label,
+ activationState,
+ secondaryLabel,
+ setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK),
+ contentDescription,
+ null,
+ QSTileState.SideViewIcon.None,
+ QSTileState.EnabledState.ENABLED,
+ Switch::class.qualifiedName
+ )
+ }
+
+ private companion object {
+ val formatter12Hour: DateTimeFormatter = DateTimeFormatter.ofPattern("hh:mm a")
+ val formatter24Hour: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm")
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/model/NightDisplayChangeEvent.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/model/NightDisplayChangeEvent.kt
new file mode 100644
index 0000000..8f071e4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/model/NightDisplayChangeEvent.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 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.accessibility.data.model
+
+import java.time.LocalTime
+
+sealed interface NightDisplayChangeEvent {
+ data class OnAutoModeChanged(val autoMode: Int) : NightDisplayChangeEvent
+ data class OnActivatedChanged(val isActivated: Boolean) : NightDisplayChangeEvent
+ data class OnCustomStartTimeChanged(val startTime: LocalTime?) : NightDisplayChangeEvent
+ data class OnCustomEndTimeChanged(val endTime: LocalTime?) : NightDisplayChangeEvent
+ data class OnForceAutoModeChanged(val shouldForceAutoMode: Boolean) : NightDisplayChangeEvent
+ data class OnLocationEnabledChanged(val locationEnabled: Boolean) : NightDisplayChangeEvent
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/model/NightDisplayState.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/model/NightDisplayState.kt
new file mode 100644
index 0000000..196876e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/model/NightDisplayState.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 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.accessibility.data.model
+
+import java.time.LocalTime
+
+/** models the state of NightDisplayRepository */
+data class NightDisplayState(
+ val autoMode: Int = 0,
+ val isActivated: Boolean = true,
+ val startTime: LocalTime? = null,
+ val endTime: LocalTime? = null,
+ val shouldForceAutoMode: Boolean = false,
+ val locationEnabled: Boolean = false,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/NightDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/NightDisplayRepository.kt
new file mode 100644
index 0000000..bf44fab
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/NightDisplayRepository.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2024 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.accessibility.data.repository
+
+import android.hardware.display.ColorDisplayManager
+import android.hardware.display.NightDisplayListener
+import android.os.UserHandle
+import android.provider.Settings
+import com.android.systemui.accessibility.data.model.NightDisplayChangeEvent
+import com.android.systemui.accessibility.data.model.NightDisplayState
+import com.android.systemui.dagger.NightDisplayListenerModule
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.policy.LocationController
+import com.android.systemui.user.utils.UserScopedService
+import com.android.systemui.util.kotlin.isLocationEnabledFlow
+import com.android.systemui.util.settings.GlobalSettings
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import java.time.LocalTime
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.scan
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+
+class NightDisplayRepository
+@Inject
+constructor(
+ @Background private val bgCoroutineContext: CoroutineContext,
+ @Application private val scope: CoroutineScope,
+ private val globalSettings: GlobalSettings,
+ private val secureSettings: SecureSettings,
+ private val nightDisplayListenerBuilder: NightDisplayListenerModule.Builder,
+ private val colorDisplayManagerUserScopedService: UserScopedService<ColorDisplayManager>,
+ private val locationController: LocationController,
+) {
+ private val stateFlowUserMap = mutableMapOf<Int, Flow<NightDisplayState>>()
+
+ fun nightDisplayState(user: UserHandle): Flow<NightDisplayState> =
+ stateFlowUserMap.getOrPut(user.identifier) {
+ return merge(
+ colorDisplayManagerChangeEventFlow(user),
+ shouldForceAutoMode(user).map {
+ NightDisplayChangeEvent.OnForceAutoModeChanged(it)
+ },
+ locationController.isLocationEnabledFlow().map {
+ NightDisplayChangeEvent.OnLocationEnabledChanged(it)
+ }
+ )
+ .scan(initialState(user)) { state, event ->
+ when (event) {
+ is NightDisplayChangeEvent.OnActivatedChanged ->
+ state.copy(isActivated = event.isActivated)
+ is NightDisplayChangeEvent.OnAutoModeChanged ->
+ state.copy(autoMode = event.autoMode)
+ is NightDisplayChangeEvent.OnCustomStartTimeChanged ->
+ state.copy(startTime = event.startTime)
+ is NightDisplayChangeEvent.OnCustomEndTimeChanged ->
+ state.copy(endTime = event.endTime)
+ is NightDisplayChangeEvent.OnForceAutoModeChanged ->
+ state.copy(shouldForceAutoMode = event.shouldForceAutoMode)
+ is NightDisplayChangeEvent.OnLocationEnabledChanged ->
+ state.copy(locationEnabled = event.locationEnabled)
+ }
+ }
+ .conflate()
+ .onStart { emit(initialState(user)) }
+ .flowOn(bgCoroutineContext)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), NightDisplayState())
+ }
+
+ /** Track changes in night display enabled state and its auto mode */
+ private fun colorDisplayManagerChangeEventFlow(user: UserHandle) = callbackFlow {
+ val nightDisplayListener = nightDisplayListenerBuilder.setUser(user.identifier).build()
+ val nightDisplayCallback =
+ object : NightDisplayListener.Callback {
+ override fun onActivated(activated: Boolean) {
+ trySend(NightDisplayChangeEvent.OnActivatedChanged(activated))
+ }
+
+ override fun onAutoModeChanged(autoMode: Int) {
+ trySend(NightDisplayChangeEvent.OnAutoModeChanged(autoMode))
+ }
+
+ override fun onCustomStartTimeChanged(startTime: LocalTime?) {
+ trySend(NightDisplayChangeEvent.OnCustomStartTimeChanged(startTime))
+ }
+
+ override fun onCustomEndTimeChanged(endTime: LocalTime?) {
+ trySend(NightDisplayChangeEvent.OnCustomEndTimeChanged(endTime))
+ }
+ }
+ nightDisplayListener.setCallback(nightDisplayCallback)
+ awaitClose { nightDisplayListener.setCallback(null) }
+ }
+
+ /** @return true when the option to force auto mode is available and a value has not been set */
+ private fun shouldForceAutoMode(userHandle: UserHandle): Flow<Boolean> =
+ combine(isForceAutoModeAvailable, isDisplayAutoModeRawNotSet(userHandle)) {
+ isForceAutoModeAvailable,
+ isDisplayAutoModeRawNotSet,
+ ->
+ isForceAutoModeAvailable && isDisplayAutoModeRawNotSet
+ }
+
+ private val isForceAutoModeAvailable: Flow<Boolean> =
+ globalSettings
+ .observerFlow(IS_FORCE_AUTO_MODE_AVAILABLE_SETTING_NAME)
+ .onStart { emit(Unit) }
+ .map {
+ globalSettings.getString(IS_FORCE_AUTO_MODE_AVAILABLE_SETTING_NAME) ==
+ NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE
+ }
+ .distinctUntilChanged()
+
+ /** Inspired by [ColorDisplayService.getNightDisplayAutoModeRawInternal] */
+ private fun isDisplayAutoModeRawNotSet(userHandle: UserHandle): Flow<Boolean> =
+ if (userHandle.identifier == UserHandle.USER_NULL) {
+ flowOf(IS_AUTO_MODE_RAW_NOT_SET_DEFAULT)
+ } else {
+ secureSettings
+ .observerFlow(userHandle.identifier, DISPLAY_AUTO_MODE_RAW_SETTING_NAME)
+ .onStart { emit(Unit) }
+ .map {
+ secureSettings.getIntForUser(
+ DISPLAY_AUTO_MODE_RAW_SETTING_NAME,
+ userHandle.identifier
+ ) == NIGHT_DISPLAY_AUTO_MODE_RAW_NOT_SET
+ }
+ }
+ .distinctUntilChanged()
+
+ suspend fun setNightDisplayAutoMode(autoMode: Int, user: UserHandle) {
+ withContext(bgCoroutineContext) {
+ colorDisplayManagerUserScopedService.forUser(user).nightDisplayAutoMode = autoMode
+ }
+ }
+
+ suspend fun setNightDisplayActivated(activated: Boolean, user: UserHandle) {
+ withContext(bgCoroutineContext) {
+ colorDisplayManagerUserScopedService.forUser(user).isNightDisplayActivated = activated
+ }
+ }
+
+ private fun initialState(user: UserHandle): NightDisplayState {
+ val colorDisplayManager = colorDisplayManagerUserScopedService.forUser(user)
+ return NightDisplayState(
+ colorDisplayManager.nightDisplayAutoMode,
+ colorDisplayManager.isNightDisplayActivated,
+ colorDisplayManager.nightDisplayCustomStartTime,
+ colorDisplayManager.nightDisplayCustomEndTime,
+ globalSettings.getString(IS_FORCE_AUTO_MODE_AVAILABLE_SETTING_NAME) ==
+ NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE &&
+ secureSettings.getIntForUser(DISPLAY_AUTO_MODE_RAW_SETTING_NAME, user.identifier) ==
+ NIGHT_DISPLAY_AUTO_MODE_RAW_NOT_SET,
+ locationController.isLocationEnabled,
+ )
+ }
+
+ private companion object {
+ const val NIGHT_DISPLAY_AUTO_MODE_RAW_NOT_SET = -1
+ const val NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE = "1"
+ const val IS_AUTO_MODE_RAW_NOT_SET_DEFAULT = true
+ const val IS_FORCE_AUTO_MODE_AVAILABLE_SETTING_NAME =
+ Settings.Global.NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE
+ const val DISPLAY_AUTO_MODE_RAW_SETTING_NAME = Settings.Secure.NIGHT_DISPLAY_AUTO_MODE
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt b/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt
index 54dd6d0..ed9597d 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt
@@ -41,6 +41,10 @@
import com.android.systemui.qs.tiles.impl.inversion.domain.interactor.ColorInversionTileDataInteractor
import com.android.systemui.qs.tiles.impl.inversion.domain.interactor.ColorInversionUserActionInteractor
import com.android.systemui.qs.tiles.impl.inversion.domain.model.ColorInversionTileModel
+import com.android.systemui.qs.tiles.impl.night.domain.interactor.NightDisplayTileDataInteractor
+import com.android.systemui.qs.tiles.impl.night.domain.interactor.NightDisplayTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.night.domain.model.NightDisplayTileModel
+import com.android.systemui.qs.tiles.impl.night.ui.NightDisplayTileMapper
import com.android.systemui.qs.tiles.impl.onehanded.domain.OneHandedModeTileDataInteractor
import com.android.systemui.qs.tiles.impl.onehanded.domain.OneHandedModeTileUserActionInteractor
import com.android.systemui.qs.tiles.impl.onehanded.domain.model.OneHandedModeTileModel
@@ -117,6 +121,7 @@
const val FONT_SCALING_TILE_SPEC = "font_scaling"
const val REDUCE_BRIGHTNESS_TILE_SPEC = "reduce_brightness"
const val ONE_HANDED_TILE_SPEC = "onehanded"
+ const val NIGHT_DISPLAY_TILE_SPEC = "night"
@Provides
@IntoMap
@@ -279,5 +284,41 @@
mapper,
)
else StubQSTileViewModel
+
+ @Provides
+ @IntoMap
+ @StringKey(NIGHT_DISPLAY_TILE_SPEC)
+ fun provideNightDisplayTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
+ QSTileConfig(
+ tileSpec = TileSpec.create(NIGHT_DISPLAY_TILE_SPEC),
+ uiConfig =
+ QSTileUIConfig.Resource(
+ iconRes = R.drawable.qs_nightlight_icon_off,
+ labelRes = R.string.quick_settings_night_display_label,
+ ),
+ instanceId = uiEventLogger.getNewInstanceId(),
+ )
+
+ /**
+ * Inject NightDisplay Tile into tileViewModelMap in QSModule. The tile is hidden behind a
+ * flag.
+ */
+ @Provides
+ @IntoMap
+ @StringKey(NIGHT_DISPLAY_TILE_SPEC)
+ fun provideNightDisplayTileViewModel(
+ factory: QSTileViewModelFactory.Static<NightDisplayTileModel>,
+ mapper: NightDisplayTileMapper,
+ stateInteractor: NightDisplayTileDataInteractor,
+ userActionInteractor: NightDisplayTileUserActionInteractor
+ ): QSTileViewModel =
+ if (Flags.qsNewTilesFuture())
+ factory.create(
+ TileSpec.create(NIGHT_DISPLAY_TILE_SPEC),
+ userActionInteractor,
+ stateInteractor,
+ mapper,
+ )
+ else StubQSTileViewModel
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index ef3f10f..5630af8 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -224,6 +224,13 @@
@Provides
@Singleton
+ static UserScopedService<ColorDisplayManager> provideScopedColorDisplayManager(
+ Context context) {
+ return new UserScopedServiceImpl<>(context, ColorDisplayManager.class);
+ }
+
+ @Provides
+ @Singleton
static CrossWindowBlurListeners provideCrossWindowBlurListeners() {
return CrossWindowBlurListeners.getInstance();
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
index b515ce0..278352c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
@@ -28,6 +28,7 @@
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.LogLevel.DEBUG
import com.android.systemui.log.core.LogLevel.ERROR
+import com.android.systemui.log.core.LogLevel.INFO
import com.android.systemui.log.core.LogLevel.VERBOSE
import com.android.systemui.log.dagger.QSConfigLog
import com.android.systemui.log.dagger.QSLog
@@ -56,6 +57,9 @@
fun d(@CompileTimeConstant msg: String, arg: Any) {
buffer.log(TAG, DEBUG, { str1 = arg.toString() }, { "$msg: $str1" })
}
+ fun i(@CompileTimeConstant msg: String, arg: Any) {
+ buffer.log(TAG, INFO, { str1 = arg.toString() }, { "$msg: $str1" })
+ }
fun logTileAdded(tileSpec: String) {
buffer.log(TAG, DEBUG, { str1 = tileSpec }, { "[$str1] Tile added" })
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt
index 065e89f..f0d7206 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt
@@ -175,6 +175,26 @@
)
}
+ /** Log with level [LogLevel.WARNING] */
+ fun logWarning(
+ tileSpec: TileSpec,
+ message: String,
+ ) {
+ tileSpec
+ .getLogBuffer()
+ .log(tileSpec.getLogTag(), LogLevel.WARNING, { str1 = message }, { str1!! })
+ }
+
+ /** Log with level [LogLevel.INFO] */
+ fun logInfo(
+ tileSpec: TileSpec,
+ message: String,
+ ) {
+ tileSpec
+ .getLogBuffer()
+ .log(tileSpec.getLogTag(), LogLevel.INFO, { str1 = message }, { str1!! })
+ }
+
fun logCustomTileUserActionDelivered(tileSpec: TileSpec) {
tileSpec
.getLogBuffer()
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileDataInteractor.kt
index d1c8030..bd2f2c9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileDataInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileDataInteractor.kt
@@ -17,15 +17,15 @@
package com.android.systemui.qs.tiles.impl.location.domain.interactor
import android.os.UserHandle
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow
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.location.domain.model.LocationTileModel
import com.android.systemui.statusbar.policy.LocationController
+import com.android.systemui.util.kotlin.isLocationEnabledFlow
import javax.inject.Inject
-import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
/** Observes location state changes providing the [LocationTileModel]. */
class LocationTileDataInteractor
@@ -38,19 +38,7 @@
user: UserHandle,
triggers: Flow<DataUpdateTrigger>
): Flow<LocationTileModel> =
- ConflatedCallbackFlow.conflatedCallbackFlow {
- val initialValue = locationController.isLocationEnabled
- trySend(LocationTileModel(initialValue))
-
- val callback =
- object : LocationController.LocationChangeCallback {
- override fun onLocationSettingsChanged(locationEnabled: Boolean) {
- trySend(LocationTileModel(locationEnabled))
- }
- }
- locationController.addCallback(callback)
- awaitClose { locationController.removeCallback(callback) }
- }
+ locationController.isLocationEnabledFlow().map { LocationTileModel(it) }
override fun availability(user: UserHandle): Flow<Boolean> = flowOf(true)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileDataInteractor.kt
new file mode 100644
index 0000000..88bd224
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileDataInteractor.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 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.night.domain.interactor
+
+import android.content.Context
+import android.hardware.display.ColorDisplayManager
+import android.os.UserHandle
+import com.android.systemui.accessibility.data.repository.NightDisplayRepository
+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.night.domain.model.NightDisplayTileModel
+import com.android.systemui.util.time.DateFormatUtil
+import java.time.LocalTime
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+
+/** Observes screen record state changes providing the [NightDisplayTileModel]. */
+class NightDisplayTileDataInteractor
+@Inject
+constructor(
+ @Application private val context: Context,
+ private val dateFormatUtil: DateFormatUtil,
+ private val nightDisplayRepository: NightDisplayRepository,
+) : QSTileDataInteractor<NightDisplayTileModel> {
+
+ override fun tileData(
+ user: UserHandle,
+ triggers: Flow<DataUpdateTrigger>
+ ): Flow<NightDisplayTileModel> =
+ nightDisplayRepository.nightDisplayState(user).map {
+ generateModel(
+ it.autoMode,
+ it.isActivated,
+ it.startTime,
+ it.endTime,
+ it.shouldForceAutoMode,
+ it.locationEnabled
+ )
+ }
+
+ /** This checks resources and there fore does not make a binder call. */
+ override fun availability(user: UserHandle): Flow<Boolean> =
+ flowOf(ColorDisplayManager.isNightDisplayAvailable(context))
+
+ private fun generateModel(
+ autoMode: Int,
+ isNightDisplayActivated: Boolean,
+ customStartTime: LocalTime?,
+ customEndTime: LocalTime?,
+ shouldForceAutoMode: Boolean,
+ locationEnabled: Boolean,
+ ): NightDisplayTileModel {
+ if (autoMode == ColorDisplayManager.AUTO_MODE_TWILIGHT) {
+ return NightDisplayTileModel.AutoModeTwilight(
+ isNightDisplayActivated,
+ shouldForceAutoMode,
+ locationEnabled,
+ )
+ } else if (autoMode == ColorDisplayManager.AUTO_MODE_CUSTOM_TIME) {
+ return NightDisplayTileModel.AutoModeCustom(
+ isNightDisplayActivated,
+ shouldForceAutoMode,
+ customStartTime,
+ customEndTime,
+ dateFormatUtil.is24HourFormat,
+ )
+ } else { // auto mode off
+ return NightDisplayTileModel.AutoModeOff(isNightDisplayActivated, shouldForceAutoMode)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractor.kt
new file mode 100644
index 0000000..5cee8c4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractor.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 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.night.domain.interactor
+
+import android.content.Intent
+import android.hardware.display.ColorDisplayManager.AUTO_MODE_CUSTOM_TIME
+import android.provider.Settings
+import com.android.systemui.accessibility.data.repository.NightDisplayRepository
+import com.android.systemui.accessibility.qs.QSAccessibilityModule
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.interactor.QSTileInput
+import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.base.logging.QSTileLogger
+import com.android.systemui.qs.tiles.impl.night.domain.model.NightDisplayTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import javax.inject.Inject
+
+/** Handles night display tile clicks. */
+class NightDisplayTileUserActionInteractor
+@Inject
+constructor(
+ private val nightDisplayRepository: NightDisplayRepository,
+ private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
+ private val qsLogger: QSTileLogger,
+) : QSTileUserActionInteractor<NightDisplayTileModel> {
+ override suspend fun handleInput(input: QSTileInput<NightDisplayTileModel>): Unit =
+ with(input) {
+ when (action) {
+ is QSTileUserAction.Click -> {
+ // Enroll in forced auto mode if eligible.
+ if (data.isEnrolledInForcedNightDisplayAutoMode) {
+ nightDisplayRepository.setNightDisplayAutoMode(AUTO_MODE_CUSTOM_TIME, user)
+ qsLogger.logInfo(spec, "Enrolled in forced night display auto mode")
+ }
+ nightDisplayRepository.setNightDisplayActivated(!data.isActivated, user)
+ }
+ is QSTileUserAction.LongClick -> {
+ qsTileIntentUserActionHandler.handle(
+ action.expandable,
+ Intent(Settings.ACTION_NIGHT_DISPLAY_SETTINGS)
+ )
+ }
+ }
+ }
+
+ companion object {
+ val spec = TileSpec.create(QSAccessibilityModule.NIGHT_DISPLAY_TILE_SPEC)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/model/NightDisplayTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/model/NightDisplayTileModel.kt
new file mode 100644
index 0000000..6b1bd5b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/model/NightDisplayTileModel.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 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.night.domain.model
+
+import java.time.LocalTime
+
+/** Data model for night display tile */
+sealed interface NightDisplayTileModel {
+ val isActivated: Boolean
+ val isEnrolledInForcedNightDisplayAutoMode: Boolean
+ data class AutoModeTwilight(
+ override val isActivated: Boolean,
+ override val isEnrolledInForcedNightDisplayAutoMode: Boolean,
+ val isLocationEnabled: Boolean
+ ) : NightDisplayTileModel
+ data class AutoModeCustom(
+ override val isActivated: Boolean,
+ override val isEnrolledInForcedNightDisplayAutoMode: Boolean,
+ val startTime: LocalTime?,
+ val endTime: LocalTime?,
+ val is24HourFormat: Boolean
+ ) : NightDisplayTileModel
+ data class AutoModeOff(
+ override val isActivated: Boolean,
+ override val isEnrolledInForcedNightDisplayAutoMode: Boolean
+ ) : NightDisplayTileModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapper.kt
new file mode 100644
index 0000000..5c2dcfc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapper.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2024 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.night.ui
+
+import android.content.res.Resources
+import android.service.quicksettings.Tile
+import android.text.TextUtils
+import androidx.annotation.StringRes
+import com.android.systemui.accessibility.qs.QSAccessibilityModule
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.base.logging.QSTileLogger
+import com.android.systemui.qs.tiles.impl.night.domain.model.NightDisplayTileModel
+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.DateTimeException
+import java.time.LocalTime
+import java.time.format.DateTimeFormatter
+import javax.inject.Inject
+
+/** Maps [NightDisplayTileModel] to [QSTileState]. */
+class NightDisplayTileMapper
+@Inject
+constructor(
+ @Main private val resources: Resources,
+ private val theme: Resources.Theme,
+ private val logger: QSTileLogger,
+) : QSTileDataToStateMapper<NightDisplayTileModel> {
+ override fun map(config: QSTileConfig, data: NightDisplayTileModel): QSTileState =
+ QSTileState.build(resources, theme, config.uiConfig) {
+ label = resources.getString(R.string.quick_settings_night_display_label)
+ supportedActions =
+ setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK)
+ sideViewIcon = QSTileState.SideViewIcon.None
+
+ if (data.isActivated) {
+ activationState = QSTileState.ActivationState.ACTIVE
+ val loadedIcon =
+ Icon.Loaded(
+ resources.getDrawable(R.drawable.qs_nightlight_icon_on, theme),
+ contentDescription = null
+ )
+ icon = { loadedIcon }
+ } else {
+ activationState = QSTileState.ActivationState.INACTIVE
+ val loadedIcon =
+ Icon.Loaded(
+ resources.getDrawable(R.drawable.qs_nightlight_icon_off, theme),
+ contentDescription = null
+ )
+ icon = { loadedIcon }
+ }
+
+ secondaryLabel = getSecondaryLabel(data, resources)
+
+ contentDescription =
+ if (TextUtils.isEmpty(secondaryLabel)) label
+ else TextUtils.concat(label, ", ", secondaryLabel)
+ }
+
+ private fun getSecondaryLabel(
+ data: NightDisplayTileModel,
+ resources: Resources
+ ): CharSequence? {
+ when (data) {
+ is NightDisplayTileModel.AutoModeTwilight -> {
+ if (!data.isLocationEnabled) {
+ return null
+ } else {
+ return resources.getString(
+ if (data.isActivated)
+ R.string.quick_settings_night_secondary_label_until_sunrise
+ else R.string.quick_settings_night_secondary_label_on_at_sunset
+ )
+ }
+ }
+ is NightDisplayTileModel.AutoModeOff -> {
+ val subtitleArray = resources.getStringArray(R.array.tile_states_night)
+ return subtitleArray[
+ if (data.isActivated) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE]
+ }
+ is NightDisplayTileModel.AutoModeCustom -> {
+ // User-specified time, approximated to the nearest hour.
+ @StringRes val toggleTimeStringRes: Int
+ val toggleTime: LocalTime
+ if (data.isActivated) {
+ toggleTime = data.endTime ?: return null
+ toggleTimeStringRes = R.string.quick_settings_secondary_label_until
+ } else {
+ toggleTime = data.startTime ?: return null
+ toggleTimeStringRes = R.string.quick_settings_night_secondary_label_on_at
+ }
+
+ try {
+ val formatter = if (data.is24HourFormat) formatter24Hour else formatter12Hour
+ val formatArg = formatter.format(toggleTime)
+ return resources.getString(toggleTimeStringRes, formatArg)
+ } catch (exception: DateTimeException) {
+ logger.logWarning(spec, exception.message.toString())
+ return null
+ }
+ }
+ }
+ }
+
+ private companion object {
+ val formatter12Hour: DateTimeFormatter = DateTimeFormatter.ofPattern("hh:mm a")
+ val formatter24Hour: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm")
+ val spec = TileSpec.create(QSAccessibilityModule.NIGHT_DISPLAY_TILE_SPEC)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/LocationControllerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/LocationControllerExt.kt
new file mode 100644
index 0000000..ee1b565
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/LocationControllerExt.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 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.util.kotlin
+
+import com.android.systemui.statusbar.policy.LocationController
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.onStart
+
+fun LocationController.isLocationEnabledFlow(): Flow<Boolean> {
+ return conflatedCallbackFlow {
+ val locationCallback =
+ object : LocationController.LocationChangeCallback {
+ override fun onLocationSettingsChanged(locationEnabled: Boolean) {
+ trySend(locationEnabled)
+ }
+ }
+ addCallback(locationCallback)
+ awaitClose { removeCallback(locationCallback) }
+ }
+ .onStart { emit(isLocationEnabled) }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/night/NightDisplayTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/night/NightDisplayTileKosmos.kt
new file mode 100644
index 0000000..5c21ab6
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/night/NightDisplayTileKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 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.night
+
+import com.android.systemui.accessibility.qs.QSAccessibilityModule
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.qsEventLogger
+
+val Kosmos.qsNightDisplayTileConfig by
+ Kosmos.Fixture { QSAccessibilityModule.provideNightDisplayTileConfig(qsEventLogger) }