• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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.chips.ui.binder
18 
19 import android.annotation.IdRes
20 import android.content.Context
21 import android.content.res.ColorStateList
22 import android.graphics.Typeface
23 import android.graphics.drawable.GradientDrawable
24 import android.view.View
25 import android.view.ViewGroup
26 import android.widget.DateTimeView
27 import android.widget.FrameLayout
28 import android.widget.ImageView
29 import android.widget.TextView
30 import androidx.annotation.UiThread
31 import com.android.systemui.FontStyles
32 import com.android.systemui.common.shared.model.ContentDescription
33 import com.android.systemui.common.ui.binder.ContentDescriptionViewBinder
34 import com.android.systemui.common.ui.binder.IconViewBinder
35 import com.android.systemui.res.R
36 import com.android.systemui.statusbar.StatusBarIconView
37 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
38 import com.android.systemui.statusbar.chips.ui.model.ColorsModel
39 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
40 import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
41 import com.android.systemui.statusbar.chips.ui.view.ChipChronometer
42 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
43 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.IconViewStore
44 
45 /** Binder for ongoing activity chip views. */
46 object OngoingActivityChipBinder {
47     /** Binds the given [chipModel] data to the given [chipView]. */
bindnull48     fun bind(
49         chipModel: OngoingActivityChipModel,
50         viewBinding: OngoingActivityChipViewBinding,
51         iconViewStore: IconViewStore?,
52     ) {
53         val chipContext = viewBinding.rootView.context
54         val chipDefaultIconView = viewBinding.defaultIconView
55         val chipTimeView = viewBinding.timeView
56         val chipTextView = viewBinding.textView
57         val chipShortTimeDeltaView = viewBinding.shortTimeDeltaView
58         val chipBackgroundView = viewBinding.backgroundView
59 
60         when (chipModel) {
61             is OngoingActivityChipModel.Active -> {
62                 // Data
63                 setChipIcon(chipModel, chipBackgroundView, chipDefaultIconView, iconViewStore)
64                 setChipMainContent(chipModel, chipTextView, chipTimeView, chipShortTimeDeltaView)
65 
66                 viewBinding.rootView.setOnClickListener(chipModel.onClickListenerLegacy)
67                 updateChipPadding(
68                     chipModel,
69                     chipBackgroundView,
70                     chipTextView,
71                     chipTimeView,
72                     chipShortTimeDeltaView,
73                 )
74 
75                 // Accessibility
76                 setChipAccessibility(chipModel, viewBinding.rootView, chipBackgroundView)
77 
78                 // Colors
79                 val textColor = chipModel.colors.text(chipContext)
80                 chipTimeView.setTextColor(textColor)
81                 chipTextView.setTextColor(textColor)
82                 chipShortTimeDeltaView.setTextColor(textColor)
83                 (chipBackgroundView.background as GradientDrawable).setBackgroundColors(
84                     chipModel.colors,
85                     chipContext,
86                 )
87             }
88             is OngoingActivityChipModel.Inactive -> {
89                 // The Chronometer should be stopped to prevent leaks -- see b/192243808 and
90                 // [Chronometer.start].
91                 chipTimeView.stop()
92             }
93         }
94     }
95 
96     /** Stores [rootView] and relevant child views in an object for easy reference. */
createBindingnull97     fun createBinding(rootView: View): OngoingActivityChipViewBinding {
98         return OngoingActivityChipViewBinding(
99             rootView = rootView,
100             timeView = rootView.requireViewById(R.id.ongoing_activity_chip_time),
101             textView = rootView.requireViewById(R.id.ongoing_activity_chip_text),
102             shortTimeDeltaView =
103                 rootView.requireViewById(R.id.ongoing_activity_chip_short_time_delta),
104             defaultIconView = rootView.requireViewById(R.id.ongoing_activity_chip_icon),
105             backgroundView = rootView.requireViewById(R.id.ongoing_activity_chip_background),
106         )
107     }
108 
109     /**
110      * Resets any width restrictions that were placed on the primary chip's contents.
111      *
112      * Should be used when the user's screen bounds changed because there may now be more room in
113      * the status bar to show additional content.
114      */
resetPrimaryChipWidthRestrictionsnull115     fun resetPrimaryChipWidthRestrictions(
116         primaryChipViewBinding: OngoingActivityChipViewBinding,
117         currentPrimaryChipViewModel: OngoingActivityChipModel,
118     ) {
119         if (currentPrimaryChipViewModel is OngoingActivityChipModel.Inactive) {
120             return
121         }
122         resetChipMainContentWidthRestrictions(
123             primaryChipViewBinding,
124             currentPrimaryChipViewModel as OngoingActivityChipModel.Active,
125         )
126     }
127 
128     /**
129      * Resets any width restrictions that were placed on the secondary chip and its contents.
130      *
131      * Should be used when the user's screen bounds changed because there may now be more room in
132      * the status bar to show additional content.
133      */
resetSecondaryChipWidthRestrictionsnull134     fun resetSecondaryChipWidthRestrictions(
135         secondaryChipViewBinding: OngoingActivityChipViewBinding,
136         currentSecondaryChipModel: OngoingActivityChipModel,
137     ) {
138         if (currentSecondaryChipModel is OngoingActivityChipModel.Inactive) {
139             return
140         }
141         secondaryChipViewBinding.rootView.resetWidthRestriction()
142         resetChipMainContentWidthRestrictions(
143             secondaryChipViewBinding,
144             currentSecondaryChipModel as OngoingActivityChipModel.Active,
145         )
146     }
147 
resetChipMainContentWidthRestrictionsnull148     private fun resetChipMainContentWidthRestrictions(
149         viewBinding: OngoingActivityChipViewBinding,
150         model: OngoingActivityChipModel.Active,
151     ) {
152         when (model) {
153             is OngoingActivityChipModel.Active.Text -> viewBinding.textView.resetWidthRestriction()
154             is OngoingActivityChipModel.Active.Timer -> viewBinding.timeView.resetWidthRestriction()
155             is OngoingActivityChipModel.Active.ShortTimeDelta ->
156                 viewBinding.shortTimeDeltaView.resetWidthRestriction()
157             is OngoingActivityChipModel.Active.IconOnly,
158             is OngoingActivityChipModel.Active.Countdown -> {}
159         }
160     }
161 
162     /**
163      * Resets any width restrictions that were placed on the given view.
164      *
165      * Should be used when the user's screen bounds changed because there may now be more room in
166      * the status bar to show additional content.
167      */
168     @UiThread
resetWidthRestrictionnull169     fun View.resetWidthRestriction() {
170         // View needs to be visible in order to be re-measured
171         visibility = View.VISIBLE
172         forceLayout()
173     }
174 
175     /** Updates the typefaces for any text shown in the chip. */
updateTypefacesnull176     fun updateTypefaces(binding: OngoingActivityChipViewBinding) {
177         binding.timeView.typeface =
178             Typeface.create(FontStyles.GSF_LABEL_LARGE_EMPHASIZED, Typeface.NORMAL)
179         binding.textView.typeface =
180             Typeface.create(FontStyles.GSF_LABEL_LARGE_EMPHASIZED, Typeface.NORMAL)
181         binding.shortTimeDeltaView.typeface =
182             Typeface.create(FontStyles.GSF_LABEL_LARGE_EMPHASIZED, Typeface.NORMAL)
183     }
184 
setChipIconnull185     private fun setChipIcon(
186         chipModel: OngoingActivityChipModel.Active,
187         backgroundView: ChipBackgroundContainer,
188         defaultIconView: ImageView,
189         iconViewStore: IconViewStore?,
190     ) {
191         // Always remove any previously set custom icon. If we have a new custom icon, we'll re-add
192         // it.
193         backgroundView.removeView(backgroundView.getCustomIconView())
194 
195         val iconTint = chipModel.colors.text(defaultIconView.context)
196 
197         when (val icon = chipModel.icon) {
198             null -> {
199                 defaultIconView.visibility = View.GONE
200             }
201             is OngoingActivityChipModel.ChipIcon.SingleColorIcon -> {
202                 IconViewBinder.bind(icon.impl, defaultIconView)
203                 defaultIconView.visibility = View.VISIBLE
204                 defaultIconView.tintView(iconTint)
205             }
206             is OngoingActivityChipModel.ChipIcon.StatusBarView -> {
207                 StatusBarConnectedDisplays.assertInLegacyMode()
208                 setStatusBarIconView(
209                     defaultIconView,
210                     icon.impl,
211                     icon.contentDescription,
212                     iconTint,
213                     backgroundView,
214                 )
215             }
216             is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon -> {
217                 StatusBarConnectedDisplays.unsafeAssertInNewMode()
218                 val iconView = fetchStatusBarIconView(iconViewStore, icon)
219                 if (iconView == null) {
220                     // This means that the notification key doesn't exist anymore.
221                     return
222                 }
223                 setStatusBarIconView(
224                     defaultIconView,
225                     iconView,
226                     icon.contentDescription,
227                     iconTint,
228                     backgroundView,
229                 )
230             }
231         }
232     }
233 
fetchStatusBarIconViewnull234     private fun fetchStatusBarIconView(
235         iconViewStore: IconViewStore?,
236         icon: OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon,
237     ): StatusBarIconView? {
238         StatusBarConnectedDisplays.unsafeAssertInNewMode()
239         if (iconViewStore == null) {
240             throw IllegalStateException("Store should always be non-null when flag is enabled.")
241         }
242         return iconViewStore.iconView(icon.notificationKey)
243     }
244 
setStatusBarIconViewnull245     private fun setStatusBarIconView(
246         defaultIconView: ImageView,
247         iconView: StatusBarIconView,
248         iconContentDescription: ContentDescription,
249         iconTint: Int,
250         backgroundView: ChipBackgroundContainer,
251     ) {
252         // Hide the default icon since we'll show this custom icon instead.
253         defaultIconView.visibility = View.GONE
254 
255         // 1. Set up the right visual params.
256         with(iconView) {
257             id = CUSTOM_ICON_VIEW_ID
258             if (StatusBarNotifChips.isEnabled) {
259                 ContentDescriptionViewBinder.bind(iconContentDescription, this)
260             } else {
261                 contentDescription =
262                     context.resources.getString(R.string.ongoing_call_content_description)
263             }
264             tintView(iconTint)
265         }
266 
267         // 2. If we just reinflated the view, we may need to detach the icon view from the old chip
268         // before we reattach it to the new one.
269         // See also: NotificationIconContainerViewBinder#bindIcons.
270         val currentParent = iconView.parent as? ViewGroup
271         if (currentParent != null && currentParent != backgroundView) {
272             currentParent.removeView(iconView)
273             currentParent.removeTransientView(iconView)
274         }
275 
276         // 3: Add the icon as the starting view.
277         backgroundView.addView(iconView, /* index= */ 0, generateCustomIconLayoutParams(iconView))
278     }
279 
getCustomIconViewnull280     private fun View.getCustomIconView(): StatusBarIconView? {
281         return this.findViewById(CUSTOM_ICON_VIEW_ID)
282     }
283 
tintViewnull284     private fun ImageView.tintView(color: Int) {
285         this.imageTintList = ColorStateList.valueOf(color)
286     }
287 
generateCustomIconLayoutParamsnull288     private fun generateCustomIconLayoutParams(iconView: ImageView): FrameLayout.LayoutParams {
289         val customIconSize =
290             iconView.context.resources.getDimensionPixelSize(
291                 R.dimen.ongoing_activity_chip_embedded_padding_icon_size
292             )
293         return FrameLayout.LayoutParams(customIconSize, customIconSize)
294     }
295 
setChipMainContentnull296     private fun setChipMainContent(
297         chipModel: OngoingActivityChipModel.Active,
298         chipTextView: TextView,
299         chipTimeView: ChipChronometer,
300         chipShortTimeDeltaView: DateTimeView,
301     ) {
302         when (chipModel) {
303             is OngoingActivityChipModel.Active.Countdown -> {
304                 chipTextView.text = chipModel.secondsUntilStarted.toString()
305                 chipTextView.visibility = View.VISIBLE
306 
307                 chipTimeView.hide()
308                 chipShortTimeDeltaView.visibility = View.GONE
309             }
310             is OngoingActivityChipModel.Active.Text -> {
311                 chipTextView.text = chipModel.text
312                 chipTextView.visibility = View.VISIBLE
313 
314                 chipTimeView.hide()
315                 chipShortTimeDeltaView.visibility = View.GONE
316             }
317             is OngoingActivityChipModel.Active.Timer -> {
318                 ChipChronometerBinder.bind(
319                     chipModel.startTimeMs,
320                     chipModel.isEventInFuture,
321                     chipTimeView,
322                 )
323                 chipTimeView.visibility = View.VISIBLE
324 
325                 chipTextView.visibility = View.GONE
326                 chipShortTimeDeltaView.visibility = View.GONE
327             }
328             is OngoingActivityChipModel.Active.ShortTimeDelta -> {
329                 chipShortTimeDeltaView.setTime(chipModel.time)
330                 chipShortTimeDeltaView.visibility = View.VISIBLE
331                 chipShortTimeDeltaView.isShowRelativeTime = true
332                 chipShortTimeDeltaView.setRelativeTimeDisambiguationTextMask(
333                     DateTimeView.DISAMBIGUATION_TEXT_PAST
334                 )
335                 chipShortTimeDeltaView.setRelativeTimeUnitDisplayLength(
336                     DateTimeView.UNIT_DISPLAY_LENGTH_MEDIUM
337                 )
338 
339                 chipTextView.visibility = View.GONE
340                 chipTimeView.hide()
341             }
342             is OngoingActivityChipModel.Active.IconOnly -> {
343                 chipTextView.visibility = View.GONE
344                 chipShortTimeDeltaView.visibility = View.GONE
345                 chipTimeView.hide()
346             }
347         }
348     }
349 
hidenull350     private fun ChipChronometer.hide() {
351         // The Chronometer should be stopped to prevent leaks -- see b/192243808 and
352         // [Chronometer.start].
353         this.stop()
354         this.visibility = View.GONE
355     }
356 
updateChipPaddingnull357     private fun updateChipPadding(
358         chipModel: OngoingActivityChipModel.Active,
359         backgroundView: View,
360         chipTextView: TextView,
361         chipTimeView: ChipChronometer,
362         chipShortTimeDeltaView: DateTimeView,
363     ) {
364         val icon = chipModel.icon
365         if (icon != null) {
366             if (iconRequiresEmbeddedPadding(icon)) {
367                 // If the icon is a custom [StatusBarIconView], then it should've come from
368                 // `Notification.smallIcon`, which is required to embed its own paddings. We need to
369                 // adjust the other paddings to make everything look good :)
370                 backgroundView.setBackgroundPaddingForEmbeddedPaddingIcon()
371                 chipTextView.setTextPaddingForEmbeddedPaddingIcon()
372                 chipTimeView.setTextPaddingForEmbeddedPaddingIcon()
373                 chipShortTimeDeltaView.setTextPaddingForEmbeddedPaddingIcon()
374             } else {
375                 backgroundView.setBackgroundPaddingForNormalIcon()
376                 chipTextView.setTextPaddingForNormalIcon()
377                 chipTimeView.setTextPaddingForNormalIcon()
378                 chipShortTimeDeltaView.setTextPaddingForNormalIcon()
379             }
380         } else {
381             backgroundView.setBackgroundPaddingForNoIcon()
382             chipTextView.setTextPaddingForNoIcon()
383             chipTimeView.setTextPaddingForNoIcon()
384             chipShortTimeDeltaView.setTextPaddingForNoIcon()
385         }
386     }
387 
iconRequiresEmbeddedPaddingnull388     private fun iconRequiresEmbeddedPadding(icon: OngoingActivityChipModel.ChipIcon) =
389         icon is OngoingActivityChipModel.ChipIcon.StatusBarView ||
390             icon is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon
391 
392     private fun View.setTextPaddingForEmbeddedPaddingIcon() {
393         val newPaddingEnd =
394             context.resources.getDimensionPixelSize(
395                 R.dimen.ongoing_activity_chip_text_end_padding_for_embedded_padding_icon
396             )
397         setPaddingRelative(
398             // The icon should embed enough padding between the icon and time view.
399             /* start= */ 0,
400             this.paddingTop,
401             newPaddingEnd,
402             this.paddingBottom,
403         )
404     }
405 
setTextPaddingForNormalIconnull406     private fun View.setTextPaddingForNormalIcon() {
407         this.setPaddingRelative(
408             this.context.resources.getDimensionPixelSize(
409                 R.dimen.ongoing_activity_chip_icon_text_padding
410             ),
411             paddingTop,
412             // The background view will contain the right end padding.
413             /* end= */ 0,
414             paddingBottom,
415         )
416     }
417 
setTextPaddingForNoIconnull418     private fun View.setTextPaddingForNoIcon() {
419         // The background view will have even start & end paddings, so we don't want the text view
420         // to add any additional padding.
421         this.setPaddingRelative(/* start= */ 0, paddingTop, /* end= */ 0, paddingBottom)
422     }
423 
setBackgroundPaddingForEmbeddedPaddingIconnull424     private fun View.setBackgroundPaddingForEmbeddedPaddingIcon() {
425         val sidePadding =
426             if (StatusBarNotifChips.isEnabled) {
427                 context.resources.getDimensionPixelSize(
428                     R.dimen.ongoing_activity_chip_side_padding_for_embedded_padding_icon
429                 )
430             } else {
431                 context.resources.getDimensionPixelSize(
432                     R.dimen.ongoing_activity_chip_side_padding_for_embedded_padding_icon_legacy
433                 )
434             }
435         setPaddingRelative(sidePadding, paddingTop, sidePadding, paddingBottom)
436     }
437 
Viewnull438     private fun View.setBackgroundPaddingForNormalIcon() {
439         val sidePadding =
440             context.resources.getDimensionPixelSize(
441                 R.dimen.ongoing_activity_chip_side_padding_legacy
442             )
443         setPaddingRelative(sidePadding, paddingTop, sidePadding, paddingBottom)
444     }
445 
setBackgroundPaddingForNoIconnull446     private fun View.setBackgroundPaddingForNoIcon() {
447         // The padding for the normal icon is also appropriate for no icon.
448         setBackgroundPaddingForNormalIcon()
449     }
450 
setChipAccessibilitynull451     private fun setChipAccessibility(
452         chipModel: OngoingActivityChipModel.Active,
453         chipView: View,
454         chipBackgroundView: View,
455     ) {
456         when (chipModel) {
457             is OngoingActivityChipModel.Active.Countdown -> {
458                 // Set as assertive so talkback will announce the countdown
459                 chipView.accessibilityLiveRegion = View.ACCESSIBILITY_LIVE_REGION_ASSERTIVE
460             }
461             is OngoingActivityChipModel.Active.Timer,
462             is OngoingActivityChipModel.Active.Text,
463             is OngoingActivityChipModel.Active.ShortTimeDelta,
464             is OngoingActivityChipModel.Active.IconOnly -> {
465                 chipView.accessibilityLiveRegion = View.ACCESSIBILITY_LIVE_REGION_NONE
466             }
467         }
468         // Clickable chips need to be a minimum size for accessibility purposes, but let
469         // non-clickable chips be smaller.
470         val minimumWidth =
471             if (chipModel.onClickListenerLegacy != null) {
472                 chipBackgroundView.context.resources.getDimensionPixelSize(
473                     R.dimen.min_clickable_item_size
474                 )
475             } else {
476                 0
477             }
478         // The background view needs the minimum width so it only fills the area required (e.g. the
479         // 3-2-1 screen record countdown chip isn't tappable so it should have a small-width
480         // background).
481         chipBackgroundView.minimumWidth = minimumWidth
482         // The root view needs the minimum width so the second chip can hide if there isn't enough
483         // room for the chip -- see [SecondaryOngoingActivityChip].
484         chipView.minimumWidth = minimumWidth
485     }
486 
setBackgroundColorsnull487     private fun GradientDrawable.setBackgroundColors(colors: ColorsModel, context: Context) {
488         this.color = colors.background(context)
489         val outline = colors.outline(context)
490         if (outline != null) {
491             this.setStroke(
492                 context.resources.getDimensionPixelSize(
493                     R.dimen.ongoing_activity_chip_outline_width
494                 ),
495                 outline,
496             )
497         } else {
498             this.setStroke(0, /* color= */ 0)
499         }
500     }
501 
502     @IdRes private val CUSTOM_ICON_VIEW_ID = R.id.ongoing_activity_chip_custom_icon
503 }
504