1 /* 2 * Copyright (C) 2019 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 package com.android.systemui.statusbar.notification.collection.coordinator 17 18 import android.app.Notification 19 import android.app.Notification.FLAG_FOREGROUND_SERVICE 20 import android.app.NotificationChannel.SYSTEM_RESERVED_IDS 21 import android.app.NotificationManager 22 import android.app.PendingIntent 23 import android.app.Person 24 import android.content.Intent 25 import android.graphics.Color 26 import android.platform.test.annotations.DisableFlags 27 import android.platform.test.annotations.EnableFlags 28 import android.testing.TestableLooper.RunWithLooper 29 import androidx.test.ext.junit.runners.AndroidJUnit4 30 import androidx.test.filters.SmallTest 31 import com.android.systemui.SysuiTestCase 32 import com.android.systemui.kosmos.applicationCoroutineScope 33 import com.android.systemui.kosmos.collectLastValue 34 import com.android.systemui.kosmos.runTest 35 import com.android.systemui.kosmos.useUnconfinedTestDispatcher 36 import com.android.systemui.mediaprojection.data.model.MediaProjectionState 37 import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository 38 import com.android.systemui.screenrecord.data.model.ScreenRecordModel 39 import com.android.systemui.screenrecord.data.repository.screenRecordRepository 40 import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor 41 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips 42 import com.android.systemui.statusbar.core.StatusBarRootModernization 43 import com.android.systemui.statusbar.notification.collection.buildEntry 44 import com.android.systemui.statusbar.notification.collection.buildNotificationEntry 45 import com.android.systemui.statusbar.notification.collection.buildOngoingCallEntry 46 import com.android.systemui.statusbar.notification.collection.buildPromotedOngoingEntry 47 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter 48 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner 49 import com.android.systemui.statusbar.notification.collection.makeClassifiedConversation 50 import com.android.systemui.statusbar.notification.collection.notifPipeline 51 import com.android.systemui.statusbar.notification.domain.interactor.renderNotificationListInteractor 52 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi 53 import com.android.systemui.statusbar.notification.promoted.domain.interactor.promotedNotificationsInteractor 54 import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization 55 import com.android.systemui.testKosmos 56 import com.android.systemui.util.mockito.withArgCaptor 57 import com.google.common.truth.Truth.assertThat 58 import org.junit.Assert.assertFalse 59 import org.junit.Assert.assertTrue 60 import org.junit.Before 61 import org.junit.Test 62 import org.junit.runner.RunWith 63 import org.mockito.kotlin.any 64 import org.mockito.kotlin.never 65 import org.mockito.kotlin.verify 66 67 @SmallTest 68 @RunWith(AndroidJUnit4::class) 69 @RunWithLooper 70 class ColorizedFgsCoordinatorTest : SysuiTestCase() { 71 private val kosmos = testKosmos().useUnconfinedTestDispatcher() 72 private val notifPipeline 73 get() = kosmos.notifPipeline 74 75 private lateinit var colorizedFgsCoordinator: ColorizedFgsCoordinator 76 private lateinit var sectioner: NotifSectioner 77 78 @Before setupnull79 fun setup() { 80 allowTestableLooperAsMainThread() 81 82 kosmos.statusBarNotificationChipsInteractor.start() 83 84 colorizedFgsCoordinator = 85 ColorizedFgsCoordinator( 86 kosmos.applicationCoroutineScope, 87 kosmos.promotedNotificationsInteractor, 88 ) 89 colorizedFgsCoordinator.attach(notifPipeline) 90 sectioner = colorizedFgsCoordinator.sectioner 91 } 92 93 @Test testIncludeFGSInSection_importanceDefaultnull94 fun testIncludeFGSInSection_importanceDefault() { 95 // GIVEN the notification represents a colorized foreground service with > min importance 96 val entry = buildEntry { 97 setFlag(mContext, Notification.FLAG_FOREGROUND_SERVICE, true) 98 setImportance(NotificationManager.IMPORTANCE_DEFAULT) 99 modifyNotification(mContext).setColorized(true).setColor(Color.WHITE) 100 } 101 102 // THEN the entry is in the fgs section 103 assertTrue(sectioner.isInSection(entry)) 104 } 105 106 @Test testSectioner_reject_classifiedConversationnull107 fun testSectioner_reject_classifiedConversation() { 108 kosmos.runTest { 109 for (id in SYSTEM_RESERVED_IDS) { 110 assertFalse(sectioner.isInSection(kosmos.makeClassifiedConversation(id))) 111 } 112 } 113 } 114 115 @Test testDiscludeFGSInSection_importanceMinnull116 fun testDiscludeFGSInSection_importanceMin() { 117 // GIVEN the notification represents a colorized foreground service with min importance 118 val entry = buildEntry { 119 setFlag(mContext, Notification.FLAG_FOREGROUND_SERVICE, true) 120 setImportance(NotificationManager.IMPORTANCE_MIN) 121 modifyNotification(mContext).setColorized(true).setColor(Color.WHITE) 122 } 123 124 // THEN the entry is NOT in the fgs section 125 assertFalse(sectioner.isInSection(entry)) 126 } 127 128 @Test testDiscludeNonFGSInSectionnull129 fun testDiscludeNonFGSInSection() { 130 // GIVEN the notification represents a colorized notification with high importance that 131 // is NOT a foreground service 132 val entry = buildEntry { 133 setImportance(NotificationManager.IMPORTANCE_HIGH) 134 setFlag(mContext, Notification.FLAG_FOREGROUND_SERVICE, false) 135 modifyNotification(mContext).setColorized(false) 136 } 137 138 // THEN the entry is NOT in the fgs section 139 assertFalse(sectioner.isInSection(entry)) 140 } 141 142 @Test testIncludeCallInSection_importanceDefaultnull143 fun testIncludeCallInSection_importanceDefault() { 144 // GIVEN the notification represents a call with > min importance 145 val entry = buildEntry { 146 setImportance(NotificationManager.IMPORTANCE_DEFAULT) 147 modifyNotification(mContext).setStyle(makeCallStyle()) 148 } 149 150 // THEN the entry is in the fgs section 151 assertTrue(sectioner.isInSection(entry)) 152 } 153 154 @Test testDiscludeCallInSection_importanceMinnull155 fun testDiscludeCallInSection_importanceMin() { 156 // GIVEN the notification represents a call with min importance 157 val entry = buildEntry { 158 setImportance(NotificationManager.IMPORTANCE_MIN) 159 modifyNotification(mContext).setStyle(makeCallStyle()) 160 } 161 162 // THEN the entry is NOT in the fgs section 163 assertFalse(sectioner.isInSection(entry)) 164 } 165 166 @Test 167 @EnableFlags(PromotedNotificationUi.FLAG_NAME) testIncludePromotedOngoingInSection_flagEnablednull168 fun testIncludePromotedOngoingInSection_flagEnabled() { 169 // GIVEN the notification has FLAG_PROMOTED_ONGOING 170 val entry = buildEntry { setFlag(mContext, Notification.FLAG_PROMOTED_ONGOING, true) } 171 172 // THEN the entry is in the fgs section 173 assertTrue(sectioner.isInSection(entry)) 174 } 175 176 @Test 177 @DisableFlags(PromotedNotificationUi.FLAG_NAME) testDiscludePromotedOngoingInSection_flagDisablednull178 fun testDiscludePromotedOngoingInSection_flagDisabled() { 179 // GIVEN the notification has FLAG_PROMOTED_ONGOING 180 val entry = buildEntry { setFlag(mContext, Notification.FLAG_PROMOTED_ONGOING, true) } 181 182 // THEN the entry is NOT in the fgs section 183 assertFalse(sectioner.isInSection(entry)) 184 } 185 186 @Test 187 @EnableFlags(PromotedNotificationUi.FLAG_NAME) testIncludeScreenRecordNotifInSection_importanceDefaultnull188 fun testIncludeScreenRecordNotifInSection_importanceDefault() = 189 kosmos.runTest { 190 // GIVEN a screen record event + screen record notif that has a status bar chip 191 screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording 192 fakeMediaProjectionRepository.mediaProjectionState.value = 193 MediaProjectionState.Projecting.EntireScreen(hostPackage = "test_pkg") 194 val screenRecordEntry = 195 buildNotificationEntry(tag = "screenRecord", promoted = false) { 196 setImportance(NotificationManager.IMPORTANCE_DEFAULT) 197 setFlag(context, FLAG_FOREGROUND_SERVICE, true) 198 } 199 200 renderNotificationListInteractor.setRenderedList(listOf(screenRecordEntry)) 201 202 val orderedChipNotificationKeys by 203 collectLastValue(promotedNotificationsInteractor.orderedChipNotificationKeys) 204 205 assertThat(orderedChipNotificationKeys) 206 .containsExactly("0|test_pkg|0|screenRecord|0") 207 .inOrder() 208 209 // THEN the entry is in the fgs section 210 assertTrue(sectioner.isInSection(screenRecordEntry)) 211 } 212 213 @Test 214 @EnableFlags(PromotedNotificationUi.FLAG_NAME) testDiscludeScreenRecordNotifInSection_importanceMinnull215 fun testDiscludeScreenRecordNotifInSection_importanceMin() = 216 kosmos.runTest { 217 // GIVEN a screen record event + screen record notif that has a status bar chip 218 screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording 219 fakeMediaProjectionRepository.mediaProjectionState.value = 220 MediaProjectionState.Projecting.EntireScreen(hostPackage = "test_pkg") 221 val screenRecordEntry = 222 buildNotificationEntry(tag = "screenRecord", promoted = false) { 223 setImportance(NotificationManager.IMPORTANCE_MIN) 224 setFlag(context, FLAG_FOREGROUND_SERVICE, true) 225 } 226 227 renderNotificationListInteractor.setRenderedList(listOf(screenRecordEntry)) 228 229 val orderedChipNotificationKeys by 230 collectLastValue(promotedNotificationsInteractor.orderedChipNotificationKeys) 231 232 assertThat(orderedChipNotificationKeys) 233 .containsExactly("0|test_pkg|0|screenRecord|0") 234 .inOrder() 235 236 // THEN the entry is NOT in the fgs section 237 assertFalse(sectioner.isInSection(screenRecordEntry)) 238 } 239 240 @Test 241 @DisableFlags(PromotedNotificationUi.FLAG_NAME) testDiscludeScreenRecordNotifInSection_flagDisablednull242 fun testDiscludeScreenRecordNotifInSection_flagDisabled() = 243 kosmos.runTest { 244 // GIVEN a screen record event + screen record notif that has a status bar chip 245 screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording 246 fakeMediaProjectionRepository.mediaProjectionState.value = 247 MediaProjectionState.Projecting.EntireScreen(hostPackage = "test_pkg") 248 val screenRecordEntry = 249 buildNotificationEntry(tag = "screenRecord", promoted = false) { 250 setImportance(NotificationManager.IMPORTANCE_DEFAULT) 251 setFlag(context, FLAG_FOREGROUND_SERVICE, true) 252 } 253 254 renderNotificationListInteractor.setRenderedList(listOf(screenRecordEntry)) 255 256 val orderedChipNotificationKeys by 257 collectLastValue(promotedNotificationsInteractor.orderedChipNotificationKeys) 258 259 assertThat(orderedChipNotificationKeys) 260 .containsExactly("0|test_pkg|0|screenRecord|0") 261 .inOrder() 262 263 // THEN the entry is NOT in the fgs section 264 assertFalse(sectioner.isInSection(screenRecordEntry)) 265 } 266 267 @Test 268 @EnableFlags(PromotedNotificationUi.FLAG_NAME) promoterSelectsPromotedOngoing_flagEnablednull269 fun promoterSelectsPromotedOngoing_flagEnabled() { 270 val promoter: NotifPromoter = withArgCaptor { verify(notifPipeline).addPromoter(capture()) } 271 272 // GIVEN the notification has FLAG_PROMOTED_ONGOING 273 val entry = buildEntry { setFlag(mContext, Notification.FLAG_PROMOTED_ONGOING, true) } 274 275 // THEN the entry is promoted to top level 276 assertTrue(promoter.shouldPromoteToTopLevel(entry)) 277 } 278 279 @Test 280 @EnableFlags(PromotedNotificationUi.FLAG_NAME) promoterIgnoresNonPromotedOngoing_flagEnablednull281 fun promoterIgnoresNonPromotedOngoing_flagEnabled() { 282 val promoter: NotifPromoter = withArgCaptor { verify(notifPipeline).addPromoter(capture()) } 283 284 // GIVEN the notification does not have FLAG_PROMOTED_ONGOING 285 val entry = buildEntry { setFlag(mContext, Notification.FLAG_PROMOTED_ONGOING, false) } 286 287 // THEN the entry is NOT promoted to top level 288 assertFalse(promoter.shouldPromoteToTopLevel(entry)) 289 } 290 291 @Test 292 @DisableFlags(PromotedNotificationUi.FLAG_NAME) noPromoterAdded_flagDisablednull293 fun noPromoterAdded_flagDisabled() { 294 verify(notifPipeline, never()).addPromoter(any()) 295 } 296 297 @Test 298 @EnableFlags( 299 PromotedNotificationUi.FLAG_NAME, 300 StatusBarNotifChips.FLAG_NAME, 301 StatusBarChipsModernization.FLAG_NAME, 302 StatusBarRootModernization.FLAG_NAME, 303 ) comparatorPutsCallBeforeOthernull304 fun comparatorPutsCallBeforeOther() = 305 kosmos.runTest { 306 // GIVEN a call and a promoted ongoing notification 307 val callEntry = buildOngoingCallEntry(promoted = false) 308 val ronEntry = buildPromotedOngoingEntry() 309 val otherEntry = buildNotificationEntry(tag = "other") 310 311 kosmos.renderNotificationListInteractor.setRenderedList( 312 listOf(callEntry, ronEntry, otherEntry) 313 ) 314 315 val orderedChipNotificationKeys by 316 collectLastValue(kosmos.promotedNotificationsInteractor.orderedChipNotificationKeys) 317 318 // THEN the order of the notification keys should be the call then the RON 319 assertThat(orderedChipNotificationKeys) 320 .containsExactly("0|test_pkg|0|call|0", "0|test_pkg|0|ron|0") 321 322 // VERIFY that the comparator puts the call before the ron 323 assertThat(sectioner.comparator!!.compare(callEntry, ronEntry)).isLessThan(0) 324 // VERIFY that the comparator puts the ron before the other 325 assertThat(sectioner.comparator!!.compare(ronEntry, otherEntry)).isLessThan(0) 326 } 327 makeCallStylenull328 private fun makeCallStyle(): Notification.CallStyle { 329 val pendingIntent = 330 PendingIntent.getBroadcast(mContext, 0, Intent("action"), PendingIntent.FLAG_IMMUTABLE) 331 val person = Person.Builder().setName("person").build() 332 return Notification.CallStyle.forOngoingCall(person, pendingIntent) 333 } 334 } 335