• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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