1 /*
2  * 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.brightness.data.repository
18 
19 import android.annotation.SuppressLint
20 import android.hardware.display.BrightnessInfo
21 import android.hardware.display.DisplayManager
22 import com.android.systemui.brightness.shared.model.BrightnessLog
23 import com.android.systemui.brightness.shared.model.LinearBrightness
24 import com.android.systemui.brightness.shared.model.formatBrightness
25 import com.android.systemui.brightness.shared.model.logDiffForTable
26 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
27 import com.android.systemui.dagger.SysUISingleton
28 import com.android.systemui.dagger.qualifiers.Application
29 import com.android.systemui.dagger.qualifiers.Background
30 import com.android.systemui.dagger.qualifiers.DisplayId
31 import com.android.systemui.log.LogBuffer
32 import com.android.systemui.log.core.LogLevel
33 import com.android.systemui.log.table.TableLogBuffer
34 import javax.inject.Inject
35 import kotlin.coroutines.CoroutineContext
36 import kotlinx.coroutines.CoroutineScope
37 import kotlinx.coroutines.channels.Channel
38 import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
39 import kotlinx.coroutines.channels.awaitClose
40 import kotlinx.coroutines.flow.Flow
41 import kotlinx.coroutines.flow.SharedFlow
42 import kotlinx.coroutines.flow.SharingStarted
43 import kotlinx.coroutines.flow.StateFlow
44 import kotlinx.coroutines.flow.filterNotNull
45 import kotlinx.coroutines.flow.flowOn
46 import kotlinx.coroutines.flow.map
47 import kotlinx.coroutines.flow.onStart
48 import kotlinx.coroutines.flow.stateIn
49 import kotlinx.coroutines.launch
50 import kotlinx.coroutines.withContext
51 
52 /**
53  * Repository for tracking brightness in the current display.
54  *
55  * Values are in a linear space, as used by [DisplayManager].
56  */
57 interface ScreenBrightnessRepository {
58     /** Current brightness as a value between [minLinearBrightness] and [maxLinearBrightness] */
59     val linearBrightness: Flow<LinearBrightness>
60 
61     /** Current minimum value for the brightness */
62     val minLinearBrightness: Flow<LinearBrightness>
63 
64     /** Current maximum value for the brightness */
65     val maxLinearBrightness: Flow<LinearBrightness>
66 
67     /** Gets the current values for min and max brightness */
getMinMaxLinearBrightnessnull68     suspend fun getMinMaxLinearBrightness(): Pair<LinearBrightness, LinearBrightness>
69 
70     /**
71      * Sets the temporary value for the brightness. This should change the display brightness but
72      * not trigger any updates.
73      */
74     fun setTemporaryBrightness(value: LinearBrightness)
75 
76     /** Sets the brightness definitively. */
77     fun setBrightness(value: LinearBrightness)
78 }
79 
80 @SuppressLint("MissingPermission")
81 @SysUISingleton
82 class ScreenBrightnessDisplayManagerRepository
83 @Inject
84 constructor(
85     @DisplayId private val displayId: Int,
86     private val displayManager: DisplayManager,
87     @BrightnessLog private val logBuffer: LogBuffer,
88     @BrightnessLog private val tableBuffer: TableLogBuffer,
89     @Application private val applicationScope: CoroutineScope,
90     @Background private val backgroundContext: CoroutineContext,
91 ) : ScreenBrightnessRepository {
92 
93     private val apiQueue =
94         Channel<SetBrightnessMethod>(
95             capacity = UNLIMITED,
96         )
97 
98     init {
99         applicationScope.launch(backgroundContext) {
100             for (call in apiQueue) {
101                 val bounds = getMinMaxLinearBrightness()
102                 val value = call.value.clamp(bounds.first, bounds.second).floatValue
103                 when (call) {
104                     is SetBrightnessMethod.Temporary -> {
105                         displayManager.setTemporaryBrightness(displayId, value)
106                     }
107                     is SetBrightnessMethod.Permanent -> {
108                         displayManager.setBrightness(displayId, value)
109                     }
110                 }
111                 logBrightnessChange(call is SetBrightnessMethod.Permanent, value)
112             }
113         }
114     }
115 
116     private val brightnessInfo: StateFlow<BrightnessInfo?> =
117         conflatedCallbackFlow {
118                 val listener =
119                     object : DisplayManager.DisplayListener {
120                         override fun onDisplayAdded(displayId: Int) {}
121 
122                         override fun onDisplayRemoved(displayId: Int) {}
123 
124                         override fun onDisplayChanged(displayId: Int) {
125                             if (
126                                 displayId == this@ScreenBrightnessDisplayManagerRepository.displayId
127                             ) {
128                                 trySend(Unit)
129                             }
130                         }
131                     }
132                 displayManager.registerDisplayListener(
133                     listener,
134                     null,
135                     DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS,
136                 )
137 
138                 awaitClose { displayManager.unregisterDisplayListener(listener) }
139             }
140             .onStart { emit(Unit) }
141             .map { brightnessInfoValue() }
142             .flowOn(backgroundContext)
143             .stateIn(
144                 applicationScope,
145                 SharingStarted.WhileSubscribed(replayExpirationMillis = 0L),
146                 null,
147             )
148 
149     private suspend fun brightnessInfoValue(): BrightnessInfo? {
150         return withContext(backgroundContext) {
151             displayManager.getDisplay(displayId).brightnessInfo
152         }
153     }
154 
155     override val minLinearBrightness =
156         brightnessInfo
157             .filterNotNull()
158             .map { LinearBrightness(it.brightnessMinimum) }
159             .logDiffForTable(tableBuffer, TABLE_PREFIX_LINEAR, TABLE_COLUMN_MIN, null)
160             .stateIn(applicationScope, SharingStarted.WhileSubscribed(), LinearBrightness(0f))
161 
162     override val maxLinearBrightness: SharedFlow<LinearBrightness> =
163         brightnessInfo
164             .filterNotNull()
165             .map { LinearBrightness(it.brightnessMaximum) }
166             .logDiffForTable(tableBuffer, TABLE_PREFIX_LINEAR, TABLE_COLUMN_MAX, null)
167             .stateIn(applicationScope, SharingStarted.WhileSubscribed(), LinearBrightness(1f))
168 
169     override suspend fun getMinMaxLinearBrightness(): Pair<LinearBrightness, LinearBrightness> {
170         val brightnessInfo = brightnessInfo.value ?: brightnessInfoValue()
171         val min = brightnessInfo?.brightnessMinimum ?: 0f
172         val max = brightnessInfo?.brightnessMaximum ?: 1f
173         return LinearBrightness(min) to LinearBrightness(max)
174     }
175 
176     override val linearBrightness =
177         brightnessInfo
178             .filterNotNull()
179             .map { LinearBrightness(it.brightness) }
180             .logDiffForTable(tableBuffer, TABLE_PREFIX_LINEAR, TABLE_COLUMN_BRIGHTNESS, null)
181             .stateIn(applicationScope, SharingStarted.WhileSubscribed(), LinearBrightness(0f))
182 
183     override fun setTemporaryBrightness(value: LinearBrightness) {
184         apiQueue.trySend(SetBrightnessMethod.Temporary(value))
185     }
186 
187     override fun setBrightness(value: LinearBrightness) {
188         apiQueue.trySend(SetBrightnessMethod.Permanent(value))
189     }
190 
191     private sealed interface SetBrightnessMethod {
192         val value: LinearBrightness
193         @JvmInline
194         value class Temporary(override val value: LinearBrightness) : SetBrightnessMethod
195         @JvmInline
196         value class Permanent(override val value: LinearBrightness) : SetBrightnessMethod
197     }
198 
199     private fun logBrightnessChange(permanent: Boolean, value: Float) {
200         logBuffer.log(
201             LOG_BUFFER_BRIGHTNESS_CHANGE_TAG,
202             if (permanent) LogLevel.DEBUG else LogLevel.VERBOSE,
203             { str1 = value.formatBrightness() },
204             { "Change requested: $str1" }
205         )
206     }
207 
208     private companion object {
209         const val TABLE_COLUMN_BRIGHTNESS = "brightness"
210         const val TABLE_COLUMN_MIN = "min"
211         const val TABLE_COLUMN_MAX = "max"
212         const val TABLE_PREFIX_LINEAR = "linear"
213         const val LOG_BUFFER_BRIGHTNESS_CHANGE_TAG = "BrightnessChange"
214     }
215 }
216