1 /*
<lambda>null2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.accessibility.data.repository
18 
19 import android.hardware.display.ColorDisplayManager
20 import android.hardware.display.NightDisplayListener
21 import android.os.UserHandle
22 import android.provider.Settings
23 import com.android.systemui.accessibility.data.model.NightDisplayChangeEvent
24 import com.android.systemui.accessibility.data.model.NightDisplayState
25 import com.android.systemui.dagger.NightDisplayListenerModule
26 import com.android.systemui.dagger.qualifiers.Application
27 import com.android.systemui.dagger.qualifiers.Background
28 import com.android.systemui.statusbar.policy.LocationController
29 import com.android.systemui.user.utils.UserScopedService
30 import com.android.systemui.util.kotlin.isLocationEnabledFlow
31 import com.android.systemui.util.settings.GlobalSettings
32 import com.android.systemui.util.settings.SecureSettings
33 import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
34 import java.time.LocalTime
35 import javax.inject.Inject
36 import kotlin.coroutines.CoroutineContext
37 import kotlinx.coroutines.CoroutineScope
38 import kotlinx.coroutines.channels.awaitClose
39 import kotlinx.coroutines.flow.Flow
40 import kotlinx.coroutines.flow.SharingStarted
41 import kotlinx.coroutines.flow.callbackFlow
42 import kotlinx.coroutines.flow.combine
43 import kotlinx.coroutines.flow.conflate
44 import kotlinx.coroutines.flow.distinctUntilChanged
45 import kotlinx.coroutines.flow.flowOf
46 import kotlinx.coroutines.flow.flowOn
47 import kotlinx.coroutines.flow.map
48 import kotlinx.coroutines.flow.merge
49 import kotlinx.coroutines.flow.onStart
50 import kotlinx.coroutines.flow.scan
51 import kotlinx.coroutines.flow.stateIn
52 import kotlinx.coroutines.withContext
53 
54 class NightDisplayRepository
55 @Inject
56 constructor(
57     @Background private val bgCoroutineContext: CoroutineContext,
58     @Application private val scope: CoroutineScope,
59     private val globalSettings: GlobalSettings,
60     private val secureSettings: SecureSettings,
61     private val nightDisplayListenerBuilder: NightDisplayListenerModule.Builder,
62     private val colorDisplayManagerUserScopedService: UserScopedService<ColorDisplayManager>,
63     private val locationController: LocationController,
64 ) {
65     private val stateFlowUserMap = mutableMapOf<Int, Flow<NightDisplayState>>()
66 
67     fun nightDisplayState(user: UserHandle): Flow<NightDisplayState> =
68         stateFlowUserMap.getOrPut(user.identifier) {
69             return merge(
70                     colorDisplayManagerChangeEventFlow(user),
71                     shouldForceAutoMode(user).map {
72                         NightDisplayChangeEvent.OnForceAutoModeChanged(it)
73                     },
74                     locationController.isLocationEnabledFlow().map {
75                         NightDisplayChangeEvent.OnLocationEnabledChanged(it)
76                     }
77                 )
78                 .scan(initialState(user)) { state, event ->
79                     when (event) {
80                         is NightDisplayChangeEvent.OnActivatedChanged ->
81                             state.copy(isActivated = event.isActivated)
82                         is NightDisplayChangeEvent.OnAutoModeChanged ->
83                             state.copy(autoMode = event.autoMode)
84                         is NightDisplayChangeEvent.OnCustomStartTimeChanged ->
85                             state.copy(startTime = event.startTime)
86                         is NightDisplayChangeEvent.OnCustomEndTimeChanged ->
87                             state.copy(endTime = event.endTime)
88                         is NightDisplayChangeEvent.OnForceAutoModeChanged ->
89                             state.copy(shouldForceAutoMode = event.shouldForceAutoMode)
90                         is NightDisplayChangeEvent.OnLocationEnabledChanged ->
91                             state.copy(locationEnabled = event.locationEnabled)
92                     }
93                 }
94                 .conflate()
95                 .onStart { emit(initialState(user)) }
96                 .flowOn(bgCoroutineContext)
97                 .stateIn(scope, SharingStarted.WhileSubscribed(), NightDisplayState())
98         }
99 
100     /** Track changes in night display enabled state and its auto mode */
101     private fun colorDisplayManagerChangeEventFlow(user: UserHandle) = callbackFlow {
102         val nightDisplayListener = nightDisplayListenerBuilder.setUser(user.identifier).build()
103         val nightDisplayCallback =
104             object : NightDisplayListener.Callback {
105                 override fun onActivated(activated: Boolean) {
106                     trySend(NightDisplayChangeEvent.OnActivatedChanged(activated))
107                 }
108 
109                 override fun onAutoModeChanged(autoMode: Int) {
110                     trySend(NightDisplayChangeEvent.OnAutoModeChanged(autoMode))
111                 }
112 
113                 override fun onCustomStartTimeChanged(startTime: LocalTime?) {
114                     trySend(NightDisplayChangeEvent.OnCustomStartTimeChanged(startTime))
115                 }
116 
117                 override fun onCustomEndTimeChanged(endTime: LocalTime?) {
118                     trySend(NightDisplayChangeEvent.OnCustomEndTimeChanged(endTime))
119                 }
120             }
121         nightDisplayListener.setCallback(nightDisplayCallback)
122         awaitClose { nightDisplayListener.setCallback(null) }
123     }
124 
125     /** @return true when the option to force auto mode is available and a value has not been set */
126     private fun shouldForceAutoMode(userHandle: UserHandle): Flow<Boolean> =
127         combine(isForceAutoModeAvailable, isDisplayAutoModeRawNotSet(userHandle)) {
128             isForceAutoModeAvailable,
129             isDisplayAutoModeRawNotSet,
130             ->
131             isForceAutoModeAvailable && isDisplayAutoModeRawNotSet
132         }
133 
134     private val isForceAutoModeAvailable: Flow<Boolean> =
135         globalSettings
136             .observerFlow(IS_FORCE_AUTO_MODE_AVAILABLE_SETTING_NAME)
137             .onStart { emit(Unit) }
138             .map {
139                 globalSettings.getString(IS_FORCE_AUTO_MODE_AVAILABLE_SETTING_NAME) ==
140                     NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE
141             }
142             .distinctUntilChanged()
143 
144     /** Inspired by [ColorDisplayService.getNightDisplayAutoModeRawInternal] */
145     private fun isDisplayAutoModeRawNotSet(userHandle: UserHandle): Flow<Boolean> =
146         if (userHandle.identifier == UserHandle.USER_NULL) {
147                 flowOf(IS_AUTO_MODE_RAW_NOT_SET_DEFAULT)
148             } else {
149                 secureSettings
150                     .observerFlow(userHandle.identifier, DISPLAY_AUTO_MODE_RAW_SETTING_NAME)
151                     .onStart { emit(Unit) }
152                     .map { isNightDisplayAutoModeRawSettingNotSet(userHandle.identifier) }
153             }
154             .distinctUntilChanged()
155 
156     suspend fun setNightDisplayAutoMode(autoMode: Int, user: UserHandle) {
157         withContext(bgCoroutineContext) {
158             colorDisplayManagerUserScopedService.forUser(user).nightDisplayAutoMode = autoMode
159         }
160     }
161 
162     suspend fun setNightDisplayActivated(activated: Boolean, user: UserHandle) {
163         withContext(bgCoroutineContext) {
164             colorDisplayManagerUserScopedService.forUser(user).isNightDisplayActivated = activated
165         }
166     }
167 
168     private fun initialState(user: UserHandle): NightDisplayState {
169         val colorDisplayManager = colorDisplayManagerUserScopedService.forUser(user)
170         return NightDisplayState(
171             colorDisplayManager.nightDisplayAutoMode,
172             colorDisplayManager.isNightDisplayActivated,
173             colorDisplayManager.nightDisplayCustomStartTime,
174             colorDisplayManager.nightDisplayCustomEndTime,
175             globalSettings.getString(IS_FORCE_AUTO_MODE_AVAILABLE_SETTING_NAME) ==
176                 NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE &&
177                 isNightDisplayAutoModeRawSettingNotSet(user.identifier),
178             locationController.isLocationEnabled,
179         )
180     }
181 
182     private fun isNightDisplayAutoModeRawSettingNotSet(userId: Int): Boolean {
183         return secureSettings.getIntForUser(
184             DISPLAY_AUTO_MODE_RAW_SETTING_NAME,
185             NIGHT_DISPLAY_AUTO_MODE_RAW_NOT_SET,
186             userId
187         ) == NIGHT_DISPLAY_AUTO_MODE_RAW_NOT_SET
188     }
189 
190     private companion object {
191         const val NIGHT_DISPLAY_AUTO_MODE_RAW_NOT_SET = -1
192         const val NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE = "1"
193         const val IS_AUTO_MODE_RAW_NOT_SET_DEFAULT = true
194         const val IS_FORCE_AUTO_MODE_AVAILABLE_SETTING_NAME =
195             Settings.Global.NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE
196         const val DISPLAY_AUTO_MODE_RAW_SETTING_NAME = Settings.Secure.NIGHT_DISPLAY_AUTO_MODE
197     }
198 }
199