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