Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5caee228 authored by Julia Reynolds's avatar Julia Reynolds Committed by Android (Google) Code Review
Browse files

Merge "Add guts for bundled notifications" into main

parents 8fe006d9 3e69b5df
Loading
Loading
Loading
Loading
+9 −0
Original line number Diff line number Diff line
@@ -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.
     *
+24 −3
Original line number Diff line number Diff line
@@ -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(
@@ -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);
    }
}
+18 −0
Original line number Diff line number Diff line
@@ -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
@@ -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()
    }
}
+40 −0
Original line number Diff line number Diff line
@@ -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
@@ -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))
    }
}
+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