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 package com.android.systemui.common.ui.data.repository 17 18 import android.content.Context 19 import android.content.res.Configuration 20 import android.view.Display 21 import android.view.DisplayInfo 22 import androidx.annotation.DimenRes 23 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging 24 import com.android.systemui.dagger.SysUISingleton 25 import com.android.systemui.dagger.qualifiers.Application 26 import com.android.systemui.dagger.qualifiers.Main 27 import com.android.systemui.statusbar.policy.ConfigurationController 28 import com.android.systemui.util.wrapper.DisplayUtilsWrapper 29 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow 30 import dagger.Binds 31 import dagger.Module 32 import dagger.Provides 33 import dagger.assisted.Assisted 34 import dagger.assisted.AssistedFactory 35 import dagger.assisted.AssistedInject 36 import kotlinx.coroutines.CoroutineScope 37 import kotlinx.coroutines.channels.awaitClose 38 import kotlinx.coroutines.flow.Flow 39 import kotlinx.coroutines.flow.MutableStateFlow 40 import kotlinx.coroutines.flow.SharingStarted 41 import kotlinx.coroutines.flow.StateFlow 42 import kotlinx.coroutines.flow.distinctUntilChanged 43 import kotlinx.coroutines.flow.mapLatest 44 import kotlinx.coroutines.flow.stateIn 45 46 interface ConfigurationRepository { 47 /** Called whenever ui mode, theme or configuration has changed. */ 48 val onAnyConfigurationChange: Flow<Unit> 49 50 /** Called whenever the configuration has changed. */ 51 val onConfigurationChange: Flow<Unit> 52 53 val scaleForResolution: StateFlow<Float> 54 55 val configurationValues: Flow<Configuration> 56 57 /** Emits the latest display this configuration controller has been moved to. */ 58 val onMovedToDisplay: StateFlow<Int> 59 getResolutionScalenull60 fun getResolutionScale(): Float 61 62 /** Convenience to context.resources.getDimensionPixelSize() */ 63 fun getDimensionPixelSize(id: Int): Int 64 } 65 66 class ConfigurationRepositoryImpl 67 @AssistedInject 68 constructor( 69 @Assisted private val configurationController: ConfigurationController, 70 @Assisted private val context: Context, 71 @Application private val scope: CoroutineScope, 72 private val displayUtils: DisplayUtilsWrapper, 73 ) : ConfigurationRepository { 74 private val displayInfo = MutableStateFlow(DisplayInfo()) 75 76 override val onAnyConfigurationChange: Flow<Unit> = conflatedCallbackFlow { 77 val callback = 78 object : ConfigurationController.ConfigurationListener { 79 override fun onUiModeChanged() { 80 sendUpdate("ConfigurationRepository#onUiModeChanged") 81 } 82 83 override fun onThemeChanged() { 84 sendUpdate("ConfigurationRepository#onThemeChanged") 85 } 86 87 override fun onConfigChanged(newConfig: Configuration) { 88 sendUpdate("ConfigurationRepository#onConfigChanged") 89 } 90 91 fun sendUpdate(reason: String) { 92 trySendWithFailureLogging(Unit, reason) 93 } 94 } 95 configurationController.addCallback(callback) 96 awaitClose { configurationController.removeCallback(callback) } 97 } 98 99 override val onConfigurationChange: Flow<Unit> = conflatedCallbackFlow { 100 val callback = 101 object : ConfigurationController.ConfigurationListener { 102 override fun onConfigChanged(newConfig: Configuration) { 103 trySendWithFailureLogging(Unit, "ConfigurationRepository#onConfigChanged") 104 } 105 } 106 configurationController.addCallback(callback) 107 awaitClose { configurationController.removeCallback(callback) } 108 } 109 110 override val configurationValues: Flow<Configuration> = conflatedCallbackFlow { 111 val callback = 112 object : ConfigurationController.ConfigurationListener { 113 override fun onConfigChanged(newConfig: Configuration) { 114 trySend(newConfig) 115 } 116 } 117 118 trySend(context.resources.configuration) 119 configurationController.addCallback(callback) 120 awaitClose { configurationController.removeCallback(callback) } 121 } 122 override val onMovedToDisplay: StateFlow<Int> = 123 conflatedCallbackFlow { 124 val callback = 125 object : ConfigurationController.ConfigurationListener { 126 override fun onMovedToDisplay( 127 newDisplayId: Int, 128 newConfiguration: Configuration?, 129 ) { 130 trySend(newDisplayId) 131 } 132 } 133 configurationController.addCallback(callback) 134 awaitClose { configurationController.removeCallback(callback) } 135 } 136 .stateIn(scope, SharingStarted.Eagerly, Display.DEFAULT_DISPLAY) 137 138 override val scaleForResolution: StateFlow<Float> = 139 onConfigurationChange 140 .mapLatest { getResolutionScale() } 141 .distinctUntilChanged() 142 .stateIn(scope, SharingStarted.WhileSubscribed(), getResolutionScale()) 143 144 override fun getResolutionScale(): Float { 145 context.display?.getDisplayInfo(displayInfo.value) 146 val maxDisplayMode = 147 displayUtils.getMaximumResolutionDisplayMode(displayInfo.value.supportedModes) 148 maxDisplayMode?.let { 149 val scaleFactor = 150 displayUtils.getPhysicalPixelDisplaySizeRatio( 151 maxDisplayMode.physicalWidth, 152 maxDisplayMode.physicalHeight, 153 displayInfo.value.naturalWidth, 154 displayInfo.value.naturalHeight, 155 ) 156 return if (scaleFactor == Float.POSITIVE_INFINITY) 1f else scaleFactor 157 } 158 return 1f 159 } 160 161 override fun getDimensionPixelSize(@DimenRes id: Int): Int { 162 return context.resources.getDimensionPixelSize(id) 163 } 164 165 @AssistedFactory 166 interface Factory { 167 fun create( 168 context: Context, 169 configurationController: ConfigurationController, 170 ): ConfigurationRepositoryImpl 171 } 172 } 173 174 @Module 175 abstract class ConfigurationRepositoryModule { 176 177 /** 178 * For compatibility reasons. Ideally, only the an annotated [ConfigurationRepository] should be 179 * injected. 180 */ 181 @Binds 182 @Deprecated("Use the ConfigurationRepository annotated with @Main instead.") 183 @SysUISingleton provideDefaultConfigRepositorynull184 abstract fun provideDefaultConfigRepository( 185 @Main configurationRepository: ConfigurationRepository 186 ): ConfigurationRepository 187 188 companion object { 189 @Provides 190 @Main 191 @SysUISingleton 192 fun provideGlobalConfigRepository( 193 context: Context, 194 @Main configurationController: ConfigurationController, 195 factory: ConfigurationRepositoryImpl.Factory, 196 ): ConfigurationRepository { 197 return factory.create(context, configurationController) 198 } 199 } 200 } 201