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.ui.viewmodel 18 19 import android.content.Context 20 import androidx.compose.runtime.getValue 21 import androidx.compose.ui.unit.dp 22 import com.android.systemui.common.shared.model.ContentDescription 23 import com.android.systemui.dagger.qualifiers.Application 24 import com.android.systemui.lifecycle.ExclusiveActivatable 25 import com.android.systemui.lifecycle.Hydrator 26 import com.android.systemui.res.R 27 import com.android.systemui.statusbar.pipeline.battery.domain.interactor.BatteryAttributionModel.Charging 28 import com.android.systemui.statusbar.pipeline.battery.domain.interactor.BatteryAttributionModel.Defend 29 import com.android.systemui.statusbar.pipeline.battery.domain.interactor.BatteryAttributionModel.PowerSave 30 import com.android.systemui.statusbar.pipeline.battery.domain.interactor.BatteryInteractor 31 import com.android.systemui.statusbar.pipeline.battery.shared.ui.BatteryColors 32 import com.android.systemui.statusbar.pipeline.battery.shared.ui.BatteryFrame 33 import com.android.systemui.statusbar.pipeline.battery.shared.ui.BatteryGlyph 34 import com.android.systemui.statusbar.pipeline.battery.ui.model.AttributionGlyph 35 import dagger.assisted.AssistedFactory 36 import dagger.assisted.AssistedInject 37 import kotlinx.coroutines.ExperimentalCoroutinesApi 38 import kotlinx.coroutines.flow.Flow 39 import kotlinx.coroutines.flow.combine 40 import kotlinx.coroutines.flow.flatMapLatest 41 import kotlinx.coroutines.flow.map 42 43 /** View-model for the unified, compose-based battery icon. */ 44 @OptIn(ExperimentalCoroutinesApi::class) 45 class BatteryViewModel 46 @AssistedInject 47 constructor(interactor: BatteryInteractor, @Application context: Context) : ExclusiveActivatable() { 48 private val hydrator: Hydrator = Hydrator("BatteryViewModel.hydrator") 49 50 val batteryFrame = BatteryFrame.pathSpec 51 val innerWidth = BatteryFrame.innerWidth 52 val innerHeight = BatteryFrame.innerHeight 53 val aspectRatio = BatteryFrame.innerWidth / BatteryFrame.innerHeight 54 55 val level by 56 hydrator.hydratedStateOf(traceName = "level", initialValue = 0, source = interactor.level) 57 58 val isFull by 59 hydrator.hydratedStateOf( 60 traceName = "isFull", 61 initialValue = false, 62 source = interactor.isFull, 63 ) 64 65 /** The current attribution, if any */ 66 private val attributionGlyph: Flow<AttributionGlyph?> = 67 interactor.batteryAttributionType.map { 68 when (it) { 69 Charging -> 70 AttributionGlyph( 71 inline = BatteryGlyph.Bolt, 72 standalone = BatteryGlyph.BoltLarge, 73 ) 74 75 PowerSave -> 76 AttributionGlyph( 77 inline = BatteryGlyph.Plus, 78 standalone = BatteryGlyph.PlusLarge, 79 ) 80 81 Defend -> 82 AttributionGlyph( 83 inline = BatteryGlyph.Defend, 84 standalone = BatteryGlyph.DefendLarge, 85 ) 86 87 else -> null 88 } 89 } 90 91 /** A [List<BatteryGlyph>] representation of the current [level] */ 92 private val levelGlyphs: Flow<List<BatteryGlyph>> = 93 interactor.level.map { it.glyphRepresentation() } 94 95 private val _glyphList: Flow<List<BatteryGlyph>> = 96 interactor.isBatteryPercentSettingEnabled.flatMapLatest { 97 if (it) { 98 combine(interactor.isFull, levelGlyphs, attributionGlyph) { 99 isFull, 100 levelGlyphs, 101 attr -> 102 // Don't ever show "100<attr>", since it won't fit. Just show the attr 103 if (isFull && attr != null) { 104 listOf(attr.standalone) 105 } else if (attr != null) { 106 levelGlyphs + attr.inline 107 } else { 108 levelGlyphs 109 } 110 } 111 } else { 112 attributionGlyph.map { attr -> 113 if (attr == null) { 114 emptyList() 115 } else { 116 listOf(attr.standalone) 117 } 118 } 119 } 120 } 121 122 /** For the status bar battery, this is the complete set of glyphs to show */ 123 val glyphList: List<BatteryGlyph> by 124 hydrator.hydratedStateOf( 125 traceName = "glyphList", 126 initialValue = emptyList(), 127 source = _glyphList, 128 ) 129 130 private val _colorProfile: Flow<ColorProfile> = 131 combine(interactor.batteryAttributionType, interactor.isCritical) { attr, isCritical -> 132 when (attr) { 133 Charging, 134 Defend -> 135 ColorProfile( 136 dark = BatteryColors.DarkThemeChargingColors, 137 light = BatteryColors.LightThemeChargingColors, 138 ) 139 PowerSave -> 140 ColorProfile( 141 dark = BatteryColors.DarkThemePowerSaveColors, 142 light = BatteryColors.LightThemePowerSaveColors, 143 ) 144 else -> 145 if (isCritical) { 146 ColorProfile( 147 dark = BatteryColors.DarkThemeErrorColors, 148 light = BatteryColors.LightThemeErrorColors, 149 ) 150 } else { 151 ColorProfile( 152 dark = BatteryColors.DarkThemeDefaultColors, 153 light = BatteryColors.LightThemeDefaultColors, 154 ) 155 } 156 } 157 } 158 159 /** For the current battery state, what is the relevant color profile to use */ 160 val colorProfile: ColorProfile by 161 hydrator.hydratedStateOf( 162 traceName = "colorProfile", 163 initialValue = 164 ColorProfile( 165 dark = BatteryColors.DarkThemeDefaultColors, 166 light = BatteryColors.LightThemeDefaultColors, 167 ), 168 source = _colorProfile, 169 ) 170 171 val contentDescription: ContentDescription by 172 hydrator.hydratedStateOf( 173 traceName = "contentDescription", 174 initialValue = ContentDescription.Loaded(null), 175 source = 176 combine( 177 interactor.batteryAttributionType, 178 interactor.isStateUnknown, 179 interactor.level, 180 ) { attr, isUnknown, level -> 181 when { 182 isUnknown -> 183 ContentDescription.Resource(R.string.accessibility_battery_unknown) 184 attr == Defend -> { 185 val descr = 186 context.getString( 187 R.string.accessibility_battery_level_charging_paused, 188 level, 189 ) 190 191 ContentDescription.Loaded(descr) 192 } 193 attr == Charging -> { 194 val descr = 195 context.getString( 196 R.string.accessibility_battery_level_charging, 197 level, 198 ) 199 ContentDescription.Loaded(descr) 200 } 201 else -> { 202 val descr = 203 context.getString(R.string.accessibility_battery_level, level) 204 ContentDescription.Loaded(descr) 205 } 206 } 207 }, 208 ) 209 210 val batteryTimeRemainingEstimate: String? by 211 hydrator.hydratedStateOf( 212 traceName = "timeRemainingEstimate", 213 initialValue = null, 214 source = interactor.batteryTimeRemainingEstimate, 215 ) 216 217 override suspend fun onActivated(): Nothing { 218 hydrator.activate() 219 } 220 221 @AssistedFactory 222 interface Factory { 223 fun create(): BatteryViewModel 224 } 225 226 companion object { 227 // Status bar battery height, based on a 21x12 base canvas 228 val STATUS_BAR_BATTERY_HEIGHT = 13.dp 229 val STATUS_BAR_BATTERY_WIDTH = 22.75.dp 230 231 fun Int.glyphRepresentation(): List<BatteryGlyph> = toString().map { it.toGlyph() } 232 233 private fun Char.toGlyph(): BatteryGlyph = 234 when (this) { 235 '0' -> BatteryGlyph.Zero 236 '1' -> BatteryGlyph.One 237 '2' -> BatteryGlyph.Two 238 '3' -> BatteryGlyph.Three 239 '4' -> BatteryGlyph.Four 240 '5' -> BatteryGlyph.Five 241 '6' -> BatteryGlyph.Six 242 '7' -> BatteryGlyph.Seven 243 '8' -> BatteryGlyph.Eight 244 '9' -> BatteryGlyph.Nine 245 else -> throw IllegalArgumentException("cannot make glyph from char ($this)") 246 } 247 } 248 } 249 250 /** Wrap the light and dark color into a single object so the view can decide which one it needs */ 251 data class ColorProfile(val dark: BatteryColors, val light: BatteryColors) 252