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.notification.domain.interactor 18 19 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor 20 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor 21 import com.android.systemui.keyguard.shared.model.KeyguardState 22 import com.android.systemui.scene.shared.flag.SceneContainerFlag 23 import com.android.systemui.shade.domain.interactor.ShadeInteractor 24 import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository 25 import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository 26 import com.android.systemui.statusbar.notification.domain.model.TopPinnedState 27 import com.android.systemui.statusbar.notification.headsup.PinnedStatus 28 import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey 29 import javax.inject.Inject 30 import kotlinx.coroutines.flow.Flow 31 import kotlinx.coroutines.flow.combine 32 import kotlinx.coroutines.flow.distinctUntilChanged 33 import kotlinx.coroutines.flow.flatMapLatest 34 import kotlinx.coroutines.flow.flowOf 35 import kotlinx.coroutines.flow.map 36 37 class HeadsUpNotificationInteractor 38 @Inject 39 constructor( 40 private val headsUpRepository: HeadsUpRepository, 41 faceAuthInteractor: DeviceEntryFaceAuthInteractor, 42 keyguardTransitionInteractor: KeyguardTransitionInteractor, 43 notificationsKeyguardInteractor: NotificationsKeyguardInteractor, 44 shadeInteractor: ShadeInteractor, 45 ) { 46 47 /** The top-ranked heads up row, regardless of pinned state */ 48 val topHeadsUpRow: Flow<HeadsUpRowKey?> = headsUpRepository.topHeadsUpRow 49 50 /** The top-ranked heads up row, if that row is pinned */ 51 val topHeadsUpRowIfPinned: Flow<HeadsUpRowKey?> = 52 headsUpRepository.topHeadsUpRow 53 .flatMapLatest { repository -> 54 repository?.pinnedStatus?.map { pinnedStatus -> 55 repository.takeIf { pinnedStatus.isPinned } 56 } ?: flowOf(null) 57 } 58 .distinctUntilChanged() 59 60 private val activeHeadsUpRows: Flow<Set<Pair<HeadsUpRowKey, Boolean>>> by lazy { 61 if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) { 62 flowOf(emptySet()) 63 } else { 64 headsUpRepository.activeHeadsUpRows.flatMapLatest { repositories -> 65 if (repositories.isNotEmpty()) { 66 val toCombine: List<Flow<Pair<HeadsUpRowRepository, Boolean>>> = 67 repositories.map { repo -> 68 repo.pinnedStatus.map { pinnedStatus -> repo to pinnedStatus.isPinned } 69 } 70 combine(toCombine) { pairs -> pairs.toSet() } 71 } else { 72 // if the set is empty, there are no flows to combine 73 flowOf(emptySet()) 74 } 75 } 76 } 77 } 78 79 /** Set of currently active top-level heads up rows to be displayed. */ 80 val activeHeadsUpRowKeys: Flow<Set<HeadsUpRowKey>> by lazy { 81 if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) { 82 flowOf(emptySet()) 83 } else { 84 activeHeadsUpRows.map { it.map { (repo, _) -> repo }.toSet() }.distinctUntilChanged() 85 } 86 } 87 88 /** Set of currently pinned top-level heads up rows to be displayed. */ 89 val pinnedHeadsUpRowKeys: Flow<Set<HeadsUpRowKey>> by lazy { 90 if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) { 91 flowOf(emptySet()) 92 } else { 93 activeHeadsUpRows 94 .map { it.filter { (_, isPinned) -> isPinned }.map { (repo, _) -> repo }.toSet() } 95 .distinctUntilChanged() // TODO(b/402428276) stop sending duplicate updates instead 96 } 97 } 98 99 /** What [PinnedStatus] and key does the top row have? */ 100 private val topPinnedState: Flow<TopPinnedState> = 101 headsUpRepository.activeHeadsUpRows.flatMapLatest { rows -> 102 if (rows.isNotEmpty()) { 103 // For each row, emits a (key, pinnedStatus) pair each time any row's 104 // `pinnedStatus` changes 105 val toCombine: List<Flow<Pair<String, PinnedStatus>>> = 106 rows.map { row -> row.pinnedStatus.map { status -> row.key to status } } 107 combine(toCombine) { pairs -> 108 val topPinnedRow: Pair<String, PinnedStatus>? = 109 pairs.firstOrNull { it.second.isPinned } 110 if (topPinnedRow != null) { 111 TopPinnedState.Pinned( 112 key = topPinnedRow.first, 113 status = topPinnedRow.second, 114 ) 115 } else { 116 TopPinnedState.NothingPinned 117 } 118 } 119 } else { 120 flowOf(TopPinnedState.NothingPinned) 121 } 122 } 123 124 /** Are there any pinned heads up rows to display? */ 125 val hasPinnedRows: Flow<Boolean> = 126 topPinnedState.map { 127 when (it) { 128 is TopPinnedState.Pinned -> it.status.isPinned 129 is TopPinnedState.NothingPinned -> false 130 } 131 } 132 133 val isHeadsUpOrAnimatingAway: Flow<Boolean> by lazy { 134 if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) { 135 flowOf(false) 136 } else { 137 combine(hasPinnedRows, headsUpRepository.isHeadsUpAnimatingAway) { 138 hasPinnedRows, 139 animatingAway -> 140 hasPinnedRows || animatingAway 141 } 142 } 143 } 144 145 private val canShowHeadsUp: Flow<Boolean> = 146 combine( 147 faceAuthInteractor.isBypassEnabled, 148 shadeInteractor.isShadeFullyCollapsed, 149 keyguardTransitionInteractor.currentKeyguardState, 150 notificationsKeyguardInteractor.areNotificationsFullyHidden, 151 ) { isBypassEnabled, isShadeCollapsed, keyguardState, areNotificationsHidden -> 152 val isOnLockScreen = keyguardState == KeyguardState.LOCKSCREEN 153 when { 154 areNotificationsHidden -> false // don't show when notification are hidden 155 !isShadeCollapsed -> false // don't show when the shade is expanded 156 isOnLockScreen -> isBypassEnabled // on the lock screen only show for bypass 157 else -> true // show otherwise 158 } 159 } 160 161 /** 162 * Emits the pinned notification state as it relates to the status bar. Includes both the pinned 163 * status and key of the notification that's pinned (if there is a pinned notification). 164 */ 165 val statusBarHeadsUpState: Flow<TopPinnedState> = 166 combine(topPinnedState, canShowHeadsUp) { topPinnedState, canShowHeadsUp -> 167 if (canShowHeadsUp) { 168 topPinnedState 169 } else { 170 TopPinnedState.NothingPinned 171 } 172 } 173 174 /** Emits the pinned notification status as it relates to the status bar. */ 175 val statusBarHeadsUpStatus: Flow<PinnedStatus> = 176 statusBarHeadsUpState.map { 177 when (it) { 178 is TopPinnedState.Pinned -> it.status 179 is TopPinnedState.NothingPinned -> PinnedStatus.NotPinned 180 } 181 } 182 183 fun headsUpRow(key: HeadsUpRowKey): HeadsUpRowInteractor = 184 HeadsUpRowInteractor(key as HeadsUpRowRepository) 185 186 fun elementKeyFor(key: HeadsUpRowKey) = (key as HeadsUpRowRepository).elementKey 187 188 /** Returns the Notification Key (the standard string) of this row. */ 189 fun notificationKey(key: HeadsUpRowKey): String = (key as HeadsUpRowRepository).key 190 191 fun setHeadsUpAnimatingAway(animatingAway: Boolean) { 192 headsUpRepository.setHeadsUpAnimatingAway(animatingAway) 193 } 194 195 /** Snooze the currently pinned HUN. */ 196 fun snooze() { 197 headsUpRepository.snooze() 198 } 199 200 /** Unpin all currently pinned HUNs. */ 201 fun unpinAll(userUnPinned: Boolean) { 202 headsUpRepository.unpinAll(userUnPinned) 203 } 204 205 /** Notifies that the current scene transition is idle. */ 206 fun onTransitionIdle() { 207 headsUpRepository.releaseAfterExpansion() 208 } 209 } 210 211 class HeadsUpRowInteractor(repository: HeadsUpRowRepository) 212