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
18 package com.android.systemui.shade.ui.composable
19
20 import android.view.ContextThemeWrapper
21 import android.view.ViewGroup
22 import androidx.compose.foundation.background
23 import androidx.compose.foundation.clickable
24 import androidx.compose.foundation.interaction.MutableInteractionSource
25 import androidx.compose.foundation.interaction.collectIsHoveredAsState
26 import androidx.compose.foundation.layout.Arrangement
27 import androidx.compose.foundation.layout.Box
28 import androidx.compose.foundation.layout.BoxScope
29 import androidx.compose.foundation.layout.Column
30 import androidx.compose.foundation.layout.Row
31 import androidx.compose.foundation.layout.RowScope
32 import androidx.compose.foundation.layout.Spacer
33 import androidx.compose.foundation.layout.defaultMinSize
34 import androidx.compose.foundation.layout.fillMaxWidth
35 import androidx.compose.foundation.layout.height
36 import androidx.compose.foundation.layout.padding
37 import androidx.compose.foundation.layout.width
38 import androidx.compose.foundation.layout.widthIn
39 import androidx.compose.foundation.shape.RoundedCornerShape
40 import androidx.compose.material3.ColorScheme
41 import androidx.compose.material3.MaterialTheme
42 import androidx.compose.runtime.Composable
43 import androidx.compose.runtime.derivedStateOf
44 import androidx.compose.runtime.getValue
45 import androidx.compose.runtime.remember
46 import androidx.compose.runtime.rememberCoroutineScope
47 import androidx.compose.ui.Alignment
48 import androidx.compose.ui.Modifier
49 import androidx.compose.ui.graphics.Color
50 import androidx.compose.ui.graphics.TransformOrigin
51 import androidx.compose.ui.graphics.graphicsLayer
52 import androidx.compose.ui.layout.Layout
53 import androidx.compose.ui.platform.LocalContext
54 import androidx.compose.ui.platform.LocalLayoutDirection
55 import androidx.compose.ui.res.stringResource
56 import androidx.compose.ui.unit.Constraints
57 import androidx.compose.ui.unit.LayoutDirection
58 import androidx.compose.ui.unit.dp
59 import androidx.compose.ui.unit.max
60 import androidx.compose.ui.viewinterop.AndroidView
61 import androidx.lifecycle.compose.collectAsStateWithLifecycle
62 import com.android.compose.animation.scene.ContentScope
63 import com.android.compose.animation.scene.ElementKey
64 import com.android.compose.animation.scene.LowestZIndexContentPicker
65 import com.android.compose.animation.scene.ValueKey
66 import com.android.compose.animation.scene.animateElementFloatAsState
67 import com.android.compose.animation.scene.content.state.TransitionState
68 import com.android.compose.modifiers.thenIf
69 import com.android.compose.theme.colorAttr
70 import com.android.settingslib.Utils
71 import com.android.systemui.Flags
72 import com.android.systemui.battery.BatteryMeterView
73 import com.android.systemui.battery.BatteryMeterViewController
74 import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
75 import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout
76 import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius
77 import com.android.systemui.compose.modifiers.sysuiResTag
78 import com.android.systemui.kairos.ExperimentalKairosApi
79 import com.android.systemui.kairos.buildSpec
80 import com.android.systemui.privacy.OngoingPrivacyChip
81 import com.android.systemui.res.R
82 import com.android.systemui.scene.shared.model.Scenes
83 import com.android.systemui.shade.ui.composable.ShadeHeader.Colors.onScrimDim
84 import com.android.systemui.shade.ui.composable.ShadeHeader.Dimensions.ChipPaddingHorizontal
85 import com.android.systemui.shade.ui.composable.ShadeHeader.Dimensions.ChipPaddingVertical
86 import com.android.systemui.shade.ui.composable.ShadeHeader.Dimensions.CollapsedHeight
87 import com.android.systemui.shade.ui.composable.ShadeHeader.Values.ClockScale
88 import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
89 import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel.HeaderChipHighlight
90 import com.android.systemui.statusbar.phone.StatusBarLocation
91 import com.android.systemui.statusbar.phone.StatusIconContainer
92 import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView
93 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModelKairosComposeWrapper
94 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel
95 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModelKairos
96 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.composeWrapper
97 import com.android.systemui.statusbar.policy.Clock
98 import com.android.systemui.util.composable.kairos.ActivatedKairosSpec
99
100 object ShadeHeader {
101 object Elements {
102 val ExpandedContent = ElementKey("ShadeHeaderExpandedContent")
103 val CollapsedContentStart = ElementKey("ShadeHeaderCollapsedContentStart")
104 val CollapsedContentEnd = ElementKey("ShadeHeaderCollapsedContentEnd")
105 val PrivacyChip = ElementKey("PrivacyChip", contentPicker = LowestZIndexContentPicker)
106 val Clock = ElementKey("ShadeHeaderClock", contentPicker = LowestZIndexContentPicker)
107 val ShadeCarrierGroup = ElementKey("ShadeCarrierGroup")
108 }
109
110 object Values {
111 val ClockScale = ValueKey("ShadeHeaderClockScale")
112 }
113
114 object Dimensions {
115 val CollapsedHeight = 48.dp
116 val ExpandedHeight = 120.dp
117 val ChipPaddingHorizontal = 6.dp
118 val ChipPaddingVertical = 4.dp
119 }
120
121 object Colors {
122 val ColorScheme.shadeHeaderText: Color
123 get() = Color.White
124
125 val ColorScheme.onScrimDim: Color
126 get() = Color.DarkGray
127 }
128
129 object TestTags {
130 const val Root = "shade_header_root"
131 }
132 }
133
134 /** The status bar that appears above the Shade scene on small screens */
135 @Composable
ContentScopenull136 fun ContentScope.CollapsedShadeHeader(
137 viewModel: ShadeHeaderViewModel,
138 isSplitShade: Boolean,
139 modifier: Modifier = Modifier,
140 ) {
141 val cutoutLocation = LocalDisplayCutout.current.location
142 val horizontalPadding =
143 max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding)
144
145 val useExpandedTextFormat by
146 remember(cutoutLocation) {
147 derivedStateOf {
148 cutoutLocation != CutoutLocation.CENTER ||
149 shouldUseExpandedFormat(layoutState.transitionState)
150 }
151 }
152
153 val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle()
154
155 // This layout assumes it is globally positioned at (0, 0) and is the same size as the screen.
156 CutoutAwareShadeHeader(
157 modifier = modifier.sysuiResTag(ShadeHeader.TestTags.Root),
158 startContent = {
159 Row(
160 verticalAlignment = Alignment.CenterVertically,
161 horizontalArrangement = Arrangement.spacedBy(5.dp),
162 modifier = Modifier.padding(horizontal = horizontalPadding),
163 ) {
164 Clock(onClick = viewModel::onClockClicked)
165 VariableDayDate(
166 longerDateText = viewModel.longerDateText,
167 shorterDateText = viewModel.shorterDateText,
168 textColor = colorAttr(android.R.attr.textColorPrimary),
169 modifier = Modifier.element(ShadeHeader.Elements.CollapsedContentStart),
170 )
171 }
172 },
173 endContent = {
174 if (isPrivacyChipVisible) {
175 Box(
176 modifier =
177 Modifier.height(CollapsedHeight)
178 .fillMaxWidth()
179 .padding(horizontal = horizontalPadding)
180 ) {
181 PrivacyChip(
182 viewModel = viewModel,
183 modifier = Modifier.align(Alignment.CenterEnd),
184 )
185 }
186 } else {
187 Row(
188 horizontalArrangement = Arrangement.End,
189 verticalAlignment = Alignment.CenterVertically,
190 modifier =
191 Modifier.element(ShadeHeader.Elements.CollapsedContentEnd)
192 .padding(horizontal = horizontalPadding),
193 ) {
194 if (isSplitShade) {
195 ShadeCarrierGroup(viewModel = viewModel)
196 }
197 SystemIconChip(
198 onClick = viewModel::onSystemIconChipClicked.takeIf { isSplitShade }
199 ) {
200 StatusIcons(
201 viewModel = viewModel,
202 useExpandedFormat = useExpandedTextFormat,
203 modifier = Modifier.padding(end = 6.dp).weight(1f, fill = false),
204 )
205 BatteryIcon(
206 createBatteryMeterViewController =
207 viewModel.createBatteryMeterViewController,
208 useExpandedFormat = useExpandedTextFormat,
209 modifier = Modifier.padding(vertical = 8.dp),
210 )
211 }
212 }
213 }
214 },
215 )
216 }
217
218 /** The status bar that appears above the Quick Settings scene on small screens */
219 @Composable
ExpandedShadeHeadernull220 fun ContentScope.ExpandedShadeHeader(
221 viewModel: ShadeHeaderViewModel,
222 modifier: Modifier = Modifier,
223 ) {
224 val useExpandedFormat by remember {
225 derivedStateOf { shouldUseExpandedFormat(layoutState.transitionState) }
226 }
227
228 val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle()
229
230 Box(modifier = modifier.sysuiResTag(ShadeHeader.TestTags.Root)) {
231 if (isPrivacyChipVisible) {
232 Box(modifier = Modifier.height(CollapsedHeight).fillMaxWidth()) {
233 PrivacyChip(viewModel = viewModel, modifier = Modifier.align(Alignment.CenterEnd))
234 }
235 }
236 Column(
237 verticalArrangement = Arrangement.Bottom,
238 modifier =
239 Modifier.fillMaxWidth()
240 .defaultMinSize(minHeight = ShadeHeader.Dimensions.ExpandedHeight),
241 ) {
242 Box(modifier = Modifier.fillMaxWidth()) {
243 Clock(
244 onClick = viewModel::onClockClicked,
245 modifier = Modifier.align(Alignment.CenterStart),
246 scale = 2.57f,
247 )
248 Box(
249 modifier =
250 Modifier.element(ShadeHeader.Elements.ShadeCarrierGroup).fillMaxWidth()
251 ) {
252 ShadeCarrierGroup(
253 viewModel = viewModel,
254 modifier = Modifier.align(Alignment.CenterEnd),
255 )
256 }
257 }
258 Spacer(modifier = Modifier.width(5.dp))
259 Row(
260 verticalAlignment = Alignment.CenterVertically,
261 modifier = Modifier.element(ShadeHeader.Elements.ExpandedContent),
262 ) {
263 VariableDayDate(
264 longerDateText = viewModel.longerDateText,
265 shorterDateText = viewModel.shorterDateText,
266 textColor = colorAttr(android.R.attr.textColorPrimary),
267 modifier = Modifier.widthIn(max = 90.dp),
268 )
269 Spacer(modifier = Modifier.weight(1f))
270 SystemIconChip {
271 StatusIcons(
272 viewModel = viewModel,
273 useExpandedFormat = useExpandedFormat,
274 modifier = Modifier.padding(end = 6.dp).weight(1f, fill = false),
275 )
276 BatteryIcon(
277 useExpandedFormat = useExpandedFormat,
278 createBatteryMeterViewController =
279 viewModel.createBatteryMeterViewController,
280 )
281 }
282 }
283 }
284 }
285 }
286
287 /**
288 * The status bar that appears above both the Notifications and Quick Settings shade overlays when
289 * overlay shade is enabled.
290 */
291 @Composable
ContentScopenull292 fun ContentScope.OverlayShadeHeader(
293 viewModel: ShadeHeaderViewModel,
294 modifier: Modifier = Modifier,
295 ) {
296 OverlayShadeHeaderPartialStateless(
297 viewModel,
298 viewModel.showClock,
299 modifier,
300 )
301 }
302
303 /**
304 * Ideally, we should have a stateless function for overlay shade header, which facilitates testing.
305 * However, it is cumbersome to implement such a stateless function, especially when some of the
306 * overlay shade header's children accept a view model as the param. Therefore, this function only
307 * break up the clock visibility. It is where "PartialStateless" comes from.
308 */
309 @Composable
ContentScopenull310 fun ContentScope.OverlayShadeHeaderPartialStateless(
311 viewModel: ShadeHeaderViewModel,
312 showClock: Boolean,
313 modifier: Modifier = Modifier,
314 ) {
315 val horizontalPadding =
316 max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding)
317
318 val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle()
319
320 // This layout assumes it is globally positioned at (0, 0) and is the same size as the screen.
321 CutoutAwareShadeHeader(
322 modifier = modifier.sysuiResTag(ShadeHeader.TestTags.Root),
323 startContent = {
324 Row(
325 verticalAlignment = Alignment.CenterVertically,
326 horizontalArrangement = Arrangement.spacedBy(5.dp),
327 modifier = Modifier.padding(horizontal = horizontalPadding),
328 ) {
329 val chipHighlight = viewModel.notificationsChipHighlight
330 if (showClock) {
331 Clock(
332 onClick = viewModel::onClockClicked,
333 modifier = Modifier.padding(horizontal = 4.dp),
334 )
335 }
336 NotificationsChip(
337 onClick = viewModel::onNotificationIconChipClicked,
338 backgroundColor = chipHighlight.backgroundColor(MaterialTheme.colorScheme),
339 ) {
340 VariableDayDate(
341 longerDateText = viewModel.longerDateText,
342 shorterDateText = viewModel.shorterDateText,
343 textColor = chipHighlight.foregroundColor(MaterialTheme.colorScheme),
344 )
345 }
346 }
347 },
348 endContent = {
349 Row(
350 horizontalArrangement = Arrangement.End,
351 verticalAlignment = Alignment.CenterVertically,
352 modifier = Modifier.padding(horizontal = horizontalPadding),
353 ) {
354 val chipHighlight = viewModel.quickSettingsChipHighlight
355 SystemIconChip(
356 backgroundColor = chipHighlight.backgroundColor(MaterialTheme.colorScheme),
357 onClick = viewModel::onSystemIconChipClicked,
358 ) {
359 StatusIcons(
360 viewModel = viewModel,
361 useExpandedFormat = false,
362 modifier = Modifier.padding(end = 6.dp).weight(1f, fill = false),
363 )
364 BatteryIcon(
365 createBatteryMeterViewController =
366 viewModel.createBatteryMeterViewController,
367 useExpandedFormat = false,
368 chipHighlight = chipHighlight,
369 )
370 }
371 if (isPrivacyChipVisible) {
372 Box(
373 modifier =
374 Modifier.height(CollapsedHeight)
375 .fillMaxWidth()
376 .padding(horizontal = horizontalPadding)
377 ) {
378 PrivacyChip(
379 viewModel = viewModel,
380 modifier = Modifier.align(Alignment.CenterEnd),
381 )
382 }
383 }
384 }
385 },
386 )
387 }
388
389 /** The header that appears at the top of the Quick Settings shade overlay. */
390 @Composable
QuickSettingsOverlayHeadernull391 fun QuickSettingsOverlayHeader(viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifier) {
392 Row(
393 horizontalArrangement = Arrangement.SpaceBetween,
394 verticalAlignment = Alignment.CenterVertically,
395 modifier = modifier.fillMaxWidth(),
396 ) {
397 ShadeCarrierGroup(viewModel = viewModel)
398 BatteryIcon(
399 createBatteryMeterViewController = viewModel.createBatteryMeterViewController,
400 useExpandedFormat = true,
401 )
402 }
403 }
404
405 /*
406 * Places startContent and endContent according to the location of the display cutout.
407 * Assumes it is globally positioned at (0, 0) and the same size as the screen.
408 */
409 @Composable
CutoutAwareShadeHeadernull410 private fun CutoutAwareShadeHeader(
411 modifier: Modifier = Modifier,
412 startContent: @Composable () -> Unit,
413 endContent: @Composable () -> Unit,
414 ) {
415 val cutoutWidth = LocalDisplayCutout.current.width()
416 val cutoutHeight = LocalDisplayCutout.current.height()
417 val cutoutTop = LocalDisplayCutout.current.top
418 val cutoutLocation = LocalDisplayCutout.current.location
419
420 Layout(
421 modifier = modifier.sysuiResTag(ShadeHeader.TestTags.Root),
422 contents = listOf(startContent, endContent),
423 ) { measurables, constraints ->
424 check(constraints.hasBoundedWidth)
425 check(measurables.size == 2)
426 check(measurables[0].size == 1)
427 check(measurables[1].size == 1)
428
429 val screenWidth = constraints.maxWidth
430 val cutoutWidthPx = cutoutWidth.roundToPx()
431 val height = max(cutoutHeight + (cutoutTop * 2), CollapsedHeight).roundToPx()
432 val childConstraints = Constraints.fixed((screenWidth - cutoutWidthPx) / 2, height)
433
434 val startMeasurable = measurables[0][0]
435 val endMeasurable = measurables[1][0]
436
437 val startPlaceable = startMeasurable.measure(childConstraints)
438 val endPlaceable = endMeasurable.measure(childConstraints)
439
440 layout(screenWidth, height) {
441 when (cutoutLocation) {
442 CutoutLocation.NONE,
443 CutoutLocation.RIGHT -> {
444 startPlaceable.placeRelative(x = 0, y = 0)
445 endPlaceable.placeRelative(x = startPlaceable.width, y = 0)
446 }
447 CutoutLocation.CENTER -> {
448 startPlaceable.placeRelative(x = 0, y = 0)
449 endPlaceable.placeRelative(x = startPlaceable.width + cutoutWidthPx, y = 0)
450 }
451 CutoutLocation.LEFT -> {
452 startPlaceable.placeRelative(x = cutoutWidthPx, y = 0)
453 endPlaceable.placeRelative(x = startPlaceable.width + cutoutWidthPx, y = 0)
454 }
455 }
456 }
457 }
458 }
459
460 @Composable
ContentScopenull461 private fun ContentScope.Clock(
462 onClick: () -> Unit,
463 modifier: Modifier = Modifier,
464 scale: Float = 1f,
465 ) {
466 val layoutDirection = LocalLayoutDirection.current
467
468 ElementWithValues(key = ShadeHeader.Elements.Clock, modifier = modifier) {
469 val animatedScale by animateElementFloatAsState(scale, ClockScale, canOverflow = false)
470
471 content {
472 AndroidView(
473 factory = { context ->
474 Clock(
475 ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings_Header),
476 null,
477 )
478 },
479 modifier =
480 modifier
481 // use graphicsLayer instead of Modifier.scale to anchor transform to the
482 // (start, top) corner
483 .graphicsLayer {
484 scaleX = animatedScale
485 scaleY = animatedScale
486 transformOrigin =
487 TransformOrigin(
488 when (layoutDirection) {
489 LayoutDirection.Ltr -> 0f
490 LayoutDirection.Rtl -> 1f
491 },
492 0.5f,
493 )
494 }
495 .clickable { onClick() },
496 )
497 }
498 }
499 }
500
501 @Composable
BatteryIconnull502 private fun BatteryIcon(
503 createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
504 useExpandedFormat: Boolean,
505 modifier: Modifier = Modifier,
506 chipHighlight: HeaderChipHighlight = HeaderChipHighlight.None,
507 ) {
508 val localContext = LocalContext.current
509 val themedContext =
510 ContextThemeWrapper(localContext, R.style.Theme_SystemUI_QuickSettings_Header)
511 val primaryColor =
512 Utils.getColorAttrDefaultColor(themedContext, android.R.attr.textColorPrimary)
513 val inverseColor =
514 Utils.getColorAttrDefaultColor(themedContext, android.R.attr.textColorPrimaryInverse)
515
516 AndroidView(
517 factory = { context ->
518 val batteryIcon = BatteryMeterView(context, null)
519 batteryIcon.setPercentShowMode(BatteryMeterView.MODE_ON)
520
521 // [BatteryMeterView.updateColors] is an old method that was built to distinguish
522 // between dual-tone colors and single-tone. The current icon is only single-tone, so
523 // the final [fg] is the only one we actually need
524 batteryIcon.updateColors(primaryColor, inverseColor, primaryColor)
525
526 val batteryMaterViewController =
527 createBatteryMeterViewController(batteryIcon, StatusBarLocation.QS)
528 batteryMaterViewController.init()
529 batteryMaterViewController.ignoreTunerUpdates()
530
531 batteryIcon
532 },
533 update = { batteryIcon ->
534 // TODO(b/298525212): use MODE_ESTIMATE in collapsed view when the screen
535 // has no center cutout. See [QsBatteryModeController.getBatteryMode]
536 batteryIcon.setPercentShowMode(
537 if (useExpandedFormat) BatteryMeterView.MODE_ESTIMATE else BatteryMeterView.MODE_ON
538 )
539 // TODO(b/397223606): Get the actual spec for this.
540 if (chipHighlight is HeaderChipHighlight.Strong) {
541 batteryIcon.updateColors(primaryColor, inverseColor, inverseColor)
542 } else if (chipHighlight is HeaderChipHighlight.Weak) {
543 batteryIcon.updateColors(primaryColor, inverseColor, primaryColor)
544 }
545 },
546 modifier = modifier,
547 )
548 }
549
550 @OptIn(ExperimentalKairosApi::class)
551 @Composable
ShadeCarrierGroupnull552 private fun ShadeCarrierGroup(viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifier) {
553 if (Flags.statusBarMobileIconKairos()) {
554 ShadeCarrierGroupKairos(viewModel, modifier)
555 return
556 }
557
558 Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(5.dp)) {
559 for (subId in viewModel.mobileSubIds) {
560 AndroidView(
561 factory = { context ->
562 ModernShadeCarrierGroupMobileView.constructAndBind(
563 context = context,
564 logger = viewModel.mobileIconsViewModel.logger,
565 slot = "mobile_carrier_shade_group",
566 viewModel =
567 (viewModel.mobileIconsViewModel.viewModelForSub(
568 subId,
569 StatusBarLocation.SHADE_CARRIER_GROUP,
570 ) as ShadeCarrierGroupMobileIconViewModel),
571 )
572 .also { it.setOnClickListener { viewModel.onShadeCarrierGroupClicked() } }
573 }
574 )
575 }
576 }
577 }
578
579 @ExperimentalKairosApi
580 @Composable
ShadeCarrierGroupKairosnull581 private fun ShadeCarrierGroupKairos(
582 viewModel: ShadeHeaderViewModel,
583 modifier: Modifier = Modifier,
584 ) {
585 Row(modifier = modifier) {
586 ActivatedKairosSpec(
587 buildSpec = viewModel.mobileIconsViewModelKairos.get().composeWrapper(),
588 kairosNetwork = viewModel.kairosNetwork,
589 ) { iconsViewModel: MobileIconsViewModelKairosComposeWrapper ->
590 for ((subId, icon) in iconsViewModel.icons) {
591 Spacer(modifier = Modifier.width(5.dp))
592 val scope = rememberCoroutineScope()
593 AndroidView(
594 factory = { context ->
595 ModernShadeCarrierGroupMobileView.constructAndBind(
596 context = context,
597 logger = iconsViewModel.logger,
598 slot = "mobile_carrier_shade_group",
599 viewModel =
600 buildSpec {
601 ShadeCarrierGroupMobileIconViewModelKairos(
602 icon,
603 icon.iconInteractor,
604 )
605 },
606 scope = scope,
607 subscriptionId = subId,
608 location = StatusBarLocation.SHADE_CARRIER_GROUP,
609 kairosNetwork = viewModel.kairosNetwork,
610 )
611 .first
612 .also {
613 it.setOnClickListener { viewModel.onShadeCarrierGroupClicked() }
614 }
615 }
616 )
617 }
618 }
619 }
620 }
621
622 @Composable
ContentScopenull623 private fun ContentScope.StatusIcons(
624 viewModel: ShadeHeaderViewModel,
625 useExpandedFormat: Boolean,
626 modifier: Modifier = Modifier,
627 ) {
628 val localContext = LocalContext.current
629 val themedContext =
630 ContextThemeWrapper(localContext, R.style.Theme_SystemUI_QuickSettings_Header)
631 val primaryColor =
632 Utils.getColorAttrDefaultColor(themedContext, android.R.attr.textColorPrimary)
633 val inverseColor =
634 Utils.getColorAttrDefaultColor(themedContext, android.R.attr.textColorPrimaryInverse)
635
636 val carrierIconSlots =
637 listOf(stringResource(id = com.android.internal.R.string.status_bar_mobile))
638 val cameraSlot = stringResource(id = com.android.internal.R.string.status_bar_camera)
639 val micSlot = stringResource(id = com.android.internal.R.string.status_bar_microphone)
640 val locationSlot = stringResource(id = com.android.internal.R.string.status_bar_location)
641
642 val isSingleCarrier by viewModel.isSingleCarrier.collectAsStateWithLifecycle()
643 val isPrivacyChipEnabled by viewModel.isPrivacyChipEnabled.collectAsStateWithLifecycle()
644 val isMicCameraIndicationEnabled by
645 viewModel.isMicCameraIndicationEnabled.collectAsStateWithLifecycle()
646 val isLocationIndicationEnabled by
647 viewModel.isLocationIndicationEnabled.collectAsStateWithLifecycle()
648
649 val iconContainer = remember { StatusIconContainer(themedContext, null) }
650 val iconManager = remember {
651 viewModel.createTintedIconManager(iconContainer, StatusBarLocation.QS)
652 }
653
654 val chipHighlight = viewModel.quickSettingsChipHighlight
655
656 AndroidView(
657 factory = { context ->
658 iconManager.setTint(primaryColor, inverseColor)
659 viewModel.statusBarIconController.addIconGroup(iconManager)
660
661 iconContainer
662 },
663 update = { iconContainer ->
664 iconContainer.setQsExpansionTransitioning(
665 layoutState.isTransitioningBetween(Scenes.Shade, Scenes.QuickSettings)
666 )
667 if (isSingleCarrier || !useExpandedFormat) {
668 iconContainer.removeIgnoredSlots(carrierIconSlots)
669 } else {
670 iconContainer.addIgnoredSlots(carrierIconSlots)
671 }
672
673 if (isPrivacyChipEnabled) {
674 if (isMicCameraIndicationEnabled) {
675 iconContainer.addIgnoredSlot(cameraSlot)
676 iconContainer.addIgnoredSlot(micSlot)
677 } else {
678 iconContainer.removeIgnoredSlot(cameraSlot)
679 iconContainer.removeIgnoredSlot(micSlot)
680 }
681 if (isLocationIndicationEnabled) {
682 iconContainer.addIgnoredSlot(locationSlot)
683 } else {
684 iconContainer.removeIgnoredSlot(locationSlot)
685 }
686 } else {
687 iconContainer.removeIgnoredSlot(cameraSlot)
688 iconContainer.removeIgnoredSlot(micSlot)
689 iconContainer.removeIgnoredSlot(locationSlot)
690 }
691
692 // TODO(b/397223606): Get the actual spec for this.
693 if (chipHighlight is HeaderChipHighlight.Strong) {
694 iconManager.setTint(inverseColor, primaryColor)
695 } else if (chipHighlight is HeaderChipHighlight.Weak) {
696 iconManager.setTint(primaryColor, inverseColor)
697 }
698 },
699 modifier = modifier,
700 )
701 }
702
703 @Composable
NotificationsChipnull704 private fun NotificationsChip(
705 onClick: () -> Unit,
706 modifier: Modifier = Modifier,
707 backgroundColor: Color = Color.Unspecified,
708 content: @Composable BoxScope.() -> Unit,
709 ) {
710 val interactionSource = remember { MutableInteractionSource() }
711 Box(
712 modifier =
713 modifier
714 .clickable(
715 interactionSource = interactionSource,
716 indication = null,
717 onClick = onClick,
718 )
719 .background(backgroundColor, RoundedCornerShape(25.dp))
720 .padding(horizontal = ChipPaddingHorizontal, vertical = ChipPaddingVertical)
721 ) {
722 content()
723 }
724 }
725
726 @Composable
SystemIconChipnull727 private fun SystemIconChip(
728 modifier: Modifier = Modifier,
729 backgroundColor: Color = Color.Unspecified,
730 onClick: (() -> Unit)? = null,
731 content: @Composable RowScope.() -> Unit,
732 ) {
733 val interactionSource = remember { MutableInteractionSource() }
734 val isHovered by interactionSource.collectIsHoveredAsState()
735 val hoverModifier =
736 with(MaterialTheme.colorScheme) {
737 Modifier.background(onScrimDim, RoundedCornerShape(CollapsedHeight / 4))
738 }
739
740 Row(
741 verticalAlignment = Alignment.CenterVertically,
742 modifier =
743 modifier
744 .thenIf(backgroundColor != Color.Unspecified) {
745 Modifier.background(backgroundColor, RoundedCornerShape(25.dp))
746 .padding(horizontal = ChipPaddingHorizontal, vertical = ChipPaddingVertical)
747 }
748 .thenIf(onClick != null) {
749 Modifier.clickable(
750 interactionSource = interactionSource,
751 indication = null,
752 onClick = { onClick?.invoke() },
753 )
754 }
755 .thenIf(isHovered) { hoverModifier },
756 content = content,
757 )
758 }
759
760 @Composable
ContentScopenull761 private fun ContentScope.PrivacyChip(
762 viewModel: ShadeHeaderViewModel,
763 modifier: Modifier = Modifier,
764 ) {
765 val privacyList by viewModel.privacyItems.collectAsStateWithLifecycle()
766
767 AndroidView(
768 factory = { context ->
769 val view =
770 OngoingPrivacyChip(context, null).also { privacyChip ->
771 privacyChip.privacyList = privacyList
772 privacyChip.setOnClickListener { viewModel.onPrivacyChipClicked(privacyChip) }
773 }
774 view
775 },
776 update = { it.privacyList = privacyList },
777 modifier = modifier.element(ShadeHeader.Elements.PrivacyChip),
778 )
779 }
780
shouldUseExpandedFormatnull781 private fun shouldUseExpandedFormat(state: TransitionState): Boolean {
782 return when (state) {
783 is TransitionState.Idle -> {
784 state.currentScene == Scenes.QuickSettings
785 }
786 is TransitionState.Transition -> {
787 ((state.isTransitioning(Scenes.Shade, Scenes.QuickSettings) ||
788 state.isTransitioning(Scenes.Gone, Scenes.QuickSettings) ||
789 state.isTransitioning(Scenes.Lockscreen, Scenes.QuickSettings)) &&
790 state.progress >= 0.5) ||
791 ((state.isTransitioning(Scenes.QuickSettings, Scenes.Shade) ||
792 state.isTransitioning(Scenes.QuickSettings, Scenes.Gone) ||
793 state.isTransitioning(Scenes.QuickSettings, Scenes.Lockscreen)) &&
794 state.progress <= 0.5)
795 }
796 }
797 }
798