• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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 
17 @file:OptIn(ExperimentalKairosApi::class)
18 
19 package com.android.systemui.shade.ui.viewmodel
20 
21 import android.content.Context
22 import android.content.Intent
23 import android.icu.text.DateFormat
24 import android.icu.text.DisplayContext
25 import android.provider.Settings
26 import android.view.ViewGroup
27 import androidx.compose.material3.ColorScheme
28 import androidx.compose.runtime.getValue
29 import androidx.compose.ui.graphics.Color
30 import com.android.app.tracing.coroutines.launchTraced as launch
31 import com.android.compose.animation.scene.OverlayKey
32 import com.android.systemui.battery.BatteryMeterViewController
33 import com.android.systemui.kairos.ExperimentalKairosApi
34 import com.android.systemui.kairos.KairosNetwork
35 import com.android.systemui.lifecycle.ExclusiveActivatable
36 import com.android.systemui.lifecycle.Hydrator
37 import com.android.systemui.plugins.ActivityStarter
38 import com.android.systemui.privacy.OngoingPrivacyChip
39 import com.android.systemui.privacy.PrivacyItem
40 import com.android.systemui.res.R
41 import com.android.systemui.scene.domain.interactor.SceneInteractor
42 import com.android.systemui.scene.shared.model.Overlays
43 import com.android.systemui.scene.shared.model.TransitionKeys.SlightlyFasterShadeCollapse
44 import com.android.systemui.shade.ShadeDisplayAware
45 import com.android.systemui.shade.domain.interactor.PrivacyChipInteractor
46 import com.android.systemui.shade.domain.interactor.ShadeHeaderClockInteractor
47 import com.android.systemui.shade.domain.interactor.ShadeInteractor
48 import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
49 import com.android.systemui.statusbar.phone.StatusBarLocation
50 import com.android.systemui.statusbar.phone.ui.StatusBarIconController
51 import com.android.systemui.statusbar.phone.ui.TintedIconManager
52 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
53 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
54 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModelKairos
55 import dagger.assisted.AssistedFactory
56 import dagger.assisted.AssistedInject
57 import java.util.Locale
58 import kotlinx.coroutines.ExperimentalCoroutinesApi
59 import kotlinx.coroutines.awaitCancellation
60 import kotlinx.coroutines.coroutineScope
61 import kotlinx.coroutines.flow.Flow
62 import kotlinx.coroutines.flow.StateFlow
63 import kotlinx.coroutines.flow.combine
64 import kotlinx.coroutines.flow.map
65 import kotlinx.coroutines.flow.mapLatest
66 
67 /** Models UI state for the shade header. */
68 @OptIn(ExperimentalCoroutinesApi::class)
69 class ShadeHeaderViewModel
70 @AssistedInject
71 constructor(
72     @ShadeDisplayAware context: Context,
73     private val activityStarter: ActivityStarter,
74     private val sceneInteractor: SceneInteractor,
75     private val shadeInteractor: ShadeInteractor,
76     private val shadeModeInteractor: ShadeModeInteractor,
77     mobileIconsInteractor: MobileIconsInteractor,
78     val mobileIconsViewModel: MobileIconsViewModel,
79     private val privacyChipInteractor: PrivacyChipInteractor,
80     private val clockInteractor: ShadeHeaderClockInteractor,
81     private val tintedIconManagerFactory: TintedIconManager.Factory,
82     private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory,
83     val statusBarIconController: StatusBarIconController,
84     val kairosNetwork: KairosNetwork,
85     val mobileIconsViewModelKairos: dagger.Lazy<MobileIconsViewModelKairos>,
86 ) : ExclusiveActivatable() {
87 
88     private val hydrator = Hydrator("ShadeHeaderViewModel.hydrator")
89 
90     val createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager =
91         tintedIconManagerFactory::create
92 
93     val createBatteryMeterViewController:
94         (ViewGroup, StatusBarLocation) -> BatteryMeterViewController =
95         batteryMeterViewControllerFactory::create
96 
97     val showClock: Boolean by
98         hydrator.hydratedStateOf(
99             traceName = "showClock",
100             initialValue =
101                 shouldShowClock(
102                     isShadeLayoutWide = shadeModeInteractor.isShadeLayoutWide.value,
103                     overlays = sceneInteractor.currentOverlays.value,
104                 ),
105             source =
106                 combine(
107                     shadeModeInteractor.isShadeLayoutWide,
108                     sceneInteractor.currentOverlays,
109                     ::shouldShowClock,
110                 ),
111         )
112 
113     val notificationsChipHighlight: HeaderChipHighlight by
114         hydrator.hydratedStateOf(
115             traceName = "notificationsChipHighlight",
116             initialValue = HeaderChipHighlight.None,
117             source =
118                 sceneInteractor.currentOverlays.map { overlays ->
119                     when {
120                         Overlays.NotificationsShade in overlays -> HeaderChipHighlight.Strong
121                         Overlays.QuickSettingsShade in overlays -> HeaderChipHighlight.Weak
122                         else -> HeaderChipHighlight.None
123                     }
124                 },
125         )
126 
127     val quickSettingsChipHighlight: HeaderChipHighlight by
128         hydrator.hydratedStateOf(
129             traceName = "quickSettingsChipHighlight",
130             initialValue = HeaderChipHighlight.None,
131             source =
132                 sceneInteractor.currentOverlays.map { overlays ->
133                     when {
134                         Overlays.QuickSettingsShade in overlays -> HeaderChipHighlight.Strong
135                         Overlays.NotificationsShade in overlays -> HeaderChipHighlight.Weak
136                         else -> HeaderChipHighlight.None
137                     }
138                 },
139         )
140 
141     /** True if there is exactly one mobile connection. */
142     val isSingleCarrier: StateFlow<Boolean> = mobileIconsInteractor.isSingleCarrier
143 
144     /** The list of subscription Ids for current mobile connections. */
145     val mobileSubIds: List<Int> by
146         hydrator.hydratedStateOf(
147             traceName = "mobileSubIds",
148             initialValue = emptyList(),
149             source =
150                 mobileIconsInteractor.filteredSubscriptions.map { list ->
151                     list.map { it.subscriptionId }
152                 },
153         )
154 
155     /** The list of PrivacyItems to be displayed by the privacy chip. */
156     val privacyItems: StateFlow<List<PrivacyItem>> = privacyChipInteractor.privacyItems
157 
158     /** Whether or not mic & camera indicators are enabled in the device privacy config. */
159     val isMicCameraIndicationEnabled: StateFlow<Boolean> =
160         privacyChipInteractor.isMicCameraIndicationEnabled
161 
162     /** Whether or not location indicators are enabled in the device privacy config. */
163     val isLocationIndicationEnabled: StateFlow<Boolean> =
164         privacyChipInteractor.isLocationIndicationEnabled
165 
166     /** Whether or not the privacy chip should be visible. */
167     val isPrivacyChipVisible: StateFlow<Boolean> = privacyChipInteractor.isChipVisible
168 
169     /** Whether or not the privacy chip is enabled in the device privacy config. */
170     val isPrivacyChipEnabled: StateFlow<Boolean> = privacyChipInteractor.isChipEnabled
171 
172     private val longerPattern = context.getString(R.string.abbrev_wday_month_day_no_year_alarm)
173     private val shorterPattern = context.getString(R.string.abbrev_month_day_no_year)
174 
175     private val longerDateFormat: Flow<DateFormat> =
176         clockInteractor.onTimezoneOrLocaleChanged.mapLatest { getFormatFromPattern(longerPattern) }
177     private val shorterDateFormat: Flow<DateFormat> =
178         clockInteractor.onTimezoneOrLocaleChanged.mapLatest { getFormatFromPattern(shorterPattern) }
179 
180     val longerDateText: String by
181         hydrator.hydratedStateOf(
182             traceName = "longerDateText",
183             initialValue = "",
184             source =
185                 combine(longerDateFormat, clockInteractor.currentTime) { format, time ->
186                     format.format(time)
187                 },
188         )
189 
190     val shorterDateText: String by
191         hydrator.hydratedStateOf(
192             traceName = "shorterDateText",
193             initialValue = "",
194             source =
195                 combine(shorterDateFormat, clockInteractor.currentTime) { format, time ->
196                     format.format(time)
197                 },
198         )
199 
200     override suspend fun onActivated(): Nothing {
201         coroutineScope {
202             launch { hydrator.activate() }
203 
204             awaitCancellation()
205         }
206     }
207 
208     /** Notifies that the privacy chip was clicked. */
209     fun onPrivacyChipClicked(privacyChip: OngoingPrivacyChip) {
210         privacyChipInteractor.onPrivacyChipClicked(privacyChip)
211     }
212 
213     /** Notifies that the clock was clicked. */
214     fun onClockClicked() {
215         clockInteractor.launchClockActivity()
216     }
217 
218     /** Notifies that the system icons container was clicked. */
219     fun onNotificationIconChipClicked() {
220         if (!shadeModeInteractor.isDualShade) {
221             return
222         }
223         val loggingReason = "ShadeHeaderViewModel.onNotificationIconChipClicked"
224         val currentOverlays = sceneInteractor.currentOverlays.value
225         if (Overlays.NotificationsShade in currentOverlays) {
226             shadeInteractor.collapseNotificationsShade(
227                 loggingReason = loggingReason,
228                 transitionKey = SlightlyFasterShadeCollapse,
229             )
230         } else {
231             shadeInteractor.expandNotificationsShade(loggingReason)
232         }
233     }
234 
235     /** Notifies that the system icons container was clicked. */
236     fun onSystemIconChipClicked() {
237         val loggingReason = "ShadeHeaderViewModel.onSystemIconChipClicked"
238         if (shadeModeInteractor.isDualShade) {
239             val currentOverlays = sceneInteractor.currentOverlays.value
240             if (Overlays.QuickSettingsShade in currentOverlays) {
241                 shadeInteractor.collapseQuickSettingsShade(
242                     loggingReason = loggingReason,
243                     transitionKey = SlightlyFasterShadeCollapse,
244                 )
245             } else {
246                 shadeInteractor.expandQuickSettingsShade(loggingReason)
247             }
248         } else {
249             shadeInteractor.collapseEitherShade(
250                 loggingReason = loggingReason,
251                 transitionKey = SlightlyFasterShadeCollapse,
252             )
253         }
254     }
255 
256     /** Notifies that the shadeCarrierGroup was clicked. */
257     fun onShadeCarrierGroupClicked() {
258         activityStarter.postStartActivityDismissingKeyguard(
259             Intent(Settings.ACTION_WIRELESS_SETTINGS),
260             0,
261         )
262     }
263 
264     /** Represents the background highlight of a header icons chip. */
265     sealed interface HeaderChipHighlight {
266 
267         fun backgroundColor(colorScheme: ColorScheme): Color
268 
269         fun foregroundColor(colorScheme: ColorScheme): Color
270 
271         data object None : HeaderChipHighlight {
272             override fun backgroundColor(colorScheme: ColorScheme): Color = Color.Unspecified
273 
274             override fun foregroundColor(colorScheme: ColorScheme): Color = colorScheme.primary
275         }
276 
277         data object Weak : HeaderChipHighlight {
278             override fun backgroundColor(colorScheme: ColorScheme): Color =
279                 colorScheme.surface.copy(alpha = 0.1f)
280 
281             override fun foregroundColor(colorScheme: ColorScheme): Color = colorScheme.onSurface
282         }
283 
284         data object Strong : HeaderChipHighlight {
285             override fun backgroundColor(colorScheme: ColorScheme): Color =
286                 colorScheme.primaryContainer
287 
288             override fun foregroundColor(colorScheme: ColorScheme): Color =
289                 colorScheme.onPrimaryContainer
290         }
291     }
292 
293     private fun shouldShowClock(isShadeLayoutWide: Boolean, overlays: Set<OverlayKey>): Boolean {
294         // Notifications shade on narrow layout renders its own clock. Hide the header clock.
295         return isShadeLayoutWide || Overlays.NotificationsShade !in overlays
296     }
297 
298     private fun getFormatFromPattern(pattern: String?): DateFormat {
299         val format = DateFormat.getInstanceForSkeleton(pattern, Locale.getDefault())
300         format.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE)
301         return format
302     }
303 
304     @AssistedFactory
305     interface Factory {
306         fun create(): ShadeHeaderViewModel
307     }
308 }
309