1 /*
2  * Copyright (C) 2023 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 @file:OptIn(ExperimentalCoroutinesApi::class)
17 
18 package com.android.systemui.common.ui.data.repository
19 
20 import android.content.Context
21 import android.content.res.Configuration
22 import android.view.DisplayInfo
23 import androidx.annotation.DimenRes
24 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
25 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
26 import com.android.systemui.dagger.SysUISingleton
27 import com.android.systemui.dagger.qualifiers.Application
28 import com.android.systemui.statusbar.policy.ConfigurationController
29 import com.android.systemui.util.wrapper.DisplayUtilsWrapper
30 import dagger.Binds
31 import dagger.Module
32 import javax.inject.Inject
33 import kotlinx.coroutines.CoroutineScope
34 import kotlinx.coroutines.ExperimentalCoroutinesApi
35 import kotlinx.coroutines.channels.awaitClose
36 import kotlinx.coroutines.flow.Flow
37 import kotlinx.coroutines.flow.MutableStateFlow
38 import kotlinx.coroutines.flow.SharingStarted
39 import kotlinx.coroutines.flow.StateFlow
40 import kotlinx.coroutines.flow.distinctUntilChanged
41 import kotlinx.coroutines.flow.mapLatest
42 import kotlinx.coroutines.flow.stateIn
43 
44 interface ConfigurationRepository {
45     /** Called whenever ui mode, theme or configuration has changed. */
46     val onAnyConfigurationChange: Flow<Unit>
47 
48     /** Called whenever the configuration has changed. */
49     val onConfigurationChange: Flow<Unit>
50 
51     val scaleForResolution: Flow<Float>
52     val configurationValues: Flow<Configuration>
53 
getResolutionScalenull54     fun getResolutionScale(): Float
55 
56     /** Convenience to context.resources.getDimensionPixelSize() */
57     fun getDimensionPixelSize(id: Int): Int
58 }
59 
60 @SysUISingleton
61 class ConfigurationRepositoryImpl
62 @Inject
63 constructor(
64     private val configurationController: ConfigurationController,
65     private val context: Context,
66     @Application private val scope: CoroutineScope,
67     private val displayUtils: DisplayUtilsWrapper,
68 ) : ConfigurationRepository {
69     private val displayInfo = MutableStateFlow(DisplayInfo())
70 
71     override val onAnyConfigurationChange: Flow<Unit> =
72         conflatedCallbackFlow {
73             val callback =
74                 object : ConfigurationController.ConfigurationListener {
75                     override fun onUiModeChanged() {
76                         sendUpdate("ConfigurationRepository#onUiModeChanged")
77                     }
78 
79                     override fun onThemeChanged() {
80                         sendUpdate("ConfigurationRepository#onThemeChanged")
81                     }
82 
83                     override fun onConfigChanged(newConfig: Configuration) {
84                         sendUpdate("ConfigurationRepository#onConfigChanged")
85                     }
86 
87                     fun sendUpdate(reason: String) {
88                         trySendWithFailureLogging(Unit, reason)
89                     }
90                 }
91             configurationController.addCallback(callback)
92             awaitClose { configurationController.removeCallback(callback) }
93         }
94 
95     override val onConfigurationChange: Flow<Unit> =
96         conflatedCallbackFlow {
97             val callback =
98                 object : ConfigurationController.ConfigurationListener {
99                     override fun onConfigChanged(newConfig: Configuration) {
100                         trySendWithFailureLogging(Unit, "ConfigurationRepository#onConfigChanged")
101                     }
102                 }
103             configurationController.addCallback(callback)
104             awaitClose { configurationController.removeCallback(callback) }
105         }
106 
107     override val configurationValues: Flow<Configuration> =
108             conflatedCallbackFlow {
109                 val callback =
110                         object : ConfigurationController.ConfigurationListener {
111                             override fun onConfigChanged(newConfig: Configuration) {
112                                 trySend(newConfig)
113                             }
114                         }
115 
116                 trySend(context.resources.configuration)
117                 configurationController.addCallback(callback)
118                 awaitClose { configurationController.removeCallback(callback) }
119             }
120 
121     override val scaleForResolution: StateFlow<Float> =
122         onConfigurationChange
123             .mapLatest { getResolutionScale() }
124             .distinctUntilChanged()
125             .stateIn(scope, SharingStarted.WhileSubscribed(), getResolutionScale())
126 
127     override fun getResolutionScale(): Float {
128         context.display?.getDisplayInfo(displayInfo.value)
129         val maxDisplayMode =
130             displayUtils.getMaximumResolutionDisplayMode(displayInfo.value.supportedModes)
131         maxDisplayMode?.let {
132             val scaleFactor =
133                 displayUtils.getPhysicalPixelDisplaySizeRatio(
134                     maxDisplayMode.physicalWidth,
135                     maxDisplayMode.physicalHeight,
136                     displayInfo.value.naturalWidth,
137                     displayInfo.value.naturalHeight
138                 )
139             return if (scaleFactor == Float.POSITIVE_INFINITY) 1f else scaleFactor
140         }
141         return 1f
142     }
143 
144     override fun getDimensionPixelSize(@DimenRes id: Int): Int {
145         return context.resources.getDimensionPixelSize(id)
146     }
147 }
148 
149 @Module
150 interface ConfigurationRepositoryModule {
bindImplnull151     @Binds fun bindImpl(impl: ConfigurationRepositoryImpl): ConfigurationRepository
152 }
153