• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2025 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.row
17 
18 import android.app.Flags
19 import android.app.INotificationManager
20 import android.app.Notification
21 import android.app.NotificationChannel
22 import android.app.NotificationChannel.SOCIAL_MEDIA_ID
23 import android.app.NotificationChannelGroup
24 import android.app.NotificationManager
25 import android.app.NotificationManager.IMPORTANCE_LOW
26 import android.app.PendingIntent
27 import android.app.Person
28 import android.content.ComponentName
29 import android.content.Intent
30 import android.content.mockPackageManager
31 import android.content.pm.ActivityInfo
32 import android.content.pm.ApplicationInfo
33 import android.content.pm.PackageInfo
34 import android.content.pm.PackageManager
35 import android.content.pm.ResolveInfo
36 import android.graphics.drawable.Drawable
37 import android.os.RemoteException
38 import android.os.UserHandle
39 import android.os.testableLooper
40 import android.platform.test.annotations.DisableFlags
41 import android.platform.test.annotations.EnableFlags
42 import android.print.PrintManager
43 import android.service.notification.StatusBarNotification
44 import android.telecom.TelecomManager
45 import android.testing.TestableLooper.RunWithLooper
46 import android.view.LayoutInflater
47 import android.view.View
48 import android.view.View.GONE
49 import android.view.View.VISIBLE
50 import android.widget.ImageView
51 import android.widget.TextView
52 import androidx.core.view.isVisible
53 import androidx.test.ext.junit.runners.AndroidJUnit4
54 import androidx.test.filters.SmallTest
55 import com.android.internal.logging.MetricsLogger
56 import com.android.internal.logging.UiEventLogger
57 import com.android.internal.logging.metricsLogger
58 import com.android.internal.logging.uiEventLoggerFake
59 import com.android.systemui.Dependency
60 import com.android.systemui.SysuiTestCase
61 import com.android.systemui.kosmos.testCase
62 import com.android.systemui.res.R
63 import com.android.systemui.statusbar.RankingBuilder
64 import com.android.systemui.statusbar.notification.AssistantFeedbackController
65 import com.android.systemui.statusbar.notification.collection.EntryAdapter
66 import com.android.systemui.statusbar.notification.collection.NotificationEntry
67 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
68 import com.android.systemui.statusbar.notification.collection.coordinator.mockVisualStabilityCoordinator
69 import com.android.systemui.statusbar.notification.promoted.domain.interactor.PackageDemotionInteractor
70 import com.android.systemui.statusbar.notification.row.icon.AppIconProvider
71 import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider
72 import com.android.systemui.statusbar.notification.row.icon.mockAppIconProvider
73 import com.android.systemui.statusbar.notification.row.icon.mockNotificationIconStyleProvider
74 import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
75 import com.android.systemui.testKosmos
76 import com.android.telecom.telecomManager
77 import com.google.common.truth.Truth.assertThat
78 import java.util.concurrent.CountDownLatch
79 import org.junit.Before
80 import org.junit.Test
81 import org.junit.runner.RunWith
82 import org.mockito.ArgumentMatchers.anyBoolean
83 import org.mockito.ArgumentMatchers.anyInt
84 import org.mockito.ArgumentMatchers.anyString
85 import org.mockito.kotlin.any
86 import org.mockito.kotlin.anyOrNull
87 import org.mockito.kotlin.argumentCaptor
88 import org.mockito.kotlin.eq
89 import org.mockito.kotlin.mock
90 import org.mockito.kotlin.never
91 import org.mockito.kotlin.verify
92 import org.mockito.kotlin.whenever
93 
94 @SmallTest
95 @RunWith(AndroidJUnit4::class)
96 @RunWithLooper
97 class NotificationInfoTest : SysuiTestCase() {
98     private val kosmos = testKosmos().also { it.testCase = this }
99 
100     private lateinit var underTest: NotificationInfo
101     private lateinit var notificationChannel: NotificationChannel
102     private lateinit var defaultNotificationChannel: NotificationChannel
103     private lateinit var classifiedNotificationChannel: NotificationChannel
104     private lateinit var sbn: StatusBarNotification
105     private lateinit var entry: NotificationEntry
106     private lateinit var entryAdapter: EntryAdapter
107 
108     private val mockPackageManager = kosmos.mockPackageManager
109     private val mockAppIconProvider = kosmos.mockAppIconProvider
110     private val mockIconStyleProvider = kosmos.mockNotificationIconStyleProvider
111     private val uiEventLogger = kosmos.uiEventLoggerFake
112     private val testableLooper by lazy { kosmos.testableLooper }
113 
114     private val onUserInteractionCallback = mock<OnUserInteractionCallback>()
115     private val mockINotificationManager = mock<INotificationManager>()
116     private val channelEditorDialogController = mock<ChannelEditorDialogController>()
117     private val packageDemotionInteractor = mock<PackageDemotionInteractor>()
118     private val assistantFeedbackController = mock<AssistantFeedbackController>()
119 
120     @Before
121     fun setUp() {
122         mContext.addMockSystemService(TelecomManager::class.java, kosmos.telecomManager)
123 
124         mDependency.injectTestDependency(Dependency.BG_LOOPER, testableLooper.looper)
125 
126         // Inflate the layout
127         val inflater = LayoutInflater.from(mContext)
128         underTest = inflater.inflate(R.layout.notification_info, null) as NotificationInfo
129 
130         underTest.setGutsParent(mock<NotificationGuts>())
131 
132         // Our view is never attached to a window so the View#post methods in NotificationInfo never
133         // get called. Setting this will skip the post and do the action immediately.
134         underTest.mSkipPost = true
135 
136         // PackageManager must return a packageInfo and applicationInfo.
137         val packageInfo = PackageInfo()
138         packageInfo.packageName = TEST_PACKAGE_NAME
139         whenever(mockPackageManager.getPackageInfo(eq(TEST_PACKAGE_NAME), anyInt()))
140             .thenReturn(packageInfo)
141         val applicationInfo = ApplicationInfo()
142         applicationInfo.uid = TEST_UID // non-zero
143         val systemPackageInfo = PackageInfo()
144         systemPackageInfo.packageName = TEST_SYSTEM_PACKAGE_NAME
145         whenever(mockPackageManager.getPackageInfo(eq(TEST_SYSTEM_PACKAGE_NAME), anyInt()))
146             .thenReturn(systemPackageInfo)
147         whenever(mockPackageManager.getPackageInfo(eq("android"), anyInt())).thenReturn(packageInfo)
148 
149         val assistant = ComponentName("package", "service")
150         whenever(mockINotificationManager.allowedNotificationAssistant).thenReturn(assistant)
151         val ri = ResolveInfo()
152         ri.activityInfo = ActivityInfo()
153         ri.activityInfo.packageName = assistant.packageName
154         ri.activityInfo.name = "activity"
155         whenever(mockPackageManager.queryIntentActivities(any(), anyInt())).thenReturn(listOf(ri))
156 
157         // Package has one channel by default.
158         whenever(
159                 mockINotificationManager.getNumNotificationChannelsForPackage(
160                     eq(TEST_PACKAGE_NAME),
161                     eq(TEST_UID),
162                     anyBoolean(),
163                 )
164             )
165             .thenReturn(1)
166 
167         // Some test channels.
168         notificationChannel = NotificationChannel(TEST_CHANNEL, TEST_CHANNEL_NAME, IMPORTANCE_LOW)
169         defaultNotificationChannel =
170             NotificationChannel(
171                 NotificationChannel.DEFAULT_CHANNEL_ID,
172                 TEST_CHANNEL_NAME,
173                 IMPORTANCE_LOW,
174             )
175         classifiedNotificationChannel =
176             NotificationChannel(SOCIAL_MEDIA_ID, "social", IMPORTANCE_LOW)
177 
178         val notification = Notification()
179         notification.extras.putParcelable(
180             Notification.EXTRA_BUILDER_APPLICATION_INFO,
181             applicationInfo,
182         )
183         sbn =
184             StatusBarNotification(
185                 TEST_PACKAGE_NAME,
186                 TEST_PACKAGE_NAME,
187                 0,
188                 null,
189                 TEST_UID,
190                 0,
191                 notification,
192                 UserHandle.getUserHandleForUid(TEST_UID),
193                 null,
194                 0,
195             )
196         entry =
197             NotificationEntryBuilder()
198                 .setSbn(sbn)
199                 .updateRanking { it.setChannel(notificationChannel) }
200                 .build()
201         entryAdapter = kosmos.entryAdapterFactory.create(entry)
202         whenever(assistantFeedbackController.isFeedbackEnabled).thenReturn(false)
203         whenever(assistantFeedbackController.getInlineDescriptionResource(any()))
204             .thenReturn(R.string.notification_channel_summary_automatic)
205     }
206 
207     @Test
208     fun testBindNotification_SetsTextApplicationName() {
209         whenever(mockPackageManager.getApplicationLabel(any())).thenReturn("App Name")
210         bindNotification()
211         val textView = underTest.findViewById<TextView>(R.id.pkg_name)
212         assertThat(textView.text.toString()).contains("App Name")
213         assertThat(underTest.findViewById<View>(R.id.header).visibility).isEqualTo(VISIBLE)
214     }
215 
216     @Test
217     @DisableFlags(com.android.systemui.Flags.FLAG_NOTIFICATIONS_REDESIGN_GUTS)
218     fun testBindNotification_SetsPackageIcon_flagOff() {
219         val iconDrawable = mock<Drawable>()
220         whenever(mockPackageManager.getApplicationIcon(any<ApplicationInfo>()))
221             .thenReturn(iconDrawable)
222         bindNotification()
223         val iconView = underTest.findViewById<ImageView>(R.id.pkg_icon)
224         assertThat(iconView.drawable).isEqualTo(iconDrawable)
225     }
226 
227     @Test
228     @EnableFlags(com.android.systemui.Flags.FLAG_NOTIFICATIONS_REDESIGN_GUTS)
229     fun testBindNotification_SetsPackageIcon_flagOn() {
230         val iconDrawable = mock<Drawable>()
231         whenever(mockIconStyleProvider.shouldShowWorkProfileBadge(anyOrNull(), anyOrNull()))
232             .thenReturn(false)
233         whenever(
234                 mockAppIconProvider.getOrFetchAppIcon(
235                     anyOrNull(),
236                     anyOrNull(),
237                     anyBoolean(),
238                     anyBoolean(),
239                 )
240             )
241             .thenReturn(iconDrawable)
242         bindNotification()
243         val iconView = underTest.findViewById<ImageView>(R.id.pkg_icon)
244         assertThat(iconView.drawable).isEqualTo(iconDrawable)
245     }
246 
247     @Test
248     fun testBindNotification_noDelegate() {
249         bindNotification()
250         val nameView = underTest.findViewById<TextView>(R.id.delegate_name)
251         assertThat(nameView.visibility).isEqualTo(GONE)
252     }
253 
254     @Test
255     fun testBindNotification_delegate() {
256         sbn =
257             StatusBarNotification(
258                 TEST_PACKAGE_NAME,
259                 "other",
260                 0,
261                 null,
262                 TEST_UID,
263                 0,
264                 Notification(),
265                 UserHandle.CURRENT,
266                 null,
267                 0,
268             )
269         val applicationInfo = ApplicationInfo()
270         applicationInfo.uid = 7 // non-zero
271         whenever(mockPackageManager.getApplicationInfo(eq("other"), anyInt()))
272             .thenReturn(applicationInfo)
273         whenever(mockPackageManager.getApplicationLabel(any())).thenReturn("Other")
274 
275         val entry = NotificationEntryBuilder(entry).setSbn(sbn).build()
276         bindNotification(entry = entry)
277         val nameView = underTest.findViewById<TextView>(R.id.delegate_name)
278         assertThat(nameView.visibility).isEqualTo(VISIBLE)
279         assertThat(nameView.text.toString()).contains("Proxied")
280     }
281 
282     @Test
283     fun testBindNotification_GroupNameHiddenIfNoGroup() {
284         bindNotification()
285         val groupNameView = underTest.findViewById<TextView>(R.id.group_name)
286         assertThat(groupNameView.visibility).isEqualTo(GONE)
287     }
288 
289     @Test
290     fun testBindNotification_SetsGroupNameIfNonNull() {
291         notificationChannel.group = "test_group_id"
292         val notificationChannelGroup = NotificationChannelGroup("test_group_id", "Test Group Name")
293         whenever(
294                 mockINotificationManager.getNotificationChannelGroupForPackage(
295                     eq("test_group_id"),
296                     eq(TEST_PACKAGE_NAME),
297                     eq(TEST_UID),
298                 )
299             )
300             .thenReturn(notificationChannelGroup)
301         bindNotification()
302         val groupNameView = underTest.findViewById<TextView>(R.id.group_name)
303         assertThat(groupNameView.visibility).isEqualTo(VISIBLE)
304         assertThat(groupNameView.text).isEqualTo("Test Group Name")
305     }
306 
307     @Test
308     fun testBindNotification_SetsTextChannelName() {
309         bindNotification()
310         val textView = underTest.findViewById<TextView>(R.id.channel_name)
311         assertThat(textView.text).isEqualTo(TEST_CHANNEL_NAME)
312     }
313 
314     @Test
315     fun testBindNotification_DefaultChannelDoesNotUseChannelName() {
316         entry =
317             NotificationEntryBuilder(entry)
318                 .updateRanking { it.setChannel(defaultNotificationChannel) }
319                 .build()
320         bindNotification(notificationChannel = defaultNotificationChannel)
321         val textView = underTest.findViewById<TextView>(R.id.channel_name)
322         assertThat(textView.visibility).isEqualTo(GONE)
323     }
324 
325     @Test
326     fun testBindNotification_DefaultChannelUsesChannelNameIfMoreChannelsExist() {
327         // Package has more than one channel by default.
328         whenever(
329                 mockINotificationManager.getNumNotificationChannelsForPackage(
330                     eq(TEST_PACKAGE_NAME),
331                     eq(TEST_UID),
332                     anyBoolean(),
333                 )
334             )
335             .thenReturn(10)
336         entry =
337             NotificationEntryBuilder(entry)
338                 .updateRanking { it.setChannel(notificationChannel) }
339                 .build()
340         bindNotification(notificationChannel = defaultNotificationChannel)
341         val textView = underTest.findViewById<TextView>(R.id.channel_name)
342         assertThat(textView.visibility).isEqualTo(VISIBLE)
343     }
344 
345     @Test
346     fun testBindNotification_UnblockablePackageUsesChannelName() {
347         bindNotification(isNonblockable = true)
348         val textView = underTest.findViewById<TextView>(R.id.channel_name)
349         assertThat(textView.visibility).isEqualTo(VISIBLE)
350     }
351 
352     @Test
353     fun testBindNotification_SetsOnClickListenerForSettings() {
354         val latch = CountDownLatch(1)
355         bindNotification(
356             onSettingsClick = { _: View?, c: NotificationChannel?, _: Int ->
357                 assertThat(c).isEqualTo(notificationChannel)
358                 latch.countDown()
359             }
360         )
361 
362         val settingsButton = underTest.findViewById<View>(R.id.info)
363         settingsButton.performClick()
364         // Verify that listener was triggered.
365         assertThat(latch.count).isEqualTo(0)
366     }
367 
368     @Test
369     fun testBindNotification_SettingsButtonInvisibleWhenNoClickListener() {
370         bindNotification()
371         val settingsButton = underTest.findViewById<View>(R.id.info)
372         assertThat(settingsButton.visibility != VISIBLE).isTrue()
373     }
374 
375     @Test
376     fun testBindNotification_SettingsButtonInvisibleWhenDeviceUnprovisioned() {
377         bindNotification(
378             onSettingsClick = { _: View?, c: NotificationChannel?, _: Int ->
379                 assertThat(c).isEqualTo(notificationChannel)
380             },
381             isDeviceProvisioned = false,
382         )
383         val settingsButton = underTest.findViewById<View>(R.id.info)
384         assertThat(settingsButton.visibility != VISIBLE).isTrue()
385     }
386 
387     @Test
388     fun testBindNotification_SettingsButtonReappearsAfterSecondBind() {
389         bindNotification()
390         bindNotification(onSettingsClick = { _: View?, _: NotificationChannel?, _: Int -> })
391         val settingsButton = underTest.findViewById<View>(R.id.info)
392         assertThat(settingsButton.visibility).isEqualTo(VISIBLE)
393     }
394 
395     @Test
396     fun testBindNotification_whenAppUnblockable() {
397         bindNotification(isNonblockable = true)
398         val view = underTest.findViewById<TextView>(R.id.non_configurable_text)
399         assertThat(view.visibility).isEqualTo(VISIBLE)
400         assertThat(view.text).isEqualTo(mContext.getString(R.string.notification_unblockable_desc))
401         assertThat(underTest.findViewById<View>(R.id.interruptiveness_settings).visibility)
402             .isEqualTo(GONE)
403     }
404 
405     @Test
406     fun testBindNotification_whenCurrentlyInCall() {
407         whenever(mockINotificationManager.isInCall(anyString(), anyInt())).thenReturn(true)
408 
409         val person = Person.Builder().setName("caller").build()
410         val nb =
411             Notification.Builder(mContext, notificationChannel.id)
412                 .setContentTitle("foo")
413                 .setSmallIcon(android.R.drawable.sym_def_app_icon)
414                 .setStyle(Notification.CallStyle.forOngoingCall(person, mock<PendingIntent>()))
415                 .setFullScreenIntent(mock<PendingIntent>(), true)
416                 .addAction(Notification.Action.Builder(null, "test", null).build())
417 
418         sbn =
419             StatusBarNotification(
420                 TEST_PACKAGE_NAME,
421                 TEST_PACKAGE_NAME,
422                 0,
423                 null,
424                 TEST_UID,
425                 0,
426                 nb.build(),
427                 UserHandle.getUserHandleForUid(TEST_UID),
428                 null,
429                 0,
430             )
431         entry.sbn = sbn
432         bindNotification()
433         val view = underTest.findViewById<TextView>(R.id.non_configurable_call_text)
434         assertThat(view.visibility).isEqualTo(VISIBLE)
435         assertThat(view.text)
436             .isEqualTo(mContext.getString(R.string.notification_unblockable_call_desc))
437         assertThat(underTest.findViewById<View>(R.id.interruptiveness_settings).visibility)
438             .isEqualTo(GONE)
439         assertThat(underTest.findViewById<View>(R.id.non_configurable_text).visibility)
440             .isEqualTo(GONE)
441     }
442 
443     @Test
444     fun testBindNotification_whenCurrentlyInCall_notCall() {
445         whenever(mockINotificationManager.isInCall(anyString(), anyInt())).thenReturn(true)
446 
447         val nb =
448             Notification.Builder(mContext, notificationChannel.id)
449                 .setContentTitle("foo")
450                 .setSmallIcon(android.R.drawable.sym_def_app_icon)
451                 .setFullScreenIntent(mock<PendingIntent>(), true)
452                 .addAction(Notification.Action.Builder(null, "test", null).build())
453 
454         sbn =
455             StatusBarNotification(
456                 TEST_PACKAGE_NAME,
457                 TEST_PACKAGE_NAME,
458                 0,
459                 null,
460                 TEST_UID,
461                 0,
462                 nb.build(),
463                 UserHandle.getUserHandleForUid(TEST_UID),
464                 null,
465                 0,
466             )
467         entry.sbn = sbn
468         bindNotification()
469         assertThat(underTest.findViewById<View>(R.id.non_configurable_call_text).visibility)
470             .isEqualTo(GONE)
471         assertThat(underTest.findViewById<View>(R.id.interruptiveness_settings).visibility)
472             .isEqualTo(VISIBLE)
473         assertThat(underTest.findViewById<View>(R.id.non_configurable_text).visibility)
474             .isEqualTo(GONE)
475     }
476 
477     @Test
478     fun testBindNotification_automaticIsVisible() {
479         whenever(assistantFeedbackController.isFeedbackEnabled).thenReturn(true)
480         bindNotification()
481         assertThat(underTest.findViewById<View>(R.id.automatic).visibility).isEqualTo(VISIBLE)
482         assertThat(underTest.findViewById<View>(R.id.automatic_summary).visibility)
483             .isEqualTo(VISIBLE)
484     }
485 
486     @Test
487     fun testBindNotification_automaticIsGone() {
488         bindNotification()
489         assertThat(underTest.findViewById<View>(R.id.automatic).visibility).isEqualTo(GONE)
490         assertThat(underTest.findViewById<View>(R.id.automatic_summary).visibility).isEqualTo(GONE)
491     }
492 
493     @Test
494     fun testBindNotification_automaticIsSelected() {
495         whenever(assistantFeedbackController.isFeedbackEnabled).thenReturn(true)
496         notificationChannel.unlockFields(NotificationChannel.USER_LOCKED_IMPORTANCE)
497         bindNotification()
498         assertThat(underTest.findViewById<View>(R.id.automatic).isSelected).isTrue()
499     }
500 
501     @Test
502     fun testBindNotification_alertIsSelected() {
503         bindNotification()
504         assertThat(underTest.findViewById<View>(R.id.alert).isSelected).isTrue()
505     }
506 
507     @Test
508     fun testBindNotification_silenceIsSelected() {
509         bindNotification(wasShownHighPriority = false)
510         assertThat(underTest.findViewById<View>(R.id.silence).isSelected).isTrue()
511     }
512 
513     @Test
514     fun testBindNotification_DoesNotUpdateNotificationChannel() {
515         bindNotification()
516         testableLooper.processAllMessages()
517         verify(mockINotificationManager, never())
518             .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), any())
519     }
520 
521     @Test
522     fun testBindNotification_LogsOpen() {
523         bindNotification()
524         assertThat(uiEventLogger.numLogs()).isEqualTo(1)
525         assertThat(uiEventLogger.eventId(0))
526             .isEqualTo(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.id)
527     }
528 
529     @Test
530     fun testDoesNotUpdateNotificationChannelAfterImportanceChanged() {
531         notificationChannel.importance = IMPORTANCE_LOW
532         bindNotification(wasShownHighPriority = false)
533 
534         underTest.findViewById<View>(R.id.alert).performClick()
535         testableLooper.processAllMessages()
536         verify(mockINotificationManager, never())
537             .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), any())
538     }
539 
540     @Test
541     fun testDoesNotUpdateNotificationChannelAfterImportanceChangedSilenced() {
542         notificationChannel.importance = NotificationManager.IMPORTANCE_DEFAULT
543         bindNotification()
544 
545         underTest.findViewById<View>(R.id.silence).performClick()
546         testableLooper.processAllMessages()
547         verify(mockINotificationManager, never())
548             .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), any())
549     }
550 
551     @Test
552     fun testDoesNotUpdateNotificationChannelAfterImportanceChangedAutomatic() {
553         notificationChannel.importance = NotificationManager.IMPORTANCE_DEFAULT
554         bindNotification()
555 
556         underTest.findViewById<View>(R.id.automatic).performClick()
557         testableLooper.processAllMessages()
558         verify(mockINotificationManager, never())
559             .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), any())
560     }
561 
562     @Test
563     fun testHandleCloseControls_persistAutomatic() {
564         whenever(assistantFeedbackController.isFeedbackEnabled).thenReturn(true)
565         notificationChannel.unlockFields(NotificationChannel.USER_LOCKED_IMPORTANCE)
566         bindNotification()
567 
568         underTest.handleCloseControls(true, false)
569         testableLooper.processAllMessages()
570         verify(mockINotificationManager).unlockNotificationChannel(anyString(), eq(TEST_UID), any())
571     }
572 
573     @Test
574     fun testHandleCloseControls_DoesNotUpdateNotificationChannelIfUnchanged() {
575         val originalImportance = notificationChannel.importance
576         bindNotification()
577 
578         underTest.handleCloseControls(true, false)
579         testableLooper.processAllMessages()
580         verify(mockINotificationManager)
581             .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), any())
582         assertThat(notificationChannel.importance).isEqualTo(originalImportance)
583 
584         assertThat(uiEventLogger.numLogs()).isEqualTo(2)
585         assertThat(uiEventLogger.eventId(0))
586             .isEqualTo(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.id)
587         // The SAVE_IMPORTANCE event is logged whenever importance is saved, even if unchanged.
588         assertThat(uiEventLogger.eventId(1))
589             .isEqualTo(NotificationControlsEvent.NOTIFICATION_CONTROLS_SAVE_IMPORTANCE.id)
590     }
591 
592     @Test
593     fun testHandleCloseControls_DoesNotUpdateNotificationChannelIfUnspecified() {
594         notificationChannel.importance = NotificationManager.IMPORTANCE_UNSPECIFIED
595         bindNotification()
596 
597         underTest.handleCloseControls(true, false)
598 
599         testableLooper.processAllMessages()
600         verify(mockINotificationManager)
601             .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), any())
602         assertThat(notificationChannel.importance)
603             .isEqualTo(NotificationManager.IMPORTANCE_UNSPECIFIED)
604     }
605 
606     @Test
607     fun testSilenceCallsUpdateNotificationChannel() {
608         notificationChannel.importance = NotificationManager.IMPORTANCE_DEFAULT
609         bindNotification()
610 
611         underTest.findViewById<View>(R.id.silence).performClick()
612         underTest.findViewById<View>(R.id.done).performClick()
613         underTest.handleCloseControls(true, false)
614 
615         testableLooper.processAllMessages()
616         val updated = argumentCaptor<NotificationChannel>()
617         verify(mockINotificationManager)
618             .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), updated.capture())
619         assertThat(
620                 updated.firstValue.userLockedFields and NotificationChannel.USER_LOCKED_IMPORTANCE
621             )
622             .isNotEqualTo(0)
623         assertThat(updated.firstValue.importance).isEqualTo(IMPORTANCE_LOW)
624 
625         assertThat(uiEventLogger.numLogs()).isEqualTo(2)
626         assertThat(uiEventLogger.eventId(0))
627             .isEqualTo(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.id)
628         assertThat(uiEventLogger.eventId(1))
629             .isEqualTo(NotificationControlsEvent.NOTIFICATION_CONTROLS_SAVE_IMPORTANCE.id)
630         assertThat(underTest.shouldBeSavedOnClose()).isFalse()
631     }
632 
633     @Test
634     fun testUnSilenceCallsUpdateNotificationChannel() {
635         notificationChannel.importance = IMPORTANCE_LOW
636         bindNotification(wasShownHighPriority = false)
637 
638         underTest.findViewById<View>(R.id.alert).performClick()
639         underTest.findViewById<View>(R.id.done).performClick()
640         underTest.handleCloseControls(true, false)
641 
642         testableLooper.processAllMessages()
643         val updated = argumentCaptor<NotificationChannel>()
644         verify(mockINotificationManager)
645             .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), updated.capture())
646         assertThat(
647                 updated.firstValue.userLockedFields and NotificationChannel.USER_LOCKED_IMPORTANCE
648             )
649             .isNotEqualTo(0)
650         assertThat(updated.firstValue.importance).isEqualTo(NotificationManager.IMPORTANCE_DEFAULT)
651         assertThat(underTest.shouldBeSavedOnClose()).isFalse()
652     }
653 
654     @Test
655     fun testAutomaticUnlocksUserImportance() {
656         whenever(assistantFeedbackController.isFeedbackEnabled).thenReturn(true)
657         notificationChannel.importance = NotificationManager.IMPORTANCE_DEFAULT
658         notificationChannel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE)
659         bindNotification()
660 
661         underTest.findViewById<View>(R.id.automatic).performClick()
662         underTest.findViewById<View>(R.id.done).performClick()
663         underTest.handleCloseControls(true, false)
664 
665         testableLooper.processAllMessages()
666         verify(mockINotificationManager).unlockNotificationChannel(anyString(), eq(TEST_UID), any())
667         assertThat(notificationChannel.importance).isEqualTo(NotificationManager.IMPORTANCE_DEFAULT)
668         assertThat(underTest.shouldBeSavedOnClose()).isFalse()
669     }
670 
671     @Test
672     fun testSilenceCallsUpdateNotificationChannel_channelImportanceUnspecified() {
673         notificationChannel.importance = NotificationManager.IMPORTANCE_UNSPECIFIED
674         bindNotification()
675 
676         underTest.findViewById<View>(R.id.silence).performClick()
677         underTest.findViewById<View>(R.id.done).performClick()
678         underTest.handleCloseControls(true, false)
679 
680         testableLooper.processAllMessages()
681         val updated = argumentCaptor<NotificationChannel>()
682         verify(mockINotificationManager)
683             .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), updated.capture())
684         assertThat(
685                 updated.firstValue.userLockedFields and NotificationChannel.USER_LOCKED_IMPORTANCE
686             )
687             .isNotEqualTo(0)
688         assertThat(updated.firstValue.importance).isEqualTo(IMPORTANCE_LOW)
689         assertThat(underTest.shouldBeSavedOnClose()).isFalse()
690     }
691 
692     @Test
693     fun testSilenceCallsUpdateNotificationChannel_channelImportanceMin() {
694         notificationChannel.importance = NotificationManager.IMPORTANCE_MIN
695         bindNotification(wasShownHighPriority = false)
696 
697         assertThat((underTest.findViewById<View>(R.id.done) as TextView).text)
698             .isEqualTo(mContext.getString(R.string.inline_done_button))
699         underTest.findViewById<View>(R.id.silence).performClick()
700         assertThat((underTest.findViewById<View>(R.id.done) as TextView).text)
701             .isEqualTo(mContext.getString(R.string.inline_done_button))
702         underTest.findViewById<View>(R.id.done).performClick()
703         underTest.handleCloseControls(true, false)
704 
705         testableLooper.processAllMessages()
706         val updated = argumentCaptor<NotificationChannel>()
707         verify(mockINotificationManager)
708             .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), updated.capture())
709         assertThat(
710                 updated.firstValue.userLockedFields and NotificationChannel.USER_LOCKED_IMPORTANCE
711             )
712             .isNotEqualTo(0)
713         assertThat(updated.firstValue.importance).isEqualTo(NotificationManager.IMPORTANCE_MIN)
714         assertThat(underTest.shouldBeSavedOnClose()).isFalse()
715     }
716 
717     @Test
718     @Throws(RemoteException::class)
719     fun testSilence_closeGutsThenTryToSave() {
720         notificationChannel.importance = NotificationManager.IMPORTANCE_DEFAULT
721         bindNotification(wasShownHighPriority = false)
722 
723         underTest.findViewById<View>(R.id.silence).performClick()
724         underTest.handleCloseControls(false, false)
725         underTest.handleCloseControls(true, false)
726 
727         testableLooper.processAllMessages()
728 
729         assertThat(notificationChannel.importance).isEqualTo(NotificationManager.IMPORTANCE_DEFAULT)
730         assertThat(underTest.shouldBeSavedOnClose()).isFalse()
731     }
732 
733     @Test
734     fun testAlertCallsUpdateNotificationChannel_channelImportanceMin() {
735         notificationChannel.importance = NotificationManager.IMPORTANCE_MIN
736         bindNotification(wasShownHighPriority = false)
737 
738         assertThat((underTest.findViewById<View>(R.id.done) as TextView).text)
739             .isEqualTo(mContext.getString(R.string.inline_done_button))
740         underTest.findViewById<View>(R.id.alert).performClick()
741         assertThat((underTest.findViewById<View>(R.id.done) as TextView).text)
742             .isEqualTo(mContext.getString(R.string.inline_ok_button))
743         underTest.findViewById<View>(R.id.done).performClick()
744         underTest.handleCloseControls(true, false)
745 
746         testableLooper.processAllMessages()
747         val updated = argumentCaptor<NotificationChannel>()
748         verify(mockINotificationManager)
749             .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), updated.capture())
750         assertThat(
751                 updated.firstValue.userLockedFields and NotificationChannel.USER_LOCKED_IMPORTANCE
752             )
753             .isNotEqualTo(0)
754         assertThat(updated.firstValue.importance).isEqualTo(NotificationManager.IMPORTANCE_DEFAULT)
755         assertThat(underTest.shouldBeSavedOnClose()).isFalse()
756     }
757 
758     @Test
759     fun testAdjustImportanceTemporarilyAllowsReordering() {
760         notificationChannel.importance = NotificationManager.IMPORTANCE_DEFAULT
761         bindNotification()
762 
763         underTest.findViewById<View>(R.id.silence).performClick()
764         underTest.findViewById<View>(R.id.done).performClick()
765         underTest.handleCloseControls(true, false)
766 
767         if (NotificationBundleUi.isEnabled) {
768             verify(kosmos.mockVisualStabilityCoordinator)
769                 .temporarilyAllowSectionChanges(eq(entry), any())
770         } else {
771             verify(onUserInteractionCallback).onImportanceChanged(entry)
772         }
773 
774         assertThat(underTest.shouldBeSavedOnClose()).isFalse()
775     }
776 
777     @Test
778     fun testDoneText() {
779         notificationChannel.importance = IMPORTANCE_LOW
780         bindNotification(wasShownHighPriority = false)
781 
782         assertThat((underTest.findViewById<View>(R.id.done) as TextView).text)
783             .isEqualTo(mContext.getString(R.string.inline_done_button))
784         underTest.findViewById<View>(R.id.alert).performClick()
785         assertThat((underTest.findViewById<View>(R.id.done) as TextView).text)
786             .isEqualTo(mContext.getString(R.string.inline_ok_button))
787         underTest.findViewById<View>(R.id.silence).performClick()
788         assertThat((underTest.findViewById<View>(R.id.done) as TextView).text)
789             .isEqualTo(mContext.getString(R.string.inline_done_button))
790     }
791 
792     @Test
793     fun testUnSilenceCallsUpdateNotificationChannel_channelImportanceUnspecified() {
794         notificationChannel.importance = IMPORTANCE_LOW
795         bindNotification(wasShownHighPriority = false)
796 
797         underTest.findViewById<View>(R.id.alert).performClick()
798         underTest.findViewById<View>(R.id.done).performClick()
799         underTest.handleCloseControls(true, false)
800 
801         testableLooper.processAllMessages()
802         val updated = argumentCaptor<NotificationChannel>()
803         verify(mockINotificationManager)
804             .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), updated.capture())
805         assertThat(
806                 updated.firstValue.userLockedFields and NotificationChannel.USER_LOCKED_IMPORTANCE
807             )
808             .isNotEqualTo(0)
809         assertThat(updated.firstValue.importance).isEqualTo(NotificationManager.IMPORTANCE_DEFAULT)
810         assertThat(underTest.shouldBeSavedOnClose()).isFalse()
811     }
812 
813     @Test
814     fun testCloseControlsDoesNotUpdateIfSaveIsFalse() {
815         notificationChannel.importance = IMPORTANCE_LOW
816         bindNotification(wasShownHighPriority = false)
817 
818         underTest.findViewById<View>(R.id.alert).performClick()
819         underTest.findViewById<View>(R.id.done).performClick()
820         underTest.handleCloseControls(false, false)
821 
822         testableLooper.processAllMessages()
823         verify(mockINotificationManager, never())
824             .updateNotificationChannelForPackage(
825                 eq(TEST_PACKAGE_NAME),
826                 eq(TEST_UID),
827                 eq(notificationChannel),
828             )
829 
830         assertThat(uiEventLogger.numLogs()).isEqualTo(1)
831         assertThat(uiEventLogger.eventId(0))
832             .isEqualTo(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.id)
833     }
834 
835     @Test
836     fun testCloseControlsUpdatesWhenCheckSaveListenerUsesCallback() {
837         notificationChannel.importance = IMPORTANCE_LOW
838         bindNotification(wasShownHighPriority = false)
839 
840         underTest.findViewById<View>(R.id.alert).performClick()
841         underTest.findViewById<View>(R.id.done).performClick()
842         testableLooper.processAllMessages()
843         verify(mockINotificationManager, never())
844             .updateNotificationChannelForPackage(
845                 eq(TEST_PACKAGE_NAME),
846                 eq(TEST_UID),
847                 eq(notificationChannel),
848             )
849 
850         underTest.handleCloseControls(true, false)
851 
852         testableLooper.processAllMessages()
853         verify(mockINotificationManager)
854             .updateNotificationChannelForPackage(
855                 eq(TEST_PACKAGE_NAME),
856                 eq(TEST_UID),
857                 eq(notificationChannel),
858             )
859     }
860 
861     @Test
862     fun testCloseControls_withoutHittingApply() {
863         notificationChannel.importance = IMPORTANCE_LOW
864         bindNotification(wasShownHighPriority = false)
865 
866         underTest.findViewById<View>(R.id.alert).performClick()
867 
868         assertThat(underTest.shouldBeSavedOnClose()).isFalse()
869     }
870 
871     @Test
872     fun testWillBeRemovedReturnsFalse() {
873         assertThat(underTest.willBeRemoved()).isFalse()
874 
875         bindNotification(wasShownHighPriority = false)
876 
877         assertThat(underTest.willBeRemoved()).isFalse()
878     }
879 
880     @Test
881     @DisableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI)
882     @Throws(Exception::class)
883     fun testBindNotification_HidesFeedbackLink_flagOff() {
884         bindNotification()
885         assertThat(underTest.findViewById<View>(R.id.feedback).visibility).isEqualTo(GONE)
886     }
887 
888     @Test
889     @EnableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI)
890     @Throws(RemoteException::class)
891     fun testBindNotification_SetsFeedbackLink_isReservedChannel() {
892         entry.setRanking(
893             RankingBuilder(entry.ranking)
894                 .setSummarization("something")
895                 .setChannel(classifiedNotificationChannel)
896                 .build()
897         )
898         val latch = CountDownLatch(1)
899         bindNotification(
900             notificationChannel = classifiedNotificationChannel,
901             onFeedbackClickListener = { _: View?, _: Intent? -> latch.countDown() },
902             wasShownHighPriority = false,
903         )
904 
905         val feedback: View = underTest.findViewById(R.id.feedback)
906         assertThat(feedback.visibility).isEqualTo(VISIBLE)
907         feedback.performClick()
908         // Verify that listener was triggered.
909         assertThat(latch.count).isEqualTo(0)
910     }
911 
912     @Test
913     @EnableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI)
914     @Throws(Exception::class)
915     fun testBindNotification_hidesFeedbackLink_notReservedChannel() {
916         bindNotification()
917 
918         assertThat(underTest.findViewById<View>(R.id.feedback).visibility).isEqualTo(GONE)
919     }
920 
921     @Test
922     @Throws(RemoteException::class)
923     fun testDismissListenerBound() {
924         val latch = CountDownLatch(1)
925         bindNotification(onCloseClick = { _: View? -> latch.countDown() })
926 
927         val dismissView = underTest.findViewById<View>(R.id.inline_dismiss)
928         assertThat(dismissView.isVisible).isTrue()
929         dismissView.performClick()
930 
931         // Verify that listener was triggered.
932         assertThat(latch.count).isEqualTo(0)
933     }
934 
935     @Test
936     @Throws(RemoteException::class)
937     fun testDismissHiddenWhenUndismissable() {
938 
939         entry.sbn.notification.flags =
940             entry.sbn.notification.flags or android.app.Notification.FLAG_NO_DISMISS
941         bindNotification(isDismissable = false)
942         val dismissView = underTest.findViewById<View>(R.id.inline_dismiss)
943         assertThat(dismissView.isVisible).isFalse()
944     }
945 
946     private fun bindNotification(
947         pm: PackageManager = this.mockPackageManager,
948         iNotificationManager: INotificationManager = this.mockINotificationManager,
949         appIconProvider: AppIconProvider = this.mockAppIconProvider,
950         iconStyleProvider: NotificationIconStyleProvider = this.mockIconStyleProvider,
951         onUserInteractionCallback: OnUserInteractionCallback = this.onUserInteractionCallback,
952         channelEditorDialogController: ChannelEditorDialogController =
953             this.channelEditorDialogController,
954         packageDemotionInteractor: PackageDemotionInteractor = this.packageDemotionInteractor,
955         pkg: String = TEST_PACKAGE_NAME,
956         notificationChannel: NotificationChannel = this.notificationChannel,
957         entry: NotificationEntry = this.entry,
958         entryAdapter: EntryAdapter = this.entryAdapter,
959         onSettingsClick: NotificationInfo.OnSettingsClickListener? = null,
960         onAppSettingsClick: NotificationInfo.OnAppSettingsClickListener? = null,
961         onFeedbackClickListener: NotificationInfo.OnFeedbackClickListener? = null,
962         uiEventLogger: UiEventLogger = this.uiEventLogger,
963         isDeviceProvisioned: Boolean = true,
964         isNonblockable: Boolean = false,
965         isDismissable: Boolean = true,
966         wasShownHighPriority: Boolean = true,
967         assistantFeedbackController: AssistantFeedbackController = this.assistantFeedbackController,
968         metricsLogger: MetricsLogger = kosmos.metricsLogger,
969         onCloseClick: View.OnClickListener? = null,
970     ) {
971         underTest.bindNotification(
972             pm,
973             iNotificationManager,
974             appIconProvider,
975             iconStyleProvider,
976             onUserInteractionCallback,
977             channelEditorDialogController,
978             packageDemotionInteractor,
979             pkg,
980             entry.ranking,
981             entry.sbn,
982             entry,
983             entryAdapter,
984             onSettingsClick,
985             onAppSettingsClick,
986             onFeedbackClickListener,
987             uiEventLogger,
988             isDeviceProvisioned,
989             isNonblockable,
990             isDismissable,
991             wasShownHighPriority,
992             assistantFeedbackController,
993             metricsLogger,
994             onCloseClick,
995         )
996     }
997 
998     companion object {
999         private const val TEST_PACKAGE_NAME = "test_package"
1000         private const val TEST_SYSTEM_PACKAGE_NAME = PrintManager.PRINT_SPOOLER_PACKAGE_NAME
1001         private const val TEST_UID = 1
1002         private const val TEST_CHANNEL = "test_channel"
1003         private const val TEST_CHANNEL_NAME = "TEST CHANNEL NAME"
1004     }
1005 }
1006