Loading core/java/android/provider/Settings.java +9 −0 Original line number Diff line number Diff line Loading @@ -2406,6 +2406,15 @@ public final class Settings { public static final String ACTION_NOTIFICATION_HISTORY = "android.settings.NOTIFICATION_HISTORY"; /** * Activity Action: Show notification bundling settings screen * * @hide */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_NOTIFICATION_BUNDLES = "android.settings.NOTIFICATION_BUNDLES"; /** * Activity Action: Show app listing settings, filtered by those that send notifications. * Loading packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationUiAdjustmentTest.java +24 −3 Original line number Diff line number Diff line Loading @@ -232,6 +232,22 @@ public class NotificationUiAdjustmentTest extends SysuiTestCase { .isTrue(); } @Test public void needReinflate_differentBundling() { assertThat(NotificationUiAdjustment.needReinflate( createUiAdjustmentFromSmartReplies("first", new CharSequence[]{"a", "b"}), createUiAdjustmentFromSmartReplies("first", new CharSequence[] {"b", "a"}))) .isTrue(); } @Test public void needReinflate_sameBundling() { assertThat(NotificationUiAdjustment.needReinflate( createUiAdjustmentForBundling("first", true), createUiAdjustmentForBundling("first", true))) .isFalse(); } private Notification.Action.Builder createActionBuilder( String title, int drawableRes, PendingIntent pendingIntent) { return new Notification.Action.Builder( Loading @@ -244,16 +260,21 @@ public class NotificationUiAdjustmentTest extends SysuiTestCase { private NotificationUiAdjustment createUiAdjustmentFromSmartActions( String key, List<Notification.Action> actions) { return new NotificationUiAdjustment(key, actions, null, false); return new NotificationUiAdjustment(key, actions, null, false, false); } private NotificationUiAdjustment createUiAdjustmentFromSmartReplies( String key, CharSequence[] replies) { return new NotificationUiAdjustment(key, null, Arrays.asList(replies), false); return new NotificationUiAdjustment(key, null, Arrays.asList(replies), false, false); } private NotificationUiAdjustment createUiAdjustmentForConversation( String key, boolean isConversation) { return new NotificationUiAdjustment(key, null, null, isConversation); return new NotificationUiAdjustment(key, null, null, isConversation, false); } private NotificationUiAdjustment createUiAdjustmentForBundling( String key, boolean isBundle) { return new NotificationUiAdjustment(key, null, null, false, isBundle); } } packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt +18 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.notification.collection import android.app.ActivityManager import android.app.Notification import android.app.NotificationChannel import android.app.NotificationChannel.NEWS_ID import android.app.NotificationManager import android.app.PendingIntent import android.os.UserHandle Loading Loading @@ -563,4 +565,20 @@ class NotificationEntryAdapterTest : SysuiTestCase() { assertThat(underTest.remoteInputEntryAdapter) .isSameInstanceAs(entry.remoteInputEntryAdapter) } @Test fun isBundled() { val notification: Notification = Notification.Builder(mContext, "") .setSmallIcon(R.drawable.ic_person) .build() val entry = NotificationEntryBuilder() .setNotification(notification) .setChannel(NotificationChannel(NEWS_ID, NEWS_ID, 2)) .build() underTest = factory.create(entry) as NotificationEntryAdapter assertThat(underTest.isBundled).isTrue() } } packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt +40 −0 Original line number Diff line number Diff line Loading @@ -15,6 +15,9 @@ */ package com.android.systemui.statusbar.notification.collection.inflation import android.app.NotificationChannel import android.app.NotificationChannel.SOCIAL_MEDIA_ID import android.app.NotificationManager.IMPORTANCE_LOW import android.database.ContentObserver import android.os.Handler import android.platform.test.annotations.DisableFlags Loading Loading @@ -281,4 +284,41 @@ class NotifUiAdjustmentProviderTest : SysuiTestCase() { // Then: Need re-inflation assertTrue(NotifUiAdjustment.needReinflate(oldAdjustment, newAdjustment)) } @Test @EnableFlags(android.app.Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI) fun changeIsBundled_needReInflation_becomesBundled() { // Given: an Entry that is not bundled val oldAdjustment = adjustmentProvider.calculateAdjustment(entry) // When: the Entry is now bundled val rb = RankingBuilder(entry.ranking) rb.setChannel(NotificationChannel(SOCIAL_MEDIA_ID, "social", IMPORTANCE_LOW)) entry.ranking = rb.build() val newAdjustment = adjustmentProvider.calculateAdjustment(entry) assertThat(newAdjustment).isNotEqualTo(oldAdjustment) // Then: Need re-inflation assertTrue(NotifUiAdjustment.needReinflate(oldAdjustment, newAdjustment)) } @Test @EnableFlags(android.app.Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI) fun changeIsBundled_needReInflation_becomesUnbundled() { // Given: an Entry that is bundled val rb = RankingBuilder(entry.ranking) rb.setChannel(NotificationChannel(SOCIAL_MEDIA_ID, "social", IMPORTANCE_LOW)) entry.ranking = rb.build() val oldAdjustment = adjustmentProvider.calculateAdjustment(entry) // When: the Entry is now not bundled val rb2 = RankingBuilder(entry.ranking) rb2.setChannel(NotificationChannel("anything", "anything", IMPORTANCE_LOW)) entry.ranking = rb2.build() val newAdjustment = adjustmentProvider.calculateAdjustment(entry) assertThat(newAdjustment).isNotEqualTo(oldAdjustment) // Then: Need re-inflation assertTrue(NotifUiAdjustment.needReinflate(oldAdjustment, newAdjustment)) } } packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/BundledNotificationInfoTest.kt 0 → 100644 +270 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.systemui.statusbar.notification.row import android.app.INotificationManager import android.app.Notification import android.app.NotificationChannel import android.app.NotificationChannel.SOCIAL_MEDIA_ID import android.app.NotificationManager.IMPORTANCE_LOW import android.content.ComponentName import android.content.mockPackageManager import android.content.pm.ActivityInfo import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.os.UserHandle import android.os.testableLooper import android.print.PrintManager import android.service.notification.StatusBarNotification import android.telecom.TelecomManager import android.testing.TestableLooper.RunWithLooper import android.view.LayoutInflater import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.logging.MetricsLogger import com.android.internal.logging.UiEventLogger import com.android.internal.logging.metricsLogger import com.android.internal.logging.uiEventLoggerFake import com.android.systemui.Dependency import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.testCase import com.android.systemui.res.R import com.android.systemui.statusbar.notification.AssistantFeedbackController import com.android.systemui.statusbar.notification.collection.EntryAdapter import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.promoted.domain.interactor.PackageDemotionInteractor import com.android.systemui.statusbar.notification.row.icon.AppIconProvider import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider import com.android.systemui.statusbar.notification.row.icon.mockAppIconProvider import com.android.systemui.statusbar.notification.row.icon.mockNotificationIconStyleProvider import com.android.systemui.testKosmos import com.android.telecom.telecomManager import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyString import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @RunWithLooper class BundledNotificationInfoTest : SysuiTestCase() { private val kosmos = testKosmos().also { it.testCase = this } private lateinit var underTest: NotificationInfo private lateinit var notificationChannel: NotificationChannel private lateinit var defaultNotificationChannel: NotificationChannel private lateinit var classifiedNotificationChannel: NotificationChannel private lateinit var sbn: StatusBarNotification private lateinit var entry: NotificationEntry private lateinit var entryAdapter: EntryAdapter private val mockPackageManager = kosmos.mockPackageManager private val mockAppIconProvider = kosmos.mockAppIconProvider private val mockIconStyleProvider = kosmos.mockNotificationIconStyleProvider private val uiEventLogger = kosmos.uiEventLoggerFake private val testableLooper by lazy { kosmos.testableLooper } private val onUserInteractionCallback = mock<OnUserInteractionCallback>() private val mockINotificationManager = mock<INotificationManager>() private val channelEditorDialogController = mock<ChannelEditorDialogController>() private val packageDemotionInteractor = mock<PackageDemotionInteractor>() private val assistantFeedbackController = mock<AssistantFeedbackController>() @Before fun setUp() { mContext.addMockSystemService(TelecomManager::class.java, kosmos.telecomManager) mDependency.injectTestDependency(Dependency.BG_LOOPER, testableLooper.looper) // Inflate the layout val inflater = LayoutInflater.from(mContext) underTest = inflater.inflate(R.layout.bundled_notification_info, null) as NotificationInfo underTest.setGutsParent(mock<NotificationGuts>()) // Our view is never attached to a window so the View#post methods in NotificationInfo never // get called. Setting this will skip the post and do the action immediately. underTest.mSkipPost = true // PackageManager must return a packageInfo and applicationInfo. val packageInfo = PackageInfo() packageInfo.packageName = TEST_PACKAGE_NAME whenever(mockPackageManager.getPackageInfo(eq(TEST_PACKAGE_NAME), anyInt())) .thenReturn(packageInfo) val applicationInfo = ApplicationInfo() applicationInfo.uid = TEST_UID // non-zero val systemPackageInfo = PackageInfo() systemPackageInfo.packageName = TEST_SYSTEM_PACKAGE_NAME whenever(mockPackageManager.getPackageInfo(eq(TEST_SYSTEM_PACKAGE_NAME), anyInt())) .thenReturn(systemPackageInfo) whenever(mockPackageManager.getPackageInfo(eq("android"), anyInt())).thenReturn(packageInfo) val assistant = ComponentName("package", "service") whenever(mockINotificationManager.allowedNotificationAssistant).thenReturn(assistant) val ri = ResolveInfo() ri.activityInfo = ActivityInfo() ri.activityInfo.packageName = assistant.packageName ri.activityInfo.name = "activity" whenever(mockPackageManager.queryIntentActivities(any(), anyInt())).thenReturn(listOf(ri)) // Package has one channel by default. whenever( mockINotificationManager.getNumNotificationChannelsForPackage( eq(TEST_PACKAGE_NAME), eq(TEST_UID), anyBoolean(), ) ) .thenReturn(1) // Some test channels. notificationChannel = NotificationChannel(TEST_CHANNEL, TEST_CHANNEL_NAME, IMPORTANCE_LOW) defaultNotificationChannel = NotificationChannel( NotificationChannel.DEFAULT_CHANNEL_ID, TEST_CHANNEL_NAME, IMPORTANCE_LOW, ) classifiedNotificationChannel = NotificationChannel(SOCIAL_MEDIA_ID, "social", IMPORTANCE_LOW) val notification = Notification() notification.extras.putParcelable( Notification.EXTRA_BUILDER_APPLICATION_INFO, applicationInfo, ) sbn = StatusBarNotification( TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, 0, notification, UserHandle.getUserHandleForUid(TEST_UID), null, 0, ) entry = NotificationEntryBuilder() .setSbn(sbn) .updateRanking { it.setChannel(notificationChannel) } .build() entryAdapter = kosmos.entryAdapterFactory.create(entry) whenever(assistantFeedbackController.isFeedbackEnabled).thenReturn(false) whenever(assistantFeedbackController.getInlineDescriptionResource(any())) .thenReturn(R.string.notification_channel_summary_automatic) } @Test fun testHandleCloseControls_DoesNotMakeBinderCalllIfUnchanged() { bindNotification() underTest.handleCloseControls(true, false) testableLooper.processAllMessages() verify(mockINotificationManager, never()) .setAdjustmentSupportedForPackage(anyString(), anyString(), anyBoolean()) } @Test fun testToggleCallsUpdate() { whenever(mockINotificationManager.isAdjustmentSupportedForPackage( anyString(), anyString())).thenReturn(true) bindNotification() underTest.findViewById<View>(R.id.feature_toggle).performClick() underTest.findViewById<View>(R.id.done).performClick() underTest.handleCloseControls(true, false) testableLooper.processAllMessages() verify(mockINotificationManager) .setAdjustmentSupportedForPackage(anyString(), anyString(), eq(false)) } private fun bindNotification( pm: PackageManager = this.mockPackageManager, iNotificationManager: INotificationManager = this.mockINotificationManager, appIconProvider: AppIconProvider = this.mockAppIconProvider, iconStyleProvider: NotificationIconStyleProvider = this.mockIconStyleProvider, onUserInteractionCallback: OnUserInteractionCallback = this.onUserInteractionCallback, channelEditorDialogController: ChannelEditorDialogController = this.channelEditorDialogController, packageDemotionInteractor: PackageDemotionInteractor = this.packageDemotionInteractor, pkg: String = TEST_PACKAGE_NAME, entry: NotificationEntry = this.entry, entryAdapter: EntryAdapter = this.entryAdapter, onSettingsClick: NotificationInfo.OnSettingsClickListener? = mock(), onAppSettingsClick: NotificationInfo.OnAppSettingsClickListener? = mock(), onFeedbackClickListener: NotificationInfo.OnFeedbackClickListener? = mock(), uiEventLogger: UiEventLogger = this.uiEventLogger, isDeviceProvisioned: Boolean = true, isNonblockable: Boolean = false, isDismissable: Boolean = true, wasShownHighPriority: Boolean = true, assistantFeedbackController: AssistantFeedbackController = this.assistantFeedbackController, metricsLogger: MetricsLogger = kosmos.metricsLogger, onCloseClick: View.OnClickListener? = mock(), ) { underTest.bindNotification( pm, iNotificationManager, appIconProvider, iconStyleProvider, onUserInteractionCallback, channelEditorDialogController, packageDemotionInteractor, pkg, entry.ranking, entry.sbn, entry, entryAdapter, onSettingsClick, onAppSettingsClick, onFeedbackClickListener, uiEventLogger, isDeviceProvisioned, isNonblockable, isDismissable, wasShownHighPriority, assistantFeedbackController, metricsLogger, onCloseClick, ) } companion object { private const val TEST_PACKAGE_NAME = "test_package" private const val TEST_SYSTEM_PACKAGE_NAME = PrintManager.PRINT_SPOOLER_PACKAGE_NAME private const val TEST_UID = 1 private const val TEST_CHANNEL = "test_channel" private const val TEST_CHANNEL_NAME = "TEST CHANNEL NAME" } } Loading
core/java/android/provider/Settings.java +9 −0 Original line number Diff line number Diff line Loading @@ -2406,6 +2406,15 @@ public final class Settings { public static final String ACTION_NOTIFICATION_HISTORY = "android.settings.NOTIFICATION_HISTORY"; /** * Activity Action: Show notification bundling settings screen * * @hide */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_NOTIFICATION_BUNDLES = "android.settings.NOTIFICATION_BUNDLES"; /** * Activity Action: Show app listing settings, filtered by those that send notifications. * Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationUiAdjustmentTest.java +24 −3 Original line number Diff line number Diff line Loading @@ -232,6 +232,22 @@ public class NotificationUiAdjustmentTest extends SysuiTestCase { .isTrue(); } @Test public void needReinflate_differentBundling() { assertThat(NotificationUiAdjustment.needReinflate( createUiAdjustmentFromSmartReplies("first", new CharSequence[]{"a", "b"}), createUiAdjustmentFromSmartReplies("first", new CharSequence[] {"b", "a"}))) .isTrue(); } @Test public void needReinflate_sameBundling() { assertThat(NotificationUiAdjustment.needReinflate( createUiAdjustmentForBundling("first", true), createUiAdjustmentForBundling("first", true))) .isFalse(); } private Notification.Action.Builder createActionBuilder( String title, int drawableRes, PendingIntent pendingIntent) { return new Notification.Action.Builder( Loading @@ -244,16 +260,21 @@ public class NotificationUiAdjustmentTest extends SysuiTestCase { private NotificationUiAdjustment createUiAdjustmentFromSmartActions( String key, List<Notification.Action> actions) { return new NotificationUiAdjustment(key, actions, null, false); return new NotificationUiAdjustment(key, actions, null, false, false); } private NotificationUiAdjustment createUiAdjustmentFromSmartReplies( String key, CharSequence[] replies) { return new NotificationUiAdjustment(key, null, Arrays.asList(replies), false); return new NotificationUiAdjustment(key, null, Arrays.asList(replies), false, false); } private NotificationUiAdjustment createUiAdjustmentForConversation( String key, boolean isConversation) { return new NotificationUiAdjustment(key, null, null, isConversation); return new NotificationUiAdjustment(key, null, null, isConversation, false); } private NotificationUiAdjustment createUiAdjustmentForBundling( String key, boolean isBundle) { return new NotificationUiAdjustment(key, null, null, false, isBundle); } }
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt +18 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.notification.collection import android.app.ActivityManager import android.app.Notification import android.app.NotificationChannel import android.app.NotificationChannel.NEWS_ID import android.app.NotificationManager import android.app.PendingIntent import android.os.UserHandle Loading Loading @@ -563,4 +565,20 @@ class NotificationEntryAdapterTest : SysuiTestCase() { assertThat(underTest.remoteInputEntryAdapter) .isSameInstanceAs(entry.remoteInputEntryAdapter) } @Test fun isBundled() { val notification: Notification = Notification.Builder(mContext, "") .setSmallIcon(R.drawable.ic_person) .build() val entry = NotificationEntryBuilder() .setNotification(notification) .setChannel(NotificationChannel(NEWS_ID, NEWS_ID, 2)) .build() underTest = factory.create(entry) as NotificationEntryAdapter assertThat(underTest.isBundled).isTrue() } }
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt +40 −0 Original line number Diff line number Diff line Loading @@ -15,6 +15,9 @@ */ package com.android.systemui.statusbar.notification.collection.inflation import android.app.NotificationChannel import android.app.NotificationChannel.SOCIAL_MEDIA_ID import android.app.NotificationManager.IMPORTANCE_LOW import android.database.ContentObserver import android.os.Handler import android.platform.test.annotations.DisableFlags Loading Loading @@ -281,4 +284,41 @@ class NotifUiAdjustmentProviderTest : SysuiTestCase() { // Then: Need re-inflation assertTrue(NotifUiAdjustment.needReinflate(oldAdjustment, newAdjustment)) } @Test @EnableFlags(android.app.Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI) fun changeIsBundled_needReInflation_becomesBundled() { // Given: an Entry that is not bundled val oldAdjustment = adjustmentProvider.calculateAdjustment(entry) // When: the Entry is now bundled val rb = RankingBuilder(entry.ranking) rb.setChannel(NotificationChannel(SOCIAL_MEDIA_ID, "social", IMPORTANCE_LOW)) entry.ranking = rb.build() val newAdjustment = adjustmentProvider.calculateAdjustment(entry) assertThat(newAdjustment).isNotEqualTo(oldAdjustment) // Then: Need re-inflation assertTrue(NotifUiAdjustment.needReinflate(oldAdjustment, newAdjustment)) } @Test @EnableFlags(android.app.Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI) fun changeIsBundled_needReInflation_becomesUnbundled() { // Given: an Entry that is bundled val rb = RankingBuilder(entry.ranking) rb.setChannel(NotificationChannel(SOCIAL_MEDIA_ID, "social", IMPORTANCE_LOW)) entry.ranking = rb.build() val oldAdjustment = adjustmentProvider.calculateAdjustment(entry) // When: the Entry is now not bundled val rb2 = RankingBuilder(entry.ranking) rb2.setChannel(NotificationChannel("anything", "anything", IMPORTANCE_LOW)) entry.ranking = rb2.build() val newAdjustment = adjustmentProvider.calculateAdjustment(entry) assertThat(newAdjustment).isNotEqualTo(oldAdjustment) // Then: Need re-inflation assertTrue(NotifUiAdjustment.needReinflate(oldAdjustment, newAdjustment)) } }
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/BundledNotificationInfoTest.kt 0 → 100644 +270 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.systemui.statusbar.notification.row import android.app.INotificationManager import android.app.Notification import android.app.NotificationChannel import android.app.NotificationChannel.SOCIAL_MEDIA_ID import android.app.NotificationManager.IMPORTANCE_LOW import android.content.ComponentName import android.content.mockPackageManager import android.content.pm.ActivityInfo import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.os.UserHandle import android.os.testableLooper import android.print.PrintManager import android.service.notification.StatusBarNotification import android.telecom.TelecomManager import android.testing.TestableLooper.RunWithLooper import android.view.LayoutInflater import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.logging.MetricsLogger import com.android.internal.logging.UiEventLogger import com.android.internal.logging.metricsLogger import com.android.internal.logging.uiEventLoggerFake import com.android.systemui.Dependency import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.testCase import com.android.systemui.res.R import com.android.systemui.statusbar.notification.AssistantFeedbackController import com.android.systemui.statusbar.notification.collection.EntryAdapter import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.promoted.domain.interactor.PackageDemotionInteractor import com.android.systemui.statusbar.notification.row.icon.AppIconProvider import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider import com.android.systemui.statusbar.notification.row.icon.mockAppIconProvider import com.android.systemui.statusbar.notification.row.icon.mockNotificationIconStyleProvider import com.android.systemui.testKosmos import com.android.telecom.telecomManager import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyString import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @RunWithLooper class BundledNotificationInfoTest : SysuiTestCase() { private val kosmos = testKosmos().also { it.testCase = this } private lateinit var underTest: NotificationInfo private lateinit var notificationChannel: NotificationChannel private lateinit var defaultNotificationChannel: NotificationChannel private lateinit var classifiedNotificationChannel: NotificationChannel private lateinit var sbn: StatusBarNotification private lateinit var entry: NotificationEntry private lateinit var entryAdapter: EntryAdapter private val mockPackageManager = kosmos.mockPackageManager private val mockAppIconProvider = kosmos.mockAppIconProvider private val mockIconStyleProvider = kosmos.mockNotificationIconStyleProvider private val uiEventLogger = kosmos.uiEventLoggerFake private val testableLooper by lazy { kosmos.testableLooper } private val onUserInteractionCallback = mock<OnUserInteractionCallback>() private val mockINotificationManager = mock<INotificationManager>() private val channelEditorDialogController = mock<ChannelEditorDialogController>() private val packageDemotionInteractor = mock<PackageDemotionInteractor>() private val assistantFeedbackController = mock<AssistantFeedbackController>() @Before fun setUp() { mContext.addMockSystemService(TelecomManager::class.java, kosmos.telecomManager) mDependency.injectTestDependency(Dependency.BG_LOOPER, testableLooper.looper) // Inflate the layout val inflater = LayoutInflater.from(mContext) underTest = inflater.inflate(R.layout.bundled_notification_info, null) as NotificationInfo underTest.setGutsParent(mock<NotificationGuts>()) // Our view is never attached to a window so the View#post methods in NotificationInfo never // get called. Setting this will skip the post and do the action immediately. underTest.mSkipPost = true // PackageManager must return a packageInfo and applicationInfo. val packageInfo = PackageInfo() packageInfo.packageName = TEST_PACKAGE_NAME whenever(mockPackageManager.getPackageInfo(eq(TEST_PACKAGE_NAME), anyInt())) .thenReturn(packageInfo) val applicationInfo = ApplicationInfo() applicationInfo.uid = TEST_UID // non-zero val systemPackageInfo = PackageInfo() systemPackageInfo.packageName = TEST_SYSTEM_PACKAGE_NAME whenever(mockPackageManager.getPackageInfo(eq(TEST_SYSTEM_PACKAGE_NAME), anyInt())) .thenReturn(systemPackageInfo) whenever(mockPackageManager.getPackageInfo(eq("android"), anyInt())).thenReturn(packageInfo) val assistant = ComponentName("package", "service") whenever(mockINotificationManager.allowedNotificationAssistant).thenReturn(assistant) val ri = ResolveInfo() ri.activityInfo = ActivityInfo() ri.activityInfo.packageName = assistant.packageName ri.activityInfo.name = "activity" whenever(mockPackageManager.queryIntentActivities(any(), anyInt())).thenReturn(listOf(ri)) // Package has one channel by default. whenever( mockINotificationManager.getNumNotificationChannelsForPackage( eq(TEST_PACKAGE_NAME), eq(TEST_UID), anyBoolean(), ) ) .thenReturn(1) // Some test channels. notificationChannel = NotificationChannel(TEST_CHANNEL, TEST_CHANNEL_NAME, IMPORTANCE_LOW) defaultNotificationChannel = NotificationChannel( NotificationChannel.DEFAULT_CHANNEL_ID, TEST_CHANNEL_NAME, IMPORTANCE_LOW, ) classifiedNotificationChannel = NotificationChannel(SOCIAL_MEDIA_ID, "social", IMPORTANCE_LOW) val notification = Notification() notification.extras.putParcelable( Notification.EXTRA_BUILDER_APPLICATION_INFO, applicationInfo, ) sbn = StatusBarNotification( TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, 0, notification, UserHandle.getUserHandleForUid(TEST_UID), null, 0, ) entry = NotificationEntryBuilder() .setSbn(sbn) .updateRanking { it.setChannel(notificationChannel) } .build() entryAdapter = kosmos.entryAdapterFactory.create(entry) whenever(assistantFeedbackController.isFeedbackEnabled).thenReturn(false) whenever(assistantFeedbackController.getInlineDescriptionResource(any())) .thenReturn(R.string.notification_channel_summary_automatic) } @Test fun testHandleCloseControls_DoesNotMakeBinderCalllIfUnchanged() { bindNotification() underTest.handleCloseControls(true, false) testableLooper.processAllMessages() verify(mockINotificationManager, never()) .setAdjustmentSupportedForPackage(anyString(), anyString(), anyBoolean()) } @Test fun testToggleCallsUpdate() { whenever(mockINotificationManager.isAdjustmentSupportedForPackage( anyString(), anyString())).thenReturn(true) bindNotification() underTest.findViewById<View>(R.id.feature_toggle).performClick() underTest.findViewById<View>(R.id.done).performClick() underTest.handleCloseControls(true, false) testableLooper.processAllMessages() verify(mockINotificationManager) .setAdjustmentSupportedForPackage(anyString(), anyString(), eq(false)) } private fun bindNotification( pm: PackageManager = this.mockPackageManager, iNotificationManager: INotificationManager = this.mockINotificationManager, appIconProvider: AppIconProvider = this.mockAppIconProvider, iconStyleProvider: NotificationIconStyleProvider = this.mockIconStyleProvider, onUserInteractionCallback: OnUserInteractionCallback = this.onUserInteractionCallback, channelEditorDialogController: ChannelEditorDialogController = this.channelEditorDialogController, packageDemotionInteractor: PackageDemotionInteractor = this.packageDemotionInteractor, pkg: String = TEST_PACKAGE_NAME, entry: NotificationEntry = this.entry, entryAdapter: EntryAdapter = this.entryAdapter, onSettingsClick: NotificationInfo.OnSettingsClickListener? = mock(), onAppSettingsClick: NotificationInfo.OnAppSettingsClickListener? = mock(), onFeedbackClickListener: NotificationInfo.OnFeedbackClickListener? = mock(), uiEventLogger: UiEventLogger = this.uiEventLogger, isDeviceProvisioned: Boolean = true, isNonblockable: Boolean = false, isDismissable: Boolean = true, wasShownHighPriority: Boolean = true, assistantFeedbackController: AssistantFeedbackController = this.assistantFeedbackController, metricsLogger: MetricsLogger = kosmos.metricsLogger, onCloseClick: View.OnClickListener? = mock(), ) { underTest.bindNotification( pm, iNotificationManager, appIconProvider, iconStyleProvider, onUserInteractionCallback, channelEditorDialogController, packageDemotionInteractor, pkg, entry.ranking, entry.sbn, entry, entryAdapter, onSettingsClick, onAppSettingsClick, onFeedbackClickListener, uiEventLogger, isDeviceProvisioned, isNonblockable, isDismissable, wasShownHighPriority, assistantFeedbackController, metricsLogger, onCloseClick, ) } companion object { private const val TEST_PACKAGE_NAME = "test_package" private const val TEST_SYSTEM_PACKAGE_NAME = PrintManager.PRINT_SPOOLER_PACKAGE_NAME private const val TEST_UID = 1 private const val TEST_CHANNEL = "test_channel" private const val TEST_CHANNEL_NAME = "TEST CHANNEL NAME" } }