• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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