1 /* <lambda>null2 * Copyright (C) 2025 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.statusbar.pipeline.battery.data.repository 18 19 import android.content.Context 20 import android.provider.Settings 21 import com.android.systemui.dagger.SysUISingleton 22 import com.android.systemui.dagger.qualifiers.Application 23 import com.android.systemui.dagger.qualifiers.Background 24 import com.android.systemui.shared.settings.data.repository.SystemSettingsRepository 25 import com.android.systemui.statusbar.policy.BatteryController 26 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow 27 import javax.inject.Inject 28 import kotlin.coroutines.resume 29 import kotlin.time.Duration.Companion.minutes 30 import kotlinx.coroutines.CoroutineDispatcher 31 import kotlinx.coroutines.CoroutineScope 32 import kotlinx.coroutines.channels.awaitClose 33 import kotlinx.coroutines.delay 34 import kotlinx.coroutines.flow.Flow 35 import kotlinx.coroutines.flow.SharingStarted 36 import kotlinx.coroutines.flow.StateFlow 37 import kotlinx.coroutines.flow.flow 38 import kotlinx.coroutines.flow.flowOn 39 import kotlinx.coroutines.flow.map 40 import kotlinx.coroutines.flow.scan 41 import kotlinx.coroutines.flow.stateIn 42 import kotlinx.coroutines.suspendCancellableCoroutine 43 44 /** 45 * Repository-style state for battery information. Currently we just use the [BatteryController] as 46 * our source of truth, but we could (should?) migrate away from that eventually. 47 */ 48 @SysUISingleton 49 class BatteryRepository 50 @Inject 51 constructor( 52 @Application context: Context, 53 @Background scope: CoroutineScope, 54 @Background bgDispatcher: CoroutineDispatcher, 55 private val controller: BatteryController, 56 settingsRepository: SystemSettingsRepository, 57 ) { 58 private val batteryState: StateFlow<BatteryCallbackState> = 59 conflatedCallbackFlow<(BatteryCallbackState) -> BatteryCallbackState> { 60 val callback = 61 object : BatteryController.BatteryStateChangeCallback { 62 override fun onBatteryLevelChanged( 63 level: Int, 64 pluggedIn: Boolean, 65 charging: Boolean, 66 ) { 67 trySend { prev -> prev.copy(level = level, isPluggedIn = pluggedIn) } 68 } 69 70 override fun onPowerSaveChanged(isPowerSave: Boolean) { 71 trySend { prev -> prev.copy(isPowerSaveEnabled = isPowerSave) } 72 } 73 74 override fun onIsBatteryDefenderChanged(isBatteryDefender: Boolean) { 75 trySend { prev -> 76 prev.copy(isBatteryDefenderEnabled = isBatteryDefender) 77 } 78 } 79 80 override fun onBatteryUnknownStateChanged(isUnknown: Boolean) { 81 // If the state is unknown, then all other fields are invalid 82 trySend { prev -> 83 if (isUnknown) { 84 // Forget everything before now 85 BatteryCallbackState(isStateUnknown = true) 86 } else { 87 prev.copy(isStateUnknown = false) 88 } 89 } 90 } 91 } 92 93 controller.addCallback(callback) 94 awaitClose { controller.removeCallback(callback) } 95 } 96 .scan(initial = BatteryCallbackState()) { state, eventF -> eventF(state) } 97 .flowOn(bgDispatcher) 98 .stateIn(scope, SharingStarted.Lazily, BatteryCallbackState()) 99 100 /** 101 * True if the phone is plugged in. Note that this does not always mean the device is charging 102 */ 103 val isPluggedIn = batteryState.map { it.isPluggedIn } 104 105 /** Is power saver enabled */ 106 val isPowerSaveEnabled = batteryState.map { it.isPowerSaveEnabled } 107 108 /** Battery defender means the device is plugged in but not charging to protect the battery */ 109 val isBatteryDefenderEnabled = batteryState.map { it.isBatteryDefenderEnabled } 110 111 /** The current level [0-100] */ 112 val level = batteryState.map { it.level } 113 114 /** State unknown means that we can't detect a battery */ 115 val isStateUnknown = batteryState.map { it.isStateUnknown } 116 117 /** 118 * [Settings.System.SHOW_BATTERY_PERCENT]. A user setting to indicate whether we should show the 119 * battery percentage in the home screen status bar 120 */ 121 val isShowBatteryPercentSettingEnabled = run { 122 val default = 123 context.resources.getBoolean( 124 com.android.internal.R.bool.config_defaultBatteryPercentageSetting 125 ) 126 settingsRepository 127 .boolSetting(name = Settings.System.SHOW_BATTERY_PERCENT, defaultValue = default) 128 .flowOn(bgDispatcher) 129 .stateIn(scope, SharingStarted.Lazily, default) 130 } 131 132 /** Get and re-fetch the estimate every 2 minutes while active */ 133 private val estimate: Flow<String?> = flow { 134 while (true) { 135 val estimate = fetchEstimate() 136 emit(estimate) 137 delay(2.minutes) 138 } 139 } 140 141 /** 142 * If available, this flow yields a string that describes the approximate time remaining for the 143 * current battery charge and usage information. While subscribed, the estimate is updated every 144 * 2 minutes. 145 */ 146 val batteryTimeRemainingEstimate: Flow<String?> = estimate.flowOn(bgDispatcher) 147 148 private suspend fun fetchEstimate() = suspendCancellableCoroutine { continuation -> 149 val callback = 150 BatteryController.EstimateFetchCompletion { estimate -> continuation.resume(estimate) } 151 152 controller.getEstimatedTimeRemainingString(callback) 153 } 154 } 155 156 /** Data object to track the current battery callback state */ 157 private data class BatteryCallbackState( 158 val level: Int? = null, 159 val isPluggedIn: Boolean = false, 160 val isPowerSaveEnabled: Boolean = false, 161 val isBatteryDefenderEnabled: Boolean = false, 162 val isStateUnknown: Boolean = false, 163 ) 164