• 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 
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