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

Commit d38a8e79 authored by Jeff DeCew's avatar Jeff DeCew Committed by Android (Google) Code Review
Browse files

Merge "[RONs] Improve AOD redaction to use the public version" into main

parents 49fb6997 c3f9fb01
Loading
Loading
Loading
Loading
+7 −12
Original line number Diff line number Diff line
@@ -32,12 +32,11 @@ import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.core.StatusBarRootModernization
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.buildPromotedOngoingEntry
import com.android.systemui.statusbar.notification.domain.interactor.renderNotificationListInteractor
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization
import com.android.systemui.statusbar.policy.domain.interactor.sensitiveNotificationProtectionInteractor
import com.android.systemui.statusbar.policy.mockSensitiveNotificationProtectionController
import com.android.systemui.testKosmos
@@ -50,12 +49,8 @@ import org.mockito.kotlin.whenever

@SmallTest
@RunWith(AndroidJUnit4::class)
@EnableFlags(
    PromotedNotificationUi.FLAG_NAME,
    StatusBarNotifChips.FLAG_NAME,
    StatusBarChipsModernization.FLAG_NAME,
    StatusBarRootModernization.FLAG_NAME,
)
@EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
@EnableChipsModernization
class AODPromotedNotificationsInteractorTest : SysuiTestCase() {
    private val kosmos = testKosmos().useUnconfinedTestDispatcher()

@@ -111,10 +106,10 @@ class AODPromotedNotificationsInteractorTest : SysuiTestCase() {

            renderNotificationListInteractor.setRenderedList(listOf(ronEntry))

            // THEN aod content is sensitive
            // THEN aod content is redacted
            val content by collectLastValue(underTest.content)
            assertThat(content).isNotNull()
            assertThat(content?.title).isNull() // SOON: .isEqualTo("REDACTED")
            assertThat(content!!.title).isEqualTo("REDACTED")
        }

    @Test
@@ -128,10 +123,10 @@ class AODPromotedNotificationsInteractorTest : SysuiTestCase() {

            renderNotificationListInteractor.setRenderedList(listOf(ronEntry))

            // THEN aod content is sensitive
            // THEN aod content is redacted
            val content by collectLastValue(underTest.content)
            assertThat(content).isNotNull()
            assertThat(content?.title).isNull() // SOON: .isEqualTo("REDACTED")
            assertThat(content!!.title).isEqualTo("REDACTED")
        }

    private fun Kosmos.setKeyguardLocked(locked: Boolean) {
+56 −29
Original line number Diff line number Diff line
@@ -205,18 +205,22 @@ private val PromotedNotificationContentModel.layoutResource: Int?
        return if (notificationsRedesignTemplates()) {
            when (style) {
                Style.Base -> R.layout.notification_2025_template_expanded_base
                Style.CollapsedBase -> R.layout.notification_2025_template_collapsed_base
                Style.BigPicture -> R.layout.notification_2025_template_expanded_big_picture
                Style.BigText -> R.layout.notification_2025_template_expanded_big_text
                Style.Call -> R.layout.notification_2025_template_expanded_call
                Style.CollapsedCall -> R.layout.notification_2025_template_collapsed_call
                Style.Progress -> R.layout.notification_2025_template_expanded_progress
                Style.Ineligible -> null
            }
        } else {
            when (style) {
                Style.Base -> R.layout.notification_template_material_big_base
                Style.CollapsedBase -> R.layout.notification_template_material_base
                Style.BigPicture -> R.layout.notification_template_material_big_picture
                Style.BigText -> R.layout.notification_template_material_big_text
                Style.Call -> R.layout.notification_template_material_big_call
                Style.CollapsedCall -> R.layout.notification_template_material_call
                Style.Progress -> R.layout.notification_template_material_progress
                Style.Ineligible -> null
            }
@@ -333,10 +337,12 @@ private class AODPromotedNotificationViewUpdater(root: View) {

    fun update(content: PromotedNotificationContentModel, audiblyAlertedIconVisible: Boolean) {
        when (content.style) {
            Style.Base -> updateBase(content)
            Style.Base -> updateBase(content, collapsed = false)
            Style.CollapsedBase -> updateBase(content, collapsed = true)
            Style.BigPicture -> updateBigPictureStyle(content)
            Style.BigText -> updateBigTextStyle(content)
            Style.Call -> updateCallStyle(content)
            Style.Call -> updateCallStyle(content, collapsed = false)
            Style.CollapsedCall -> updateCallStyle(content, collapsed = true)
            Style.Progress -> updateProgressStyle(content)
            Style.Ineligible -> {}
        }
@@ -346,11 +352,15 @@ private class AODPromotedNotificationViewUpdater(root: View) {

    private fun updateBase(
        content: PromotedNotificationContentModel,
        collapsed: Boolean,
        textView: ImageFloatingTextView? = text,
    ) {
        updateHeader(content)
        val headerTitleView = if (collapsed) title else null
        updateHeader(content, titleView = headerTitleView, collapsed = collapsed)

        if (headerTitleView == null) {
            updateTitle(title, content)
        }
        updateText(textView, content)
        updateSmallIcon(icon, content)
        updateImageView(rightIcon, content.skeletonLargeIcon)
@@ -358,21 +368,21 @@ private class AODPromotedNotificationViewUpdater(root: View) {
    }

    private fun updateBigPictureStyle(content: PromotedNotificationContentModel) {
        updateBase(content)
        updateBase(content, collapsed = false)
    }

    private fun updateBigTextStyle(content: PromotedNotificationContentModel) {
        updateBase(content, textView = bigText)
        updateBase(content, collapsed = false, textView = bigText)
    }

    private fun updateCallStyle(content: PromotedNotificationContentModel) {
        updateConversationHeader(content)
    private fun updateCallStyle(content: PromotedNotificationContentModel, collapsed: Boolean) {
        updateConversationHeader(content, collapsed = collapsed)

        updateText(text, content)
    }

    private fun updateProgressStyle(content: PromotedNotificationContentModel) {
        updateBase(content)
        updateBase(content, collapsed = false)

        updateNewProgressBar(content)
    }
@@ -409,24 +419,35 @@ private class AODPromotedNotificationViewUpdater(root: View) {
        }
    }

    private fun updateHeader(content: PromotedNotificationContentModel) {
        updateAppName(content)
    private fun updateHeader(
        content: PromotedNotificationContentModel,
        collapsed: Boolean,
        titleView: TextView?,
    ) {
        val hasTitle = titleView != null && content.title != null
        val hasSubText = content.subText != null
        // the collapsed form doesn't show the app name unless there is no other text in the header
        val appNameRequired = !hasTitle && !hasSubText
        val hideAppName = (!appNameRequired && collapsed)

        updateAppName(content, forceHide = hideAppName)
        updateTextView(headerTextSecondary, content.subText)
        // Not calling updateTitle(headerText, content) because the title is always a separate
        // element in the expanded layout used for AOD RONs.
        updateTitle(titleView, content)
        updateTimeAndChronometer(content)

        updateHeaderDividers(content)
        updateHeaderDividers(content, hideTitle = !hasTitle, hideAppName = hideAppName)

        updateTopLine(content)
    }

    private fun updateHeaderDividers(content: PromotedNotificationContentModel) {
        val hasAppName = content.appName != null
    private fun updateHeaderDividers(
        content: PromotedNotificationContentModel,
        hideAppName: Boolean,
        hideTitle: Boolean,
    ) {
        val hasAppName = content.appName != null && !hideAppName
        val hasSubText = content.subText != null
        // Not setting hasHeader = content.title because the title is always a separate element in
        // the expanded layout used for AOD RONs.
        val hasHeader = false
        val hasHeader = content.title != null && !hideTitle
        val hasTimeOrChronometer = content.time != null

        val hasTextBeforeSubText = hasAppName
@@ -442,13 +463,17 @@ private class AODPromotedNotificationViewUpdater(root: View) {
        timeDivider?.isVisible = showDividerBeforeTime
    }

    private fun updateConversationHeader(content: PromotedNotificationContentModel) {
        updateAppName(content)
    private fun updateConversationHeader(
        content: PromotedNotificationContentModel,
        collapsed: Boolean,
    ) {
        updateAppName(content, forceHide = collapsed)
        updateTimeAndChronometer(content)

        updateImageView(verificationIcon, content.verificationIcon)
        updateTextView(verificationText, content.verificationText)

        updateConversationHeaderDividers(content)
        updateConversationHeaderDividers(content, hideTitle = true, hideAppName = collapsed)

        updateTopLine(content)

@@ -456,11 +481,13 @@ private class AODPromotedNotificationViewUpdater(root: View) {
        updateTitle(conversationText, content)
    }

    private fun updateConversationHeaderDividers(content: PromotedNotificationContentModel) {
        // Not setting hasTitle = content.title because the title is always a separate element in
        // the expanded layout used for AOD RONs.
        val hasTitle = false
        val hasAppName = content.appName != null
    private fun updateConversationHeaderDividers(
        content: PromotedNotificationContentModel,
        hideTitle: Boolean,
        hideAppName: Boolean,
    ) {
        val hasTitle = content.title != null && !hideTitle
        val hasAppName = content.appName != null && !hideAppName
        val hasTimeOrChronometer = content.time != null
        val hasVerification =
            !content.verificationIcon.isNullOrEmpty() || content.verificationText != null
@@ -478,8 +505,8 @@ private class AODPromotedNotificationViewUpdater(root: View) {
        verificationDivider?.isVisible = showDividerBeforeVerification
    }

    private fun updateAppName(content: PromotedNotificationContentModel) {
        updateTextView(appNameText, content.appName)
    private fun updateAppName(content: PromotedNotificationContentModel, forceHide: Boolean) {
        updateTextView(appNameText, content.appName?.takeUnless { forceHide })
    }

    private fun updateTitle(titleView: TextView?, content: PromotedNotificationContentModel) {
+77 −46
Original line number Diff line number Diff line
@@ -112,12 +112,13 @@ constructor(
            if (redactionType == REDACTION_TYPE_NONE) {
                privateVersion
            } else {
                if (notification.publicVersion == null) {
                    privateVersion.toDefaultPublicVersion()
                } else {
                    // TODO(b/400991304): implement extraction for [Notification.publicVersion]
                    privateVersion.toDefaultPublicVersion()
                }
                notification.publicVersion?.let { publicNotification ->
                    createAppDefinedPublicVersion(
                        privateModel = privateVersion,
                        publicNotification = publicNotification,
                        imageModelProvider = imageModelProvider,
                    )
                } ?: createDefaultPublicVersion(privateModel = privateVersion)
            }
        return PromotedNotificationContentModels(
                privateVersion = privateVersion,
@@ -126,19 +127,59 @@ constructor(
            .also { logger.logExtractionSucceeded(entry, it) }
    }

    private fun PromotedNotificationContentModel.toDefaultPublicVersion():
        PromotedNotificationContentModel =
        PromotedNotificationContentModel.Builder(key = identity.key).let {
            it.style = if (style == Style.Ineligible) Style.Ineligible else Style.Base
            it.smallIcon = smallIcon
            it.iconLevel = iconLevel
            it.appName = appName
            it.time = time
            it.lastAudiblyAlertedMs = lastAudiblyAlertedMs
            it.profileBadgeResId = profileBadgeResId
            it.colors = colors
            it.build()
        }
    private fun copyNonSensitiveFields(
        privateModel: PromotedNotificationContentModel,
        publicBuilder: PromotedNotificationContentModel.Builder,
    ) {
        publicBuilder.smallIcon = privateModel.smallIcon
        publicBuilder.iconLevel = privateModel.iconLevel
        publicBuilder.appName = privateModel.appName
        publicBuilder.time = privateModel.time
        publicBuilder.lastAudiblyAlertedMs = privateModel.lastAudiblyAlertedMs
        publicBuilder.profileBadgeResId = privateModel.profileBadgeResId
        publicBuilder.colors = privateModel.colors
    }

    private fun createDefaultPublicVersion(
        privateModel: PromotedNotificationContentModel
    ): PromotedNotificationContentModel =
        PromotedNotificationContentModel.Builder(key = privateModel.identity.key)
            .also {
                it.style =
                    if (privateModel.style == Style.Ineligible) Style.Ineligible else Style.Base
                copyNonSensitiveFields(privateModel, it)
            }
            .build()

    private fun createAppDefinedPublicVersion(
        privateModel: PromotedNotificationContentModel,
        publicNotification: Notification,
        imageModelProvider: ImageModelProvider,
    ): PromotedNotificationContentModel =
        PromotedNotificationContentModel.Builder(key = privateModel.identity.key)
            .also { publicBuilder ->
                val notificationStyle = publicNotification.notificationStyle
                publicBuilder.style =
                    when {
                        privateModel.style == Style.Ineligible -> Style.Ineligible
                        notificationStyle == CallStyle::class.java -> Style.CollapsedCall
                        else -> Style.CollapsedBase
                    }
                copyNonSensitiveFields(privateModel = privateModel, publicBuilder = publicBuilder)
                publicBuilder.shortCriticalText = publicNotification.shortCriticalText()
                publicBuilder.subText = publicNotification.subText()
                // The standard public version is extracted as a collapsed notification,
                //  so avoid using bigTitle or bigText, and instead get the collapsed versions.
                publicBuilder.title = publicNotification.title(notificationStyle, expanded = false)
                publicBuilder.text = publicNotification.text()
                publicBuilder.skeletonLargeIcon =
                    publicNotification.skeletonLargeIcon(imageModelProvider)
                // Only CallStyle has styled content that shows in the collapsed version.
                if (publicBuilder.style == Style.Call) {
                    extractCallStyleContent(publicNotification, publicBuilder, imageModelProvider)
                }
            }
            .build()

    private fun extractPrivateContent(
        key: String,
@@ -163,8 +204,8 @@ constructor(
        contentBuilder.shortCriticalText = notification.shortCriticalText()
        contentBuilder.lastAudiblyAlertedMs = lastAudiblyAlertedMs
        contentBuilder.profileBadgeResId = null // TODO
        contentBuilder.title = notification.title(recoveredBuilder.style)
        contentBuilder.text = notification.text(recoveredBuilder.style)
        contentBuilder.title = notification.title(recoveredBuilder.style?.javaClass)
        contentBuilder.text = notification.text(recoveredBuilder.style?.javaClass)
        contentBuilder.skeletonLargeIcon = notification.skeletonLargeIcon(imageModelProvider)
        contentBuilder.oldProgress = notification.oldProgress()

@@ -191,12 +232,16 @@ constructor(
    private fun Notification.callPerson(): Person? =
        extras?.getParcelable(EXTRA_CALL_PERSON, Person::class.java)

    private fun Notification.title(style: Notification.Style?): CharSequence? {
        return when (style) {
            is BigTextStyle,
            is BigPictureStyle,
            is InboxStyle -> bigTitle()
            is CallStyle -> callPerson()?.name
    private fun Notification.title(
        styleClass: Class<out Notification.Style>?,
        expanded: Boolean = true,
    ): CharSequence? {
        // bigTitle is only used in the expanded form of 3 styles.
        return when (styleClass) {
            BigTextStyle::class.java,
            BigPictureStyle::class.java,
            InboxStyle::class.java -> if (expanded) bigTitle() else null
            CallStyle::class.java -> callPerson()?.name?.takeUnlessEmpty()
            else -> null
        } ?: title()
    }
@@ -206,9 +251,9 @@ constructor(
    private fun Notification.bigText(): CharSequence? =
        getCharSequenceExtraUnlessEmpty(EXTRA_BIG_TEXT)

    private fun Notification.text(style: Notification.Style?): CharSequence? {
        return when (style) {
            is BigTextStyle -> bigText()
    private fun Notification.text(styleClass: Class<out Notification.Style>?): CharSequence? {
        return when (styleClass) {
            BigTextStyle::class.java -> bigText()
            else -> null
        } ?: text()
    }
@@ -293,17 +338,15 @@ constructor(
                null -> Style.Base

                is BigPictureStyle -> {
                    style.extractContent(contentBuilder)
                    Style.BigPicture
                }

                is BigTextStyle -> {
                    style.extractContent(contentBuilder)
                    Style.BigText
                }

                is CallStyle -> {
                    style.extractContent(notification, contentBuilder, imageModelProvider)
                    extractCallStyleContent(notification, contentBuilder, imageModelProvider)
                    Style.Call
                }

@@ -316,19 +359,7 @@ constructor(
            }
    }

    private fun BigPictureStyle.extractContent(
        contentBuilder: PromotedNotificationContentModel.Builder
    ) {
        // Big title is handled in resolveTitle, and big picture is unsupported.
    }

    private fun BigTextStyle.extractContent(
        contentBuilder: PromotedNotificationContentModel.Builder
    ) {
        // Big title and big text are handled in resolveTitle and resolveText.
    }

    private fun CallStyle.extractContent(
    private fun extractCallStyleContent(
        notification: Notification,
        contentBuilder: PromotedNotificationContentModel.Builder,
        imageModelProvider: ImageModelProvider,
+2 −0
Original line number Diff line number Diff line
@@ -174,9 +174,11 @@ data class PromotedNotificationContentModel(
    /** The promotion-eligible style of a notification, or [Style.Ineligible] if not. */
    enum class Style {
        Base, // style == null
        CollapsedBase, // style == null
        BigPicture,
        BigText,
        Call,
        CollapsedCall,
        Progress,
        Ineligible,
    }