• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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.viewmodel
18 
19 import android.content.res.Configuration
20 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
21 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
22 import com.android.systemui.dagger.SysUISingleton
23 import com.android.systemui.dagger.qualifiers.Background
24 import com.android.systemui.log.LogBuffer
25 import com.android.systemui.log.core.LogLevel
26 import com.android.systemui.statusbar.chips.StatusBarChipLogTags.pad
27 import com.android.systemui.statusbar.chips.StatusBarChipsLog
28 import com.android.systemui.statusbar.chips.call.ui.viewmodel.CallChipViewModel
29 import com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel.CastToOtherDeviceChipViewModel
30 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
31 import com.android.systemui.statusbar.chips.notification.ui.viewmodel.NotifChipsViewModel
32 import com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel.ScreenRecordChipViewModel
33 import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.ShareToAppChipViewModel
34 import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel
35 import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModelLegacy
36 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
37 import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
38 import com.android.systemui.util.kotlin.pairwise
39 import javax.inject.Inject
40 import kotlinx.coroutines.CoroutineScope
41 import kotlinx.coroutines.flow.Flow
42 import kotlinx.coroutines.flow.MutableStateFlow
43 import kotlinx.coroutines.flow.SharingStarted
44 import kotlinx.coroutines.flow.StateFlow
45 import kotlinx.coroutines.flow.asStateFlow
46 import kotlinx.coroutines.flow.combine
47 import kotlinx.coroutines.flow.distinctUntilChanged
48 import kotlinx.coroutines.flow.map
49 import kotlinx.coroutines.flow.onEach
50 import kotlinx.coroutines.flow.stateIn
51 
52 /**
53  * View model deciding which ongoing activity chip to show in the status bar.
54  *
55  * There may be multiple ongoing activities at the same time, but we can only ever show one chip at
56  * any one time (for now). This class decides which ongoing activity to show if there are multiple.
57  */
58 @SysUISingleton
59 class OngoingActivityChipsViewModel
60 @Inject
61 constructor(
62     @Background scope: CoroutineScope,
63     screenRecordChipViewModel: ScreenRecordChipViewModel,
64     shareToAppChipViewModel: ShareToAppChipViewModel,
65     castToOtherDeviceChipViewModel: CastToOtherDeviceChipViewModel,
66     callChipViewModel: CallChipViewModel,
67     notifChipsViewModel: NotifChipsViewModel,
68     displayStateInteractor: DisplayStateInteractor,
69     configurationInteractor: ConfigurationInteractor,
70     @StatusBarChipsLog private val logger: LogBuffer,
71 ) {
72     private val isLandscape: Flow<Boolean> =
73         configurationInteractor.configurationValues
74             .map { it.isLandscape }
75             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
76 
77     private val isScreenReasonablyLarge: Flow<Boolean> =
78         combine(isLandscape, displayStateInteractor.isLargeScreen) { isLandscape, isLargeScreen ->
79                 isLandscape || isLargeScreen
80             }
81             .distinctUntilChanged()
82             .onEach {
83                 logger.log(
84                     TAG,
85                     LogLevel.DEBUG,
86                     { bool1 = it },
87                     { "isScreenReasonablyLarge: $bool1" },
88                 )
89             }
90             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
91 
92     private enum class ChipType {
93         ScreenRecord,
94         ShareToApp,
95         CastToOtherDevice,
96         Call,
97         Notification,
98     }
99 
100     /** Model that helps us internally track the various chip states from each of the types. */
101     @Deprecated("Since StatusBarChipsModernization, this isn't used anymore")
102     private sealed interface InternalChipModel {
103         /**
104          * Represents that we've internally decided to show the chip with type [type] with the given
105          * [model] information.
106          */
107         data class Active(val type: ChipType, val model: OngoingActivityChipModel.Active) :
108             InternalChipModel
109 
110         /**
111          * Represents that all chip types would like to be hidden. Each value specifies *how* that
112          * chip type should get hidden.
113          */
114         data class Inactive(
115             val screenRecord: OngoingActivityChipModel.Inactive,
116             val shareToApp: OngoingActivityChipModel.Inactive,
117             val castToOtherDevice: OngoingActivityChipModel.Inactive,
118             val call: OngoingActivityChipModel.Inactive,
119             val notifs: OngoingActivityChipModel.Inactive,
120         ) : InternalChipModel
121     }
122 
123     private data class ChipBundle(
124         val screenRecord: OngoingActivityChipModel = OngoingActivityChipModel.Inactive(),
125         val shareToApp: OngoingActivityChipModel = OngoingActivityChipModel.Inactive(),
126         val castToOtherDevice: OngoingActivityChipModel = OngoingActivityChipModel.Inactive(),
127         val call: OngoingActivityChipModel = OngoingActivityChipModel.Inactive(),
128         val notifs: List<OngoingActivityChipModel.Active> = emptyList(),
129     )
130 
131     /** Bundles all the incoming chips into one object to easily pass to various flows. */
132     private val incomingChipBundle =
133         combine(
134                 screenRecordChipViewModel.chip,
135                 shareToAppChipViewModel.chip,
136                 castToOtherDeviceChipViewModel.chip,
137                 callChipViewModel.chip,
138                 notifChipsViewModel.chips,
139             ) { screenRecord, shareToApp, castToOtherDevice, call, notifs ->
140                 logger.log(
141                     TAG,
142                     LogLevel.INFO,
143                     {
144                         str1 = screenRecord.logName
145                         str2 = shareToApp.logName
146                         str3 = castToOtherDevice.logName
147                     },
148                     { "Chips: ScreenRecord=$str1 > ShareToApp=$str2 > CastToOther=$str3..." },
149                 )
150                 logger.log(
151                     TAG,
152                     LogLevel.INFO,
153                     {
154                         str1 = call.logName
155                         // TODO(b/364653005): Log other information for notification chips.
156                         str2 = notifs.map { it.logName }.toString()
157                     },
158                     { "... > Call=$str1 > Notifs=$str2" },
159                 )
160                 ChipBundle(
161                     screenRecord = screenRecord,
162                     shareToApp = shareToApp,
163                     castToOtherDevice = castToOtherDevice,
164                     call = call,
165                     notifs = notifs,
166                 )
167             }
168             // Some of the chips could have timers in them and we don't want the start time
169             // for those timers to get reset for any reason. So, as soon as any subscriber has
170             // requested the chip information, we maintain it forever by using
171             // [SharingStarted.Lazily]. See b/347726238.
172             .stateIn(scope, SharingStarted.Lazily, ChipBundle())
173 
174     private val internalChip: Flow<InternalChipModel> =
175         incomingChipBundle.map { bundle -> pickMostImportantChip(bundle).mostImportantChip }
176 
177     /**
178      * A flow modeling the primary chip that should be shown in the status bar after accounting for
179      * possibly multiple ongoing activities and animation requirements.
180      *
181      * [com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment] is responsible for
182      * actually displaying the chip.
183      */
184     val primaryChip: StateFlow<OngoingActivityChipModel> =
185         internalChip
186             .pairwise(initialValue = DEFAULT_INTERNAL_INACTIVE_MODEL)
187             .map { (old, new) -> createOutputModel(old, new) }
188             .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Inactive())
189 
190     /**
191      * Equivalent to [MultipleOngoingActivityChipsModelLegacy] but using the internal models to do
192      * some state tracking before we get the final output.
193      */
194     @Deprecated("Since StatusBarChipsModernization, this isn't used anymore")
195     private data class InternalMultipleOngoingActivityChipsModel(
196         val primary: InternalChipModel,
197         val secondary: InternalChipModel,
198     )
199 
200     private val internalChips: Flow<InternalMultipleOngoingActivityChipsModel> =
201         combine(incomingChipBundle, isScreenReasonablyLarge) { bundle, isScreenReasonablyLarge ->
202             // First: Find the most important chip.
203             val primaryChipResult = pickMostImportantChip(bundle)
204             when (val primaryChip = primaryChipResult.mostImportantChip) {
205                 is InternalChipModel.Inactive -> {
206                     // If the primary chip is hidden, the secondary chip will also be hidden, so
207                     // just pass the same Hidden model for both.
208                     InternalMultipleOngoingActivityChipsModel(primaryChip, primaryChip)
209                 }
210                 is InternalChipModel.Active -> {
211                     // Otherwise: Find the next most important chip.
212                     val secondaryChip =
213                         pickMostImportantChip(primaryChipResult.remainingChips).mostImportantChip
214                     if (
215                         secondaryChip is InternalChipModel.Active &&
216                             StatusBarNotifChips.isEnabled &&
217                             !isScreenReasonablyLarge
218                     ) {
219                         // If we have two showing chips and we don't have a ton of room
220                         // (!isScreenReasonablyLarge), then we want to make both of them as small as
221                         // possible so that we have the highest chance of showing both chips (as
222                         // opposed to showing the primary chip with a lot of text and completely
223                         // hiding the secondary chip).
224                         // TODO(b/392895330): If StatusBarChipsModernization is enabled, do the
225                         // squishing in Compose instead, and be smart about it (e.g. if we have
226                         // room for the first chip to show text and the second chip to be icon-only,
227                         // do that instead of always squishing both chips.)
228                         InternalMultipleOngoingActivityChipsModel(
229                             primaryChip.squish(),
230                             secondaryChip.squish(),
231                         )
232                     } else {
233                         InternalMultipleOngoingActivityChipsModel(primaryChip, secondaryChip)
234                     }
235                 }
236             }
237         }
238 
239     /** Squishes the chip down to the smallest content possible. */
240     private fun InternalChipModel.Active.squish(): InternalChipModel.Active {
241         return if (model.shouldSquish()) {
242             InternalChipModel.Active(this.type, this.model.toIconOnly())
243         } else {
244             this
245         }
246     }
247 
248     private fun OngoingActivityChipModel.Active.shouldSquish(): Boolean {
249         return when (this) {
250             // Icon-only is already maximum squished
251             is OngoingActivityChipModel.Active.IconOnly,
252             // Countdown shows just a single digit, so already maximum squished
253             is OngoingActivityChipModel.Active.Countdown -> false
254             // The other chips have icon+text, so we can squish them by hiding text
255             is OngoingActivityChipModel.Active.Timer,
256             is OngoingActivityChipModel.Active.ShortTimeDelta,
257             is OngoingActivityChipModel.Active.Text -> true
258         }
259     }
260 
261     private fun OngoingActivityChipModel.Active.toIconOnly(): OngoingActivityChipModel.Active {
262         // If this chip doesn't have an icon, then it only has text and we should continue showing
263         // its text. (This is theoretically impossible because
264         // [OngoingActivityChipModel.Active.Countdown] is the only chip without an icon and
265         // [shouldSquish] returns false for that model, but protect against it just in case.)
266         val currentIcon = icon ?: return this
267         // TODO(b/364653005): Make sure every field is copied over.
268         return OngoingActivityChipModel.Active.IconOnly(
269             key = key,
270             isImportantForPrivacy = isImportantForPrivacy,
271             icon = currentIcon,
272             colors = colors,
273             onClickListenerLegacy = onClickListenerLegacy,
274             clickBehavior = clickBehavior,
275             instanceId = instanceId,
276         )
277     }
278 
279     /**
280      * A flow modeling the active and inactive chips as well as which should be shown in the status
281      * bar after accounting for possibly multiple ongoing activities and animation requirements.
282      */
283     val chips: StateFlow<MultipleOngoingActivityChipsModel> =
284         if (StatusBarChipsModernization.isEnabled) {
285             combine(
286                     incomingChipBundle.map { bundle -> rankChips(bundle) },
287                     isScreenReasonablyLarge,
288                 ) { rankedChips, isScreenReasonablyLarge ->
289                     if (
290                         StatusBarNotifChips.isEnabled &&
291                             !isScreenReasonablyLarge &&
292                             rankedChips.active.filter { !it.isHidden }.size >= 2
293                     ) {
294                         // If we have at least two showing chips and we don't have a ton of room
295                         // (!isScreenReasonablyLarge), then we want to make both of them as small as
296                         // possible so that we have the highest chance of showing both chips (as
297                         // opposed to showing the first chip with a lot of text and completely
298                         // hiding the other chips).
299                         val squishedActiveChips =
300                             rankedChips.active.map {
301                                 if (!it.isHidden && it.shouldSquish()) {
302                                     it.toIconOnly()
303                                 } else {
304                                     it
305                                 }
306                             }
307 
308                         MultipleOngoingActivityChipsModel(
309                             active = squishedActiveChips,
310                             overflow = rankedChips.overflow,
311                             inactive = rankedChips.inactive,
312                         )
313                     } else {
314                         rankedChips
315                     }
316                 }
317                 .stateIn(scope, SharingStarted.Lazily, MultipleOngoingActivityChipsModel())
318         } else {
319             MutableStateFlow(MultipleOngoingActivityChipsModel()).asStateFlow()
320         }
321 
322     /**
323      * A flow modeling the primary chip that should be shown in the status bar after accounting for
324      * possibly multiple ongoing activities and animation requirements.
325      *
326      * [com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment] is responsible for
327      * actually displaying the chip.
328      *
329      * Deprecated: since StatusBarChipsModernization, use the new [chips] instead.
330      */
331     val chipsLegacy: StateFlow<MultipleOngoingActivityChipsModelLegacy> =
332         if (StatusBarChipsModernization.isEnabled) {
333             MutableStateFlow(MultipleOngoingActivityChipsModelLegacy()).asStateFlow()
334         } else if (!StatusBarNotifChips.isEnabled) {
335             // Multiple chips are only allowed with notification chips. If the flag isn't on, use
336             // just the primary chip.
337             primaryChip
338                 .map {
339                     MultipleOngoingActivityChipsModelLegacy(
340                         primary = it,
341                         secondary = OngoingActivityChipModel.Inactive(),
342                     )
343                 }
344                 .stateIn(scope, SharingStarted.Lazily, MultipleOngoingActivityChipsModelLegacy())
345         } else {
346             internalChips
347                 .pairwise(initialValue = DEFAULT_MULTIPLE_INTERNAL_INACTIVE_MODEL)
348                 .map { (old, new) ->
349                     val correctPrimary = createOutputModel(old.primary, new.primary)
350                     val correctSecondary = createOutputModel(old.secondary, new.secondary)
351                     MultipleOngoingActivityChipsModelLegacy(correctPrimary, correctSecondary)
352                 }
353                 .stateIn(scope, SharingStarted.Lazily, MultipleOngoingActivityChipsModelLegacy())
354         }
355 
356     private val activeChips =
357         if (StatusBarChipsModernization.isEnabled) {
358             chips.map { it.active }
359         } else {
360             chipsLegacy.map {
361                 val list = mutableListOf<OngoingActivityChipModel.Active>()
362                 if (it.primary is OngoingActivityChipModel.Active) {
363                     list.add(it.primary)
364                 }
365                 if (it.secondary is OngoingActivityChipModel.Active) {
366                     list.add(it.secondary)
367                 }
368                 list
369             }
370         }
371 
372     /** A flow modeling just the keys for the currently visible chips. */
373     val visibleChipKeys: Flow<List<String>> =
374         activeChips.map { chips -> chips.filter { !it.isHidden }.map { it.key } }
375 
376     /**
377      * Sort the given chip [bundle] in order of priority, and divide the chips between active,
378      * overflow, and inactive (see [MultipleOngoingActivityChipsModel] for a description of each).
379      */
380     // IMPORTANT: PromotedNotificationsInteractor re-implements this same ordering scheme. Any
381     // changes here should also be made in PromotedNotificationsInteractor.
382     // TODO(b/402471288): Create a single source of truth for the ordering.
383     private fun rankChips(bundle: ChipBundle): MultipleOngoingActivityChipsModel {
384         val activeChips = mutableListOf<OngoingActivityChipModel.Active>()
385         val overflowChips = mutableListOf<OngoingActivityChipModel.Active>()
386         val inactiveChips = mutableListOf<OngoingActivityChipModel.Inactive>()
387 
388         val sortedChips =
389             mutableListOf(
390                     bundle.screenRecord,
391                     bundle.shareToApp,
392                     bundle.castToOtherDevice,
393                     bundle.call,
394                 )
395                 .apply { bundle.notifs.forEach { add(it) } }
396 
397         var shownSlotsRemaining = MAX_VISIBLE_CHIPS
398         for (chip in sortedChips) {
399             when (chip) {
400                 is OngoingActivityChipModel.Active -> {
401                     // Screen recording also activates the media projection APIs, which means that
402                     // whenever the screen recording chip is active, the share-to-app chip would
403                     // also be active. (Screen recording is a special case of share-to-app, where
404                     // the app receiving the share is specifically System UI.)
405                     // We want only the screen-recording-specific chip to be shown in this case. If
406                     // we did have screen recording as the primary chip, we need to suppress the
407                     // share-to-app chip to make sure they don't both show.
408                     // See b/296461748.
409                     val suppressShareToApp =
410                         chip == bundle.shareToApp &&
411                             bundle.screenRecord is OngoingActivityChipModel.Active
412                     if (shownSlotsRemaining > 0 && !suppressShareToApp) {
413                         activeChips.add(chip)
414                         if (!chip.isHidden) shownSlotsRemaining--
415                     } else {
416                         overflowChips.add(chip)
417                     }
418                 }
419 
420                 is OngoingActivityChipModel.Inactive -> inactiveChips.add(chip)
421             }
422         }
423 
424         return MultipleOngoingActivityChipsModel(activeChips, overflowChips, inactiveChips)
425     }
426 
427     /** A data class representing the return result of [pickMostImportantChip]. */
428     private data class MostImportantChipResult(
429         val mostImportantChip: InternalChipModel,
430         val remainingChips: ChipBundle,
431     )
432 
433     /**
434      * Finds the most important chip from the given [bundle].
435      *
436      * This function returns that most important chip, and it also returns any remaining chips that
437      * still want to be shown after filtering out the most important chip.
438      */
439     private fun pickMostImportantChip(bundle: ChipBundle): MostImportantChipResult {
440         // This `when` statement shows the priority order of the chips.
441         return when {
442             bundle.screenRecord is OngoingActivityChipModel.Active ->
443                 MostImportantChipResult(
444                     mostImportantChip =
445                         InternalChipModel.Active(ChipType.ScreenRecord, bundle.screenRecord),
446                     remainingChips =
447                         bundle.copy(
448                             screenRecord = OngoingActivityChipModel.Inactive(),
449                             // Screen recording also activates the media projection APIs, which
450                             // means that whenever the screen recording chip is active, the
451                             // share-to-app chip would also be active. (Screen recording is a
452                             // special case of share-to-app, where the app receiving the share is
453                             // specifically System UI.)
454                             // We want only the screen-recording-specific chip to be shown in this
455                             // case. If we did have screen recording as the primary chip, we need to
456                             // suppress the share-to-app chip to make sure they don't both show.
457                             // See b/296461748.
458                             shareToApp = OngoingActivityChipModel.Inactive(),
459                         ),
460                 )
461             bundle.shareToApp is OngoingActivityChipModel.Active ->
462                 MostImportantChipResult(
463                     mostImportantChip =
464                         InternalChipModel.Active(ChipType.ShareToApp, bundle.shareToApp),
465                     remainingChips = bundle.copy(shareToApp = OngoingActivityChipModel.Inactive()),
466                 )
467             bundle.castToOtherDevice is OngoingActivityChipModel.Active ->
468                 MostImportantChipResult(
469                     mostImportantChip =
470                         InternalChipModel.Active(
471                             ChipType.CastToOtherDevice,
472                             bundle.castToOtherDevice,
473                         ),
474                     remainingChips =
475                         bundle.copy(castToOtherDevice = OngoingActivityChipModel.Inactive()),
476                 )
477             bundle.call is OngoingActivityChipModel.Active ->
478                 MostImportantChipResult(
479                     mostImportantChip = InternalChipModel.Active(ChipType.Call, bundle.call),
480                     remainingChips = bundle.copy(call = OngoingActivityChipModel.Inactive()),
481                 )
482             bundle.notifs.isNotEmpty() ->
483                 MostImportantChipResult(
484                     mostImportantChip =
485                         InternalChipModel.Active(ChipType.Notification, bundle.notifs.first()),
486                     remainingChips =
487                         bundle.copy(notifs = bundle.notifs.subList(1, bundle.notifs.size)),
488                 )
489             else -> {
490                 // We should only get here if all chip types are hidden
491                 check(bundle.screenRecord is OngoingActivityChipModel.Inactive)
492                 check(bundle.shareToApp is OngoingActivityChipModel.Inactive)
493                 check(bundle.castToOtherDevice is OngoingActivityChipModel.Inactive)
494                 check(bundle.call is OngoingActivityChipModel.Inactive)
495                 check(bundle.notifs.isEmpty())
496                 MostImportantChipResult(
497                     mostImportantChip =
498                         InternalChipModel.Inactive(
499                             screenRecord = bundle.screenRecord,
500                             shareToApp = bundle.shareToApp,
501                             castToOtherDevice = bundle.castToOtherDevice,
502                             call = bundle.call,
503                             notifs = OngoingActivityChipModel.Inactive(),
504                         ),
505                     // All the chips are already hidden, so no need to filter anything out of the
506                     // bundle.
507                     remainingChips = bundle,
508                 )
509             }
510         }
511     }
512 
513     private fun createOutputModel(
514         old: InternalChipModel,
515         new: InternalChipModel,
516     ): OngoingActivityChipModel {
517         return if (old is InternalChipModel.Active && new is InternalChipModel.Inactive) {
518             // If we're transitioning from showing the chip to hiding the chip, different
519             // chips require different animation behaviors. For example, the screen share
520             // chips shouldn't animate if the user stopped the screen share from the dialog
521             // (see b/353249803#comment4), but the call chip should always animate.
522             //
523             // This `when` block makes sure that when we're transitioning from Active to
524             // Inactive, we check what chip type was previously showing and we use that chip
525             // type's hide animation behavior.
526             return when (old.type) {
527                 ChipType.ScreenRecord -> new.screenRecord
528                 ChipType.ShareToApp -> new.shareToApp
529                 ChipType.CastToOtherDevice -> new.castToOtherDevice
530                 ChipType.Call -> new.call
531                 ChipType.Notification -> new.notifs
532             }
533         } else if (new is InternalChipModel.Active) {
534             // If we have a chip to show, always show it.
535             new.model
536         } else {
537             // In the Hidden -> Hidden transition, it shouldn't matter which hidden model we
538             // choose because no animation should happen regardless.
539             OngoingActivityChipModel.Inactive()
540         }
541     }
542 
543     private val Configuration.isLandscape: Boolean
544         get() = orientation == Configuration.ORIENTATION_LANDSCAPE
545 
546     companion object {
547         private val TAG = "ChipsViewModel".pad()
548 
549         private val DEFAULT_INTERNAL_INACTIVE_MODEL =
550             InternalChipModel.Inactive(
551                 screenRecord = OngoingActivityChipModel.Inactive(),
552                 shareToApp = OngoingActivityChipModel.Inactive(),
553                 castToOtherDevice = OngoingActivityChipModel.Inactive(),
554                 call = OngoingActivityChipModel.Inactive(),
555                 notifs = OngoingActivityChipModel.Inactive(),
556             )
557 
558         private val DEFAULT_MULTIPLE_INTERNAL_INACTIVE_MODEL =
559             InternalMultipleOngoingActivityChipsModel(
560                 primary = DEFAULT_INTERNAL_INACTIVE_MODEL,
561                 secondary = DEFAULT_INTERNAL_INACTIVE_MODEL,
562             )
563 
564         private const val MAX_VISIBLE_CHIPS = 3
565     }
566 }
567