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