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