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