• 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.notification.ui.viewmodel
18 
19 import android.content.Context
20 import android.view.View
21 import com.android.systemui.Flags
22 import com.android.systemui.common.shared.model.ContentDescription
23 import com.android.systemui.dagger.SysUISingleton
24 import com.android.systemui.dagger.qualifiers.Application
25 import com.android.systemui.dagger.qualifiers.Main
26 import com.android.systemui.res.R
27 import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor
28 import com.android.systemui.statusbar.chips.notification.domain.model.NotificationChipModel
29 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
30 import com.android.systemui.statusbar.chips.ui.model.ColorsModel
31 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
32 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
33 import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
34 import com.android.systemui.statusbar.notification.domain.model.TopPinnedState
35 import com.android.systemui.statusbar.notification.headsup.PinnedStatus
36 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
37 import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
38 import com.android.systemui.util.time.SystemClock
39 import javax.inject.Inject
40 import kotlinx.coroutines.CoroutineScope
41 import kotlinx.coroutines.flow.Flow
42 import kotlinx.coroutines.flow.combine
43 import kotlinx.coroutines.flow.distinctUntilChanged
44 import kotlinx.coroutines.launch
45 
46 /** A view model for status bar chips for promoted ongoing notifications. */
47 @SysUISingleton
48 class NotifChipsViewModel
49 @Inject
50 constructor(
51     @Main private val context: Context,
52     @Application private val applicationScope: CoroutineScope,
53     private val notifChipsInteractor: StatusBarNotificationChipsInteractor,
54     headsUpNotificationInteractor: HeadsUpNotificationInteractor,
55     private val systemClock: SystemClock,
56 ) {
57     /**
58      * A flow modeling the current notification chips. Emits an empty list if there are no
59      * notifications that are eligible to show a status bar chip.
60      */
61     val chips: Flow<List<OngoingActivityChipModel.Active>> =
62         combine(
63                 notifChipsInteractor.allNotificationChips,
64                 headsUpNotificationInteractor.statusBarHeadsUpState,
65             ) { notifications, headsUpState ->
66                 notifications.map { it.toActivityChipModel(headsUpState) }
67             }
68             .distinctUntilChanged()
69 
70     /** Converts the notification to the [OngoingActivityChipModel] object. */
71     private fun NotificationChipModel.toActivityChipModel(
72         headsUpState: TopPinnedState
73     ): OngoingActivityChipModel.Active {
74         StatusBarNotifChips.unsafeAssertInNewMode()
75         // Chips are never shown when locked, so it's safe to use the version with sensitive content
76         val chipContent = promotedContent.privateVersion
77         val contentDescription = getContentDescription(this.appName)
78         val icon =
79             if (this.statusBarChipIconView != null) {
80                 StatusBarConnectedDisplays.assertInLegacyMode()
81                 OngoingActivityChipModel.ChipIcon.StatusBarView(
82                     this.statusBarChipIconView,
83                     contentDescription,
84                 )
85             } else {
86                 StatusBarConnectedDisplays.unsafeAssertInNewMode()
87                 OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon(
88                     this.key,
89                     contentDescription,
90                 )
91             }
92         val colors = ColorsModel.SystemThemed
93         val clickListener: () -> Unit = {
94             // The notification pipeline needs everything to run on the main thread, so keep
95             // this event on the main thread.
96             applicationScope.launch {
97                 // TODO(b/364653005): Move accessibility focus to the HUN when chip is tapped.
98                 notifChipsInteractor.onPromotedNotificationChipTapped(this@toActivityChipModel.key)
99             }
100         }
101         // If the app that posted this notification is visible, we want to hide the chip
102         // because information between the status bar chip and the app itself could be
103         // out-of-sync (like a timer that's slightly off)
104         val isHidden = this.isAppVisible
105         val onClickListenerLegacy =
106             View.OnClickListener {
107                 StatusBarChipsModernization.assertInLegacyMode()
108                 clickListener.invoke()
109             }
110         val clickBehavior =
111             OngoingActivityChipModel.ClickBehavior.ShowHeadsUpNotification({
112                 StatusBarChipsModernization.unsafeAssertInNewMode()
113                 clickListener.invoke()
114             })
115 
116         val isShowingHeadsUpFromChipTap =
117             headsUpState is TopPinnedState.Pinned &&
118                 headsUpState.status == PinnedStatus.PinnedByUser &&
119                 headsUpState.key == this.key
120         if (isShowingHeadsUpFromChipTap) {
121             // If the user tapped this chip to show the HUN, we want to just show the icon because
122             // the HUN will show the rest of the information.
123             return OngoingActivityChipModel.Active.IconOnly(
124                 key = this.key,
125                 icon = icon,
126                 colors = colors,
127                 onClickListenerLegacy = onClickListenerLegacy,
128                 clickBehavior = clickBehavior,
129                 isHidden = isHidden,
130                 instanceId = instanceId,
131             )
132         }
133 
134         if (chipContent.shortCriticalText != null) {
135             return OngoingActivityChipModel.Active.Text(
136                 key = this.key,
137                 icon = icon,
138                 colors = colors,
139                 text = chipContent.shortCriticalText,
140                 onClickListenerLegacy = onClickListenerLegacy,
141                 clickBehavior = clickBehavior,
142                 isHidden = isHidden,
143                 instanceId = instanceId,
144             )
145         }
146 
147         if (Flags.promoteNotificationsAutomatically() && chipContent.wasPromotedAutomatically) {
148             // When we're promoting notifications automatically, the `when` time set on the
149             // notification will likely just be set to the current time, which would cause the chip
150             // to always show "now". We don't want early testers to get that experience since it's
151             // not what will happen at launch, so just don't show any time.onometerstate
152             return OngoingActivityChipModel.Active.IconOnly(
153                 key = this.key,
154                 icon = icon,
155                 colors = colors,
156                 onClickListenerLegacy = onClickListenerLegacy,
157                 clickBehavior = clickBehavior,
158                 isHidden = isHidden,
159                 instanceId = instanceId,
160             )
161         }
162 
163         if (chipContent.time == null) {
164             return OngoingActivityChipModel.Active.IconOnly(
165                 key = this.key,
166                 icon = icon,
167                 colors = colors,
168                 onClickListenerLegacy = onClickListenerLegacy,
169                 clickBehavior = clickBehavior,
170                 isHidden = isHidden,
171                 instanceId = instanceId,
172             )
173         }
174 
175         when (chipContent.time) {
176             is PromotedNotificationContentModel.When.Time -> {
177                 return if (
178                     chipContent.time.currentTimeMillis >=
179                         systemClock.currentTimeMillis() + FUTURE_TIME_THRESHOLD_MILLIS
180                 ) {
181                     OngoingActivityChipModel.Active.ShortTimeDelta(
182                         key = this.key,
183                         icon = icon,
184                         colors = colors,
185                         time = chipContent.time.currentTimeMillis,
186                         onClickListenerLegacy = onClickListenerLegacy,
187                         clickBehavior = clickBehavior,
188                         isHidden = isHidden,
189                         instanceId = instanceId,
190                     )
191                 } else {
192                     // Don't show a `when` time that's close to now or in the past because it's
193                     // likely that the app didn't intentionally set the `when` time to be shown in
194                     // the status bar chip.
195                     // TODO(b/393369213): If a notification sets a `when` time in the future and
196                     // then that time comes and goes, the chip *will* start showing times in the
197                     // past. Not going to fix this right now because the Compose implementation
198                     // automatically handles this for us and we're hoping to launch the notification
199                     // chips at the same time as the Compose chips.
200                     return OngoingActivityChipModel.Active.IconOnly(
201                         key = this.key,
202                         icon = icon,
203                         colors = colors,
204                         onClickListenerLegacy = onClickListenerLegacy,
205                         clickBehavior = clickBehavior,
206                         isHidden = isHidden,
207                         instanceId = instanceId,
208                     )
209                 }
210             }
211             is PromotedNotificationContentModel.When.Chronometer -> {
212                 return OngoingActivityChipModel.Active.Timer(
213                     key = this.key,
214                     icon = icon,
215                     colors = colors,
216                     startTimeMs = chipContent.time.elapsedRealtimeMillis,
217                     isEventInFuture = chipContent.time.isCountDown,
218                     onClickListenerLegacy = onClickListenerLegacy,
219                     clickBehavior = clickBehavior,
220                     isHidden = isHidden,
221                     instanceId = instanceId,
222                 )
223             }
224         }
225     }
226 
227     private fun getContentDescription(appName: String): ContentDescription {
228         val ongoingDescription =
229             context.getString(R.string.ongoing_notification_extra_content_description)
230         return ContentDescription.Loaded(
231             context.getString(
232                 R.string.accessibility_desc_notification_icon,
233                 appName,
234                 ongoingDescription,
235             )
236         )
237     }
238 
239     companion object {
240         /**
241          * Notifications must have a `when` time of at least 1 minute in the future in order for the
242          * status bar chip to show the time.
243          */
244         private const val FUTURE_TIME_THRESHOLD_MILLIS = 60 * 1000
245     }
246 }
247