• 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.notification.promoted
18 
19 import android.app.Flags.notificationsRedesignTemplates
20 import android.app.Notification
21 import android.content.Context
22 import android.graphics.PorterDuff
23 import android.util.Log
24 import android.view.LayoutInflater
25 import android.view.NotificationTopLineView
26 import android.view.View
27 import android.view.View.GONE
28 import android.view.View.MeasureSpec.AT_MOST
29 import android.view.View.MeasureSpec.EXACTLY
30 import android.view.View.MeasureSpec.UNSPECIFIED
31 import android.view.View.MeasureSpec.makeMeasureSpec
32 import android.view.View.VISIBLE
33 import android.view.ViewGroup.MarginLayoutParams
34 import android.view.ViewStub
35 import android.widget.Chronometer
36 import android.widget.DateTimeView
37 import android.widget.FrameLayout
38 import android.widget.ImageView
39 import android.widget.ProgressBar
40 import android.widget.TextView
41 import androidx.annotation.DimenRes
42 import androidx.compose.foundation.BorderStroke
43 import androidx.compose.foundation.border
44 import androidx.compose.foundation.layout.Box
45 import androidx.compose.foundation.layout.PaddingValues
46 import androidx.compose.foundation.layout.padding
47 import androidx.compose.foundation.shape.RoundedCornerShape
48 import androidx.compose.runtime.Composable
49 import androidx.compose.runtime.key
50 import androidx.compose.ui.Modifier
51 import androidx.compose.ui.graphics.SolidColor
52 import androidx.compose.ui.platform.LocalDensity
53 import androidx.compose.ui.res.dimensionResource
54 import androidx.compose.ui.unit.Dp
55 import androidx.compose.ui.unit.dp
56 import androidx.compose.ui.viewinterop.AndroidView
57 import androidx.core.view.isVisible
58 import com.android.app.tracing.traceSection
59 import com.android.internal.R
60 import com.android.internal.widget.BigPictureNotificationImageView
61 import com.android.internal.widget.CachingIconView
62 import com.android.internal.widget.ImageFloatingTextView
63 import com.android.internal.widget.NotificationExpandButton
64 import com.android.internal.widget.NotificationProgressBar
65 import com.android.internal.widget.NotificationProgressModel
66 import com.android.internal.widget.NotificationRowIconView
67 import com.android.systemui.lifecycle.rememberViewModel
68 import com.android.systemui.res.R as systemuiR
69 import com.android.systemui.statusbar.notification.promoted.AodPromotedNotificationColor.Background
70 import com.android.systemui.statusbar.notification.promoted.AodPromotedNotificationColor.PrimaryText
71 import com.android.systemui.statusbar.notification.promoted.AodPromotedNotificationColor.SecondaryText
72 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
73 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style
74 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.When
75 import com.android.systemui.statusbar.notification.promoted.ui.viewmodel.AODPromotedNotificationViewModel
76 import com.android.systemui.statusbar.notification.row.shared.ImageModel
77 import com.android.systemui.statusbar.notification.row.shared.isNullOrEmpty
78 
79 @Composable
80 fun AODPromotedNotification(
81     viewModelFactory: AODPromotedNotificationViewModel.Factory,
82     modifier: Modifier = Modifier,
83 ) {
84     if (!PromotedNotificationUi.isEnabled) {
85         return
86     }
87 
88     val viewModel = rememberViewModel(traceName = "$TAG.viewModel") { viewModelFactory.create() }
89 
90     val content = viewModel.content ?: return
91     val audiblyAlertedIconVisible = viewModel.audiblyAlertedIconVisible
92 
93     val layoutResource = content.layoutResource
94     if (layoutResource == null) {
95         Log.w(TAG, "not displaying promoted notif with ineligible style on AOD")
96         return
97     }
98 
99     key(content.identity) {
100         AODPromotedNotificationView(
101             layoutResource = layoutResource,
102             content = content,
103             audiblyAlertedIconVisible = audiblyAlertedIconVisible,
104             modifier = modifier,
105         )
106     }
107 }
108 
109 @Composable
AODPromotedNotificationViewnull110 fun AODPromotedNotificationView(
111     layoutResource: Int,
112     content: PromotedNotificationContentModel,
113     audiblyAlertedIconVisible: Boolean,
114     modifier: Modifier = Modifier,
115 ) {
116     val sidePaddings = dimensionResource(systemuiR.dimen.notification_side_paddings)
117     val sidePaddingValues = PaddingValues(horizontal = sidePaddings, vertical = 0.dp)
118 
119     val boxModifier = modifier.padding(sidePaddingValues)
120 
121     val borderStroke = BorderStroke(1.dp, SecondaryText.brush)
122 
123     val borderRadius = dimensionResource(systemuiR.dimen.notification_corner_radius)
124     val borderShape = RoundedCornerShape(borderRadius)
125 
126     val maxHeight =
127         with(LocalDensity.current) {
128                 scaledFontHeight(systemuiR.dimen.notification_max_height_for_promoted_ongoing)
129                     .toPx()
130             }
131             .toInt()
132 
133     val viewModifier = Modifier.border(borderStroke, borderShape)
134 
135     Box(modifier = boxModifier) {
136         AndroidView(
137             factory = { context ->
138                 val notif =
139                     traceSection("$TAG.inflate") {
140                         LayoutInflater.from(context).inflate(layoutResource, /* root= */ null)
141                     }
142                 val updater =
143                     traceSection("$TAG.findViews") { AODPromotedNotificationViewUpdater(notif) }
144 
145                 val frame = FrameLayoutWithMaxHeight(maxHeight, context)
146                 frame.addView(notif)
147                 frame.setTag(viewUpdaterTagId, updater)
148 
149                 frame
150             },
151             update = { frame ->
152                 val updater = frame.getTag(viewUpdaterTagId) as AODPromotedNotificationViewUpdater
153 
154                 traceSection("$TAG.update") { updater.update(content, audiblyAlertedIconVisible) }
155                 frame.maxHeight = maxHeight
156             },
157             modifier = viewModifier,
158         )
159     }
160 }
161 
162 private class FrameLayoutWithMaxHeight(maxHeight: Int, context: Context) : FrameLayout(context) {
163     var maxHeight = maxHeight
164         set(value) {
165             if (field != value) {
166                 field = value
167                 requestLayout()
168             }
169         }
170 
171     // This mirrors the logic in NotificationContentView.onMeasure.
onMeasurenull172     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
173         if (childCount != 1) {
174             Log.wtf(TAG, "Should contain exactly one child.")
175             return super.onMeasure(widthMeasureSpec, heightMeasureSpec)
176         }
177 
178         val horizPadding = paddingStart + paddingEnd
179         val vertPadding = paddingTop + paddingBottom
180 
181         val ownWidthSize = MeasureSpec.getSize(widthMeasureSpec)
182         val ownHeightMode = MeasureSpec.getMode(heightMeasureSpec)
183         val ownHeightSize = MeasureSpec.getSize(heightMeasureSpec)
184 
185         val availableHeight =
186             if (ownHeightMode != UNSPECIFIED) {
187                 maxHeight.coerceAtMost(ownHeightSize)
188             } else {
189                 maxHeight
190             }
191 
192         val child = getChildAt(0)
193         val childWidthSpec = makeMeasureSpec(ownWidthSize, EXACTLY)
194         val childHeightSpec =
195             child.layoutParams.height
196                 .takeIf { it >= 0 }
197                 ?.let { makeMeasureSpec(availableHeight.coerceAtMost(it), EXACTLY) }
198                 ?: run { makeMeasureSpec(availableHeight, AT_MOST) }
199         measureChildWithMargins(child, childWidthSpec, horizPadding, childHeightSpec, vertPadding)
200         val childMeasuredHeight = child.measuredHeight
201 
202         val ownMeasuredWidth = MeasureSpec.getSize(widthMeasureSpec)
203         val ownMeasuredHeight =
204             if (ownHeightMode != UNSPECIFIED) {
205                 childMeasuredHeight.coerceAtMost(ownHeightSize)
206             } else {
207                 childMeasuredHeight
208             }
209         setMeasuredDimension(ownMeasuredWidth, ownMeasuredHeight)
210     }
211 }
212 
213 private val PromotedNotificationContentModel.layoutResource: Int?
214     get() {
215         return if (notificationsRedesignTemplates()) {
216             when (style) {
217                 Style.Base -> R.layout.notification_2025_template_expanded_base
218                 Style.CollapsedBase -> R.layout.notification_2025_template_collapsed_base
219                 Style.BigPicture -> R.layout.notification_2025_template_expanded_big_picture
220                 Style.BigText -> R.layout.notification_2025_template_expanded_big_text
221                 Style.Call -> R.layout.notification_2025_template_expanded_call
222                 Style.CollapsedCall -> R.layout.notification_2025_template_collapsed_call
223                 Style.Progress -> R.layout.notification_2025_template_expanded_progress
224                 Style.Ineligible -> null
225             }
226         } else {
227             when (style) {
228                 Style.Base -> R.layout.notification_template_material_big_base
229                 Style.CollapsedBase -> R.layout.notification_template_material_base
230                 Style.BigPicture -> R.layout.notification_template_material_big_picture
231                 Style.BigText -> R.layout.notification_template_material_big_text
232                 Style.Call -> R.layout.notification_template_material_big_call
233                 Style.CollapsedCall -> R.layout.notification_template_material_call
234                 Style.Progress -> R.layout.notification_template_material_progress
235                 Style.Ineligible -> null
236             }
237         }
238     }
239 
240 private class AODPromotedNotificationViewUpdater(root: View) {
241     private val alertedIcon: ImageView? = root.findViewById(R.id.alerted_icon)
242     private val alternateExpandTarget: View? = root.findViewById(R.id.alternate_expand_target)
243     private val appNameDivider: TextView? = root.findViewById(R.id.app_name_divider)
244     private val appNameText: TextView? = root.findViewById(R.id.app_name_text)
245     private val bigPicture: BigPictureNotificationImageView? = root.findViewById(R.id.big_picture)
246     private val bigText: ImageFloatingTextView? = root.findViewById(R.id.big_text)
247     private var chronometerStub: ViewStub? = root.findViewById(R.id.chronometer)
248     private var chronometer: Chronometer? = null
249     private val closeButton: View? = root.findViewById(R.id.close_button)
250     private val conversationIconBadge: View? = root.findViewById(R.id.conversation_icon_badge)
251     private val conversationIcon: CachingIconView? = root.findViewById(R.id.conversation_icon)
252     private val conversationText: TextView? =
253         root.findViewById(
254             if (notificationsRedesignTemplates()) R.id.title else R.id.conversation_text
255         )
256     private val expandButton: NotificationExpandButton? = root.findViewById(R.id.expand_button)
257     private val headerText: TextView? = root.findViewById(R.id.header_text)
258     private val headerTextDivider: TextView? = root.findViewById(R.id.header_text_divider)
259     private val headerTextSecondary: TextView? = root.findViewById(R.id.header_text_secondary)
260     private val headerTextSecondaryDivider: TextView? =
261         root.findViewById(R.id.header_text_secondary_divider)
262     private val icon: NotificationRowIconView? = root.findViewById(R.id.icon)
263     private val leftIcon: ImageView? = root.findViewById(R.id.left_icon)
264     private val mainColumn: View? = root.findViewById(R.id.notification_main_column)
265     private val notificationProgressEndIcon: CachingIconView? =
266         root.findViewById(R.id.notification_progress_end_icon)
267     private val notificationProgressStartIcon: CachingIconView? =
268         root.findViewById(R.id.notification_progress_start_icon)
269     private val profileBadge: ImageView? = root.findViewById(R.id.profile_badge)
270     private val rightIcon: ImageView? = root.findViewById(R.id.right_icon)
271     private val text: ImageFloatingTextView? = root.findViewById(R.id.text)
272     private val time: DateTimeView? = root.findViewById(R.id.time)
273     private val timeDivider: TextView? = root.findViewById(R.id.time_divider)
274     private val title: TextView? = root.findViewById(R.id.title)
275     private val topLine: NotificationTopLineView? = root.findViewById(R.id.notification_top_line)
276     private val verificationDivider: TextView? = root.findViewById(R.id.verification_divider)
277     private val verificationIcon: ImageView? = root.findViewById(R.id.verification_icon)
278     private val verificationText: TextView? = root.findViewById(R.id.verification_text)
279 
280     private var oldProgressBarStub = root.findViewById<View>(R.id.progress) as? ViewStub
281     private var oldProgressBar: ProgressBar? = null
282     private val newProgressBar = root.findViewById<View>(R.id.progress) as? NotificationProgressBar
283 
284     private val largeIconSizePx: Int =
285         root.context.resources.getDimensionPixelSize(R.dimen.notification_right_icon_size)
286 
287     private val endMarginPx: Int =
288         if (notificationsRedesignTemplates()) {
289             root.context.resources.getDimensionPixelSize(R.dimen.notification_2025_margin)
290         } else {
291             root.context.resources.getDimensionPixelSize(
292                 systemuiR.dimen.notification_shade_content_margin_horizontal
293             )
294         }
295 
296     private val imageEndMarginPx: Int
297         get() = largeIconSizePx + 2 * endMarginPx
298 
299     private val PromotedNotificationContentModel.imageEndMarginPxIfHasLargeIcon: Int
300         get() =
301             if (!skeletonLargeIcon.isNullOrEmpty()) {
302                 imageEndMarginPx
303             } else {
304                 0
305             }
306 
307     init {
308         // Hide views that are never visible in the skeleton promoted notification.
309         alternateExpandTarget?.visibility = GONE
310         bigPicture?.visibility = GONE
311         closeButton?.visibility = GONE
312         conversationIconBadge?.visibility = GONE
313         expandButton?.visibility = GONE
314         leftIcon?.visibility = GONE
315         notificationProgressEndIcon?.visibility = GONE
316         notificationProgressStartIcon?.visibility = GONE
317 
318         // Make one-time changes needed for the skeleton promoted notification.
319         alertedIcon
320             ?.drawable
321             ?.mutate()
322             ?.setColorFilter(SecondaryText.colorInt, PorterDuff.Mode.SRC_IN)
323 
<lambda>null324         (rightIcon?.layoutParams as? MarginLayoutParams)?.let {
325             it.marginEnd = endMarginPx
326             rightIcon.layoutParams = it
327         }
328         bigText?.setImageEndMargin(largeIconSizePx + endMarginPx)
329         text?.setImageEndMargin(largeIconSizePx + endMarginPx)
330 
331         setTextViewColor(appNameDivider, SecondaryText)
332         setTextViewColor(headerTextDivider, SecondaryText)
333         setTextViewColor(headerTextSecondaryDivider, SecondaryText)
334         setTextViewColor(timeDivider, SecondaryText)
335         setTextViewColor(verificationDivider, SecondaryText)
336 
337         if (notificationsRedesignTemplates()) {
mainColumnMarginsnull338             (mainColumn?.layoutParams as? MarginLayoutParams)?.let { mainColumnMargins ->
339                 mainColumnMargins.topMargin =
340                     Notification.Builder.getContentMarginTop(
341                         root.context,
342                         R.dimen.notification_2025_content_margin_top,
343                     )
344             }
345         }
346     }
347 
updatenull348     fun update(content: PromotedNotificationContentModel, audiblyAlertedIconVisible: Boolean) {
349         when (content.style) {
350             Style.Base -> updateBase(content, collapsed = false)
351             Style.CollapsedBase -> updateBase(content, collapsed = true)
352             Style.BigPicture -> updateBigPictureStyle(content)
353             Style.BigText -> updateBigTextStyle(content)
354             Style.Call -> updateCallStyle(content, collapsed = false)
355             Style.CollapsedCall -> updateCallStyle(content, collapsed = true)
356             Style.Progress -> updateProgressStyle(content)
357             Style.Ineligible -> {}
358         }
359 
360         alertedIcon?.isVisible = audiblyAlertedIconVisible
361     }
362 
updateBasenull363     private fun updateBase(
364         content: PromotedNotificationContentModel,
365         collapsed: Boolean,
366         textView: ImageFloatingTextView? = text,
367     ) {
368         val headerTitleView = if (collapsed) title else null
369         updateHeader(content, titleView = headerTitleView, collapsed = collapsed)
370 
371         if (headerTitleView == null) {
372             updateTitle(title, content)
373         }
374         updateText(textView, content)
375         updateSmallIcon(icon, content)
376         updateImageView(rightIcon, content.skeletonLargeIcon)
377         updateOldProgressBar(content)
378     }
379 
updateBigPictureStylenull380     private fun updateBigPictureStyle(content: PromotedNotificationContentModel) {
381         updateBase(content, collapsed = false)
382     }
383 
updateBigTextStylenull384     private fun updateBigTextStyle(content: PromotedNotificationContentModel) {
385         updateBase(content, collapsed = false, textView = bigText)
386     }
387 
updateCallStylenull388     private fun updateCallStyle(content: PromotedNotificationContentModel, collapsed: Boolean) {
389         updateConversationHeader(content, collapsed = collapsed)
390 
391         updateText(text, content)
392     }
393 
updateProgressStylenull394     private fun updateProgressStyle(content: PromotedNotificationContentModel) {
395         updateBase(content, collapsed = false)
396 
397         updateNewProgressBar(content)
398     }
399 
updateOldProgressBarnull400     private fun updateOldProgressBar(content: PromotedNotificationContentModel) {
401         if (
402             content.style == Style.Progress ||
403                 content.oldProgress == null ||
404                 content.oldProgress.max == 0 ||
405                 content.oldProgress.isIndeterminate
406         ) {
407             oldProgressBar?.visibility = GONE
408             return
409         }
410 
411         inflateOldProgressBar()
412 
413         val oldProgressBar = oldProgressBar ?: return
414 
415         oldProgressBar.progress = content.oldProgress.progress
416         oldProgressBar.max = content.oldProgress.max
417         oldProgressBar.isIndeterminate = content.oldProgress.isIndeterminate
418         oldProgressBar.visibility = VISIBLE
419     }
420 
updateNewProgressBarnull421     private fun updateNewProgressBar(content: PromotedNotificationContentModel) {
422         val newProgressBar = newProgressBar ?: return
423 
424         if (content.newProgress != null && !content.newProgress.isIndeterminate) {
425             newProgressBar.setProgressModel(content.newProgress.toSkeleton().toBundle())
426             newProgressBar.visibility = VISIBLE
427         } else {
428             newProgressBar.visibility = GONE
429         }
430     }
431 
updateHeadernull432     private fun updateHeader(
433         content: PromotedNotificationContentModel,
434         collapsed: Boolean,
435         titleView: TextView?,
436     ) {
437         val hasTitle = titleView != null && content.title != null
438         val hasSubText = content.subText != null
439         // the collapsed form doesn't show the app name unless there is no other text in the header
440         val appNameRequired = !hasTitle && !hasSubText
441         val hideAppName = (!appNameRequired && collapsed)
442 
443         updateAppName(content, forceHide = hideAppName)
444         updateTextView(headerTextSecondary, content.subText)
445         updateTitle(titleView, content)
446         updateTimeAndChronometer(content)
447 
448         updateHeaderDividers(content, hideTitle = !hasTitle, hideAppName = hideAppName)
449 
450         updateTopLine(content)
451     }
452 
updateHeaderDividersnull453     private fun updateHeaderDividers(
454         content: PromotedNotificationContentModel,
455         hideAppName: Boolean,
456         hideTitle: Boolean,
457     ) {
458         val hasAppName = content.appName != null && !hideAppName
459         val hasSubText = content.subText != null
460         val hasHeader = content.title != null && !hideTitle
461         val hasTimeOrChronometer = content.time != null
462 
463         val hasTextBeforeSubText = hasAppName
464         val hasTextBeforeHeader = hasAppName || hasSubText
465         val hasTextBeforeTime = hasAppName || hasSubText || hasHeader
466 
467         val showDividerBeforeSubText = hasTextBeforeSubText && hasSubText
468         val showDividerBeforeHeader = hasTextBeforeHeader && hasHeader
469         val showDividerBeforeTime = hasTextBeforeTime && hasTimeOrChronometer
470 
471         headerTextSecondaryDivider?.isVisible = showDividerBeforeSubText
472         headerTextDivider?.isVisible = showDividerBeforeHeader
473         timeDivider?.isVisible = showDividerBeforeTime
474     }
475 
updateConversationHeadernull476     private fun updateConversationHeader(
477         content: PromotedNotificationContentModel,
478         collapsed: Boolean,
479     ) {
480         updateAppName(content, forceHide = collapsed)
481         updateTimeAndChronometer(content)
482 
483         updateImageView(verificationIcon, content.verificationIcon)
484         updateTextView(verificationText, content.verificationText)
485 
486         updateConversationHeaderDividers(content, hideTitle = true, hideAppName = collapsed)
487 
488         updateTopLine(content)
489 
490         updateSmallIcon(conversationIcon, content)
491         updateTitle(conversationText, content)
492     }
493 
updateConversationHeaderDividersnull494     private fun updateConversationHeaderDividers(
495         content: PromotedNotificationContentModel,
496         hideTitle: Boolean,
497         hideAppName: Boolean,
498     ) {
499         val hasTitle = content.title != null && !hideTitle
500         val hasAppName = content.appName != null && !hideAppName
501         val hasTimeOrChronometer = content.time != null
502         val hasVerification =
503             !content.verificationIcon.isNullOrEmpty() || content.verificationText != null
504 
505         val hasTextBeforeAppName = hasTitle
506         val hasTextBeforeTime = hasTitle || hasAppName
507         val hasTextBeforeVerification = hasTitle || hasAppName || hasTimeOrChronometer
508 
509         val showDividerBeforeAppName = hasTextBeforeAppName && hasAppName
510         val showDividerBeforeTime = hasTextBeforeTime && hasTimeOrChronometer
511         val showDividerBeforeVerification = hasTextBeforeVerification && hasVerification
512 
513         appNameDivider?.isVisible = showDividerBeforeAppName
514         timeDivider?.isVisible = showDividerBeforeTime
515         verificationDivider?.isVisible = showDividerBeforeVerification
516     }
517 
updateAppNamenull518     private fun updateAppName(content: PromotedNotificationContentModel, forceHide: Boolean) {
519         updateTextView(appNameText, content.appName?.takeUnless { forceHide })
520     }
521 
updateTitlenull522     private fun updateTitle(titleView: TextView?, content: PromotedNotificationContentModel) {
523         (titleView?.layoutParams as? MarginLayoutParams)?.let {
524             it.marginEnd = content.imageEndMarginPxIfHasLargeIcon
525             titleView.layoutParams = it
526         }
527         updateTextView(titleView, content.title, color = PrimaryText)
528     }
529 
updateTimeAndChronometernull530     private fun updateTimeAndChronometer(content: PromotedNotificationContentModel) {
531         setTextViewColor(time, SecondaryText)
532         setTextViewColor(chronometer, SecondaryText)
533 
534         if (content.time is When.Time) {
535             time?.setTime(content.time.currentTimeMillis)
536         }
537 
538         if (content.time is When.Chronometer) {
539             inflateChronometer()
540             chronometer?.base = content.time.elapsedRealtimeMillis
541             chronometer?.isCountDown = content.time.isCountDown
542             chronometer?.setStarted(true)
543         } else {
544             chronometer?.stop()
545         }
546 
547         time?.isVisible = (content.time is When.Time)
548         chronometer?.isVisible = (content.time is When.Chronometer)
549     }
550 
updateSmallIconnull551     private fun updateSmallIcon(
552         smallIconView: CachingIconView?,
553         content: PromotedNotificationContentModel,
554     ) {
555         smallIconView ?: return
556 
557         // Icon binding must be called in this order
558         updateImageView(smallIconView, content.smallIcon)
559         smallIconView.setImageLevel(content.iconLevel)
560         smallIconView.setBackgroundColor(Background.colorInt)
561         smallIconView.originalIconColor = PrimaryText.colorInt
562     }
563 
inflateChronometernull564     private fun inflateChronometer() {
565         if (chronometer != null) {
566             return
567         }
568 
569         chronometer = chronometerStub?.inflate() as Chronometer
570         chronometerStub = null
571 
572         chronometer?.appendFontFeatureSetting("tnum")
573     }
574 
updateTopLinenull575     private fun updateTopLine(content: PromotedNotificationContentModel) {
576         topLine?.headerTextMarginEnd = content.imageEndMarginPxIfHasLargeIcon
577     }
578 
inflateOldProgressBarnull579     private fun inflateOldProgressBar() {
580         if (oldProgressBar != null) {
581             return
582         }
583 
584         oldProgressBar = oldProgressBarStub?.inflate() as ProgressBar
585         oldProgressBarStub = null
586     }
587 
updateTextnull588     private fun updateText(
589         view: ImageFloatingTextView?,
590         content: PromotedNotificationContentModel,
591     ) {
592         view?.setHasImage(!content.skeletonLargeIcon.isNullOrEmpty())
593         view?.setNumIndentLines(if (content.title != null) 0 else 1)
594         updateTextView(view, content.text)
595     }
596 
updateTextViewnull597     private fun updateTextView(
598         view: TextView?,
599         text: CharSequence?,
600         color: AodPromotedNotificationColor = SecondaryText,
601     ) {
602         if (view == null) return
603         setTextViewColor(view, color)
604 
605         view.text = text?.toSkeleton() ?: ""
606         view.isVisible = !text.isNullOrEmpty()
607     }
608 
updateImageViewnull609     private fun updateImageView(view: ImageView?, model: ImageModel?) {
610         if (view == null) return
611         val drawable = model?.drawable
612         view.setImageDrawable(drawable)
613         view.isVisible = drawable != null
614     }
615 
setTextViewColornull616     private fun setTextViewColor(view: TextView?, color: AodPromotedNotificationColor) {
617         view?.setTextColor(color.colorInt)
618     }
619 }
620 
toSkeletonnull621 private fun CharSequence.toSkeleton(): CharSequence {
622     return this.toString()
623 }
624 
toSkeletonnull625 private fun NotificationProgressModel.toSkeleton(): NotificationProgressModel {
626     if (isIndeterminate) {
627         return NotificationProgressModel(/* indeterminateColor= */ SecondaryText.colorInt)
628     }
629 
630     return NotificationProgressModel(
631         listOf(Notification.ProgressStyle.Segment(progressMax).toSkeleton()),
632         points.map { it.toSkeleton() }.toList(),
633         progress,
634         /* isStyledByProgress = */ true,
635         /* segmentsFallbackColor = */ SecondaryText.colorInt,
636     )
637 }
638 
toSkeletonnull639 private fun Notification.ProgressStyle.Segment.toSkeleton(): Notification.ProgressStyle.Segment {
640     return Notification.ProgressStyle.Segment(length).also {
641         it.id = id
642         it.color = SecondaryText.colorInt
643     }
644 }
645 
toSkeletonnull646 private fun Notification.ProgressStyle.Point.toSkeleton(): Notification.ProgressStyle.Point {
647     return Notification.ProgressStyle.Point(position).also {
648         it.id = id
649         it.color = SecondaryText.colorInt
650     }
651 }
652 
appendFontFeatureSettingnull653 private fun TextView.appendFontFeatureSetting(newSetting: String) {
654     fontFeatureSettings = (fontFeatureSettings?.let { "$it," } ?: "") + newSetting
655 }
656 
657 private enum class AodPromotedNotificationColor(val colorInt: Int) {
658     Background(android.graphics.Color.BLACK),
659     PrimaryText(android.graphics.Color.WHITE),
660     SecondaryText(android.graphics.Color.WHITE);
661 
662     val brush = SolidColor(androidx.compose.ui.graphics.Color(colorInt))
663 }
664 
665 @Composable
scaledFontHeightnull666 private fun scaledFontHeight(@DimenRes dimenId: Int): Dp {
667     return dimensionResource(dimenId) * LocalDensity.current.fontScale.coerceAtLeast(1f)
668 }
669 
670 private val viewUpdaterTagId = systemuiR.id.aod_promoted_notification_view_updater_tag
671 
672 private const val TAG = "AODPromotedNotification"
673