Loading core/java/com/android/internal/widget/ConversationLayout.java +5 −4 Original line number Diff line number Diff line Loading @@ -399,7 +399,9 @@ public class ConversationLayout extends FrameLayout @RemotableViewMethod(asyncImpl = "setIsCollapsedAsync") public void setIsCollapsed(boolean isCollapsed) { mIsCollapsed = isCollapsed; mMessagingLinearLayout.setMaxDisplayedLines(isCollapsed ? 1 : Integer.MAX_VALUE); mMessagingLinearLayout.setMaxDisplayedLines(isCollapsed ? TextUtils.isEmpty(mSummarizedContent) ? 1 : 2 : Integer.MAX_VALUE); updateExpandButton(); updateContentEndPaddings(); } Loading Loading @@ -448,7 +450,7 @@ public class ConversationLayout extends FrameLayout List<MessagingMessage> newMessagingMessages; mSummarizedContent = extras.getCharSequence(Notification.EXTRA_SUMMARIZED_CONTENT); if (mSummarizedContent != null && mIsCollapsed) { if (!TextUtils.isEmpty(mSummarizedContent) && mIsCollapsed) { Notification.MessagingStyle.Message summary = new Notification.MessagingStyle.Message(mSummarizedContent, 0, ""); newMessagingMessages = createMessages(List.of(summary), false, usePrecomputedText); Loading Loading @@ -1162,7 +1164,7 @@ public class ConversationLayout extends FrameLayout nameOverride = mNameReplacement; } newGroup.setShowingAvatar(!mIsOneToOne && !mIsCollapsed); newGroup.setSingleLine(mIsCollapsed); newGroup.setSingleLine(mIsCollapsed && TextUtils.isEmpty(mSummarizedContent)); newGroup.setSender(sender, nameOverride); newGroup.setSending(groupIndex == (groups.size() - 1) && showSpinner); mGroups.add(newGroup); Loading Loading @@ -1462,7 +1464,6 @@ public class ConversationLayout extends FrameLayout maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); } maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); if (maxHeight != getMeasuredHeight()) { setMeasuredDimension(getMeasuredWidth(), maxHeight); Loading core/java/com/android/internal/widget/MessagingLayout.java +3 −2 Original line number Diff line number Diff line Loading @@ -198,7 +198,8 @@ public class MessagingLayout extends FrameLayout /* isHistoric= */true, usePrecomputedText); List<MessagingMessage> newMessagingMessages; mSummarizedContent = extras.getCharSequence(Notification.EXTRA_SUMMARIZED_CONTENT); if (mSummarizedContent != null && mIsCollapsed) { if (!TextUtils.isEmpty(mSummarizedContent) && mIsCollapsed) { mMessagingLinearLayout.setMaxDisplayedLines(2); Notification.MessagingStyle.Message summary = new Notification.MessagingStyle.Message(mSummarizedContent, 0, ""); newMessagingMessages = createMessages(List.of(summary), false, usePrecomputedText); Loading Loading @@ -488,7 +489,7 @@ public class MessagingLayout extends FrameLayout if (sender != mUser && mNameReplacement != null) { nameOverride = mNameReplacement; } newGroup.setSingleLine(mIsCollapsed); newGroup.setSingleLine(mIsCollapsed && TextUtils.isEmpty(mSummarizedContent)); newGroup.setShowingAvatar(!mIsCollapsed); newGroup.setSender(sender, nameOverride); newGroup.setSending(groupIndex == (groups.size() - 1) && showSpinner); Loading core/res/res/values/dimens.xml +6 −3 Original line number Diff line number Diff line Loading @@ -431,15 +431,18 @@ <!-- The minimum height of the notification content (even when there's only one line of text) --> <dimen name="notification_2025_content_min_height">40dp</dimen> <!-- Height of a headerless notification with one or two lines --> <!-- 16 * 2 (margins) + 40 (min content height) = 72 (notification) --> <!-- Max height of a collapsed (headerless) notification with a summarization --> <dimen name="notification_collapsed_height_with_summarization">156dp</dimen> <!-- Max height of a collapsed (headerless) notification with one or two lines --> <!-- 16 * 2 (margins) + 48 (min content height) = 72 (notification) --> <dimen name="notification_2025_min_height">72dp</dimen> <!-- Height of a headerless notification with one line --> <!-- 16 * 2 (margins) + 24 (1 line) = 56 (notification) --> <dimen name="notification_headerless_min_height">56dp</dimen> <!-- Height of a small two-line notification --> <!-- Max height of a collapsed two-line notification --> <!-- 20 * 2 (margins) + 24 * 2 (2 lines) = 88 (notification) --> <dimen name="notification_min_height">88dp</dimen> Loading core/res/res/values/symbols.xml +1 −0 Original line number Diff line number Diff line Loading @@ -5899,6 +5899,7 @@ <java-symbol type="array" name="config_notificationDefaultUnsupportedAdjustments" /> <java-symbol type="drawable" name="ic_notification_summarization" /> <java-symbol type="dimen" name="notification_collapsed_height_with_summarization" /> <!-- Advanced Protection Service USB feature --> <java-symbol type="string" name="usb_apm_usb_plugged_in_when_locked_notification_title" /> Loading packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/ConversationNotificationProcessorTest.kt 0 → 100644 +161 −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 import android.app.Flags import android.app.Notification import android.app.Notification.EXTRA_SUMMARIZED_CONTENT import android.app.Person import android.content.pm.LauncherApps import android.content.pm.launcherApps import android.graphics.drawable.Icon import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.testing.TestableLooper.RunWithLooper import android.text.SpannableStringBuilder import android.text.style.ImageSpan import android.text.style.StyleSpan import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.testScope import com.android.systemui.res.R import com.android.systemui.statusbar.RankingBuilder import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.NotificationRowContentBinderLogger import com.android.systemui.statusbar.notification.row.NotificationTestHelper import com.android.systemui.statusbar.notification.row.notificationRowContentBinderLogger import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) @RunWithLooper class ConversationNotificationProcessorTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private lateinit var conversationNotificationProcessor: ConversationNotificationProcessor private lateinit var testHelper: NotificationTestHelper private lateinit var launcherApps: LauncherApps private lateinit var logger: NotificationRowContentBinderLogger private lateinit var conversationNotificationManager: ConversationNotificationManager @Before fun setup() { launcherApps = kosmos.launcherApps conversationNotificationManager = kosmos.conversationNotificationManager logger = kosmos.notificationRowContentBinderLogger testHelper = NotificationTestHelper(mContext, mDependency) conversationNotificationProcessor = ConversationNotificationProcessor( context, launcherApps, conversationNotificationManager, ) } @Test fun processNotification_notMessagingStyle() { val nb = Notification.Builder(mContext).setSmallIcon(R.drawable.ic_person) val newRow: ExpandableNotificationRow = testHelper.createRow(nb.build()) assertThat(conversationNotificationProcessor.processNotification(newRow.entry, nb, logger)) .isNull() } @Test @DisableFlags(Flags.FLAG_NM_SUMMARIZATION, Flags.FLAG_NM_SUMMARIZATION_UI) fun processNotification_messagingStyleWithSummarization_flagOff() { val summarization = "hello" val nb = getMessagingNotification() val newRow: ExpandableNotificationRow = testHelper.createRow(nb.build()) newRow.entry.setRanking( RankingBuilder(newRow.entry.ranking).setSummarization(summarization).build() ) assertThat(conversationNotificationProcessor.processNotification(newRow.entry, nb, logger)) .isNotNull() assertThat(nb.build().extras.getCharSequence(EXTRA_SUMMARIZED_CONTENT)).isNull() } @Test @EnableFlags(Flags.FLAG_NM_SUMMARIZATION) fun processNotification_messagingStyleWithSummarization() { val summarization = "hello" val nb = getMessagingNotification() val newRow: ExpandableNotificationRow = testHelper.createRow(nb.build()) newRow.entry.setRanking( RankingBuilder(newRow.entry.ranking).setSummarization(summarization).build() ) assertThat(conversationNotificationProcessor.processNotification(newRow.entry, nb, logger)) .isNotNull() val processedSummary = nb.build().extras.getCharSequence(EXTRA_SUMMARIZED_CONTENT) assertThat(processedSummary.toString()).isEqualTo("x$summarization") val checkSpans = SpannableStringBuilder(processedSummary) assertThat( checkSpans.getSpans( /* queryStart = */ 0, /* queryEnd = */ 1, /* kind = */ ImageSpan::class.java, ) ) .isNotNull() assertThat( processedSummary?.let { checkSpans.getSpans( /* queryStart = */ 0, /* queryEnd = */ it.length, /* kind = */ StyleSpan::class.java, ) } ) .isNotNull() } @Test @EnableFlags(Flags.FLAG_NM_SUMMARIZATION) fun processNotification_messagingStyleWithoutSummarization() { val nb = getMessagingNotification() val newRow: ExpandableNotificationRow = testHelper.createRow(nb.build()) assertThat(conversationNotificationProcessor.processNotification(newRow.entry, nb, logger)) .isNotNull() assertThat(nb.build().extras.getCharSequence(EXTRA_SUMMARIZED_CONTENT)).isNull() } private fun getMessagingNotification(): Notification.Builder { val displayName = "Display Name" val messageText = "Message Text" val personIcon = Icon.createWithResource(mContext, R.drawable.ic_person) val testPerson = Person.Builder().setName(displayName).setIcon(personIcon).build() val messagingStyle = Notification.MessagingStyle(testPerson) messagingStyle.addMessage( Notification.MessagingStyle.Message(messageText, System.currentTimeMillis(), testPerson) ) return Notification.Builder(mContext) .setSmallIcon(R.drawable.ic_person) .setStyle(messagingStyle) } } Loading
core/java/com/android/internal/widget/ConversationLayout.java +5 −4 Original line number Diff line number Diff line Loading @@ -399,7 +399,9 @@ public class ConversationLayout extends FrameLayout @RemotableViewMethod(asyncImpl = "setIsCollapsedAsync") public void setIsCollapsed(boolean isCollapsed) { mIsCollapsed = isCollapsed; mMessagingLinearLayout.setMaxDisplayedLines(isCollapsed ? 1 : Integer.MAX_VALUE); mMessagingLinearLayout.setMaxDisplayedLines(isCollapsed ? TextUtils.isEmpty(mSummarizedContent) ? 1 : 2 : Integer.MAX_VALUE); updateExpandButton(); updateContentEndPaddings(); } Loading Loading @@ -448,7 +450,7 @@ public class ConversationLayout extends FrameLayout List<MessagingMessage> newMessagingMessages; mSummarizedContent = extras.getCharSequence(Notification.EXTRA_SUMMARIZED_CONTENT); if (mSummarizedContent != null && mIsCollapsed) { if (!TextUtils.isEmpty(mSummarizedContent) && mIsCollapsed) { Notification.MessagingStyle.Message summary = new Notification.MessagingStyle.Message(mSummarizedContent, 0, ""); newMessagingMessages = createMessages(List.of(summary), false, usePrecomputedText); Loading Loading @@ -1162,7 +1164,7 @@ public class ConversationLayout extends FrameLayout nameOverride = mNameReplacement; } newGroup.setShowingAvatar(!mIsOneToOne && !mIsCollapsed); newGroup.setSingleLine(mIsCollapsed); newGroup.setSingleLine(mIsCollapsed && TextUtils.isEmpty(mSummarizedContent)); newGroup.setSender(sender, nameOverride); newGroup.setSending(groupIndex == (groups.size() - 1) && showSpinner); mGroups.add(newGroup); Loading Loading @@ -1462,7 +1464,6 @@ public class ConversationLayout extends FrameLayout maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); } maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); if (maxHeight != getMeasuredHeight()) { setMeasuredDimension(getMeasuredWidth(), maxHeight); Loading
core/java/com/android/internal/widget/MessagingLayout.java +3 −2 Original line number Diff line number Diff line Loading @@ -198,7 +198,8 @@ public class MessagingLayout extends FrameLayout /* isHistoric= */true, usePrecomputedText); List<MessagingMessage> newMessagingMessages; mSummarizedContent = extras.getCharSequence(Notification.EXTRA_SUMMARIZED_CONTENT); if (mSummarizedContent != null && mIsCollapsed) { if (!TextUtils.isEmpty(mSummarizedContent) && mIsCollapsed) { mMessagingLinearLayout.setMaxDisplayedLines(2); Notification.MessagingStyle.Message summary = new Notification.MessagingStyle.Message(mSummarizedContent, 0, ""); newMessagingMessages = createMessages(List.of(summary), false, usePrecomputedText); Loading Loading @@ -488,7 +489,7 @@ public class MessagingLayout extends FrameLayout if (sender != mUser && mNameReplacement != null) { nameOverride = mNameReplacement; } newGroup.setSingleLine(mIsCollapsed); newGroup.setSingleLine(mIsCollapsed && TextUtils.isEmpty(mSummarizedContent)); newGroup.setShowingAvatar(!mIsCollapsed); newGroup.setSender(sender, nameOverride); newGroup.setSending(groupIndex == (groups.size() - 1) && showSpinner); Loading
core/res/res/values/dimens.xml +6 −3 Original line number Diff line number Diff line Loading @@ -431,15 +431,18 @@ <!-- The minimum height of the notification content (even when there's only one line of text) --> <dimen name="notification_2025_content_min_height">40dp</dimen> <!-- Height of a headerless notification with one or two lines --> <!-- 16 * 2 (margins) + 40 (min content height) = 72 (notification) --> <!-- Max height of a collapsed (headerless) notification with a summarization --> <dimen name="notification_collapsed_height_with_summarization">156dp</dimen> <!-- Max height of a collapsed (headerless) notification with one or two lines --> <!-- 16 * 2 (margins) + 48 (min content height) = 72 (notification) --> <dimen name="notification_2025_min_height">72dp</dimen> <!-- Height of a headerless notification with one line --> <!-- 16 * 2 (margins) + 24 (1 line) = 56 (notification) --> <dimen name="notification_headerless_min_height">56dp</dimen> <!-- Height of a small two-line notification --> <!-- Max height of a collapsed two-line notification --> <!-- 20 * 2 (margins) + 24 * 2 (2 lines) = 88 (notification) --> <dimen name="notification_min_height">88dp</dimen> Loading
core/res/res/values/symbols.xml +1 −0 Original line number Diff line number Diff line Loading @@ -5899,6 +5899,7 @@ <java-symbol type="array" name="config_notificationDefaultUnsupportedAdjustments" /> <java-symbol type="drawable" name="ic_notification_summarization" /> <java-symbol type="dimen" name="notification_collapsed_height_with_summarization" /> <!-- Advanced Protection Service USB feature --> <java-symbol type="string" name="usb_apm_usb_plugged_in_when_locked_notification_title" /> Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/ConversationNotificationProcessorTest.kt 0 → 100644 +161 −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 import android.app.Flags import android.app.Notification import android.app.Notification.EXTRA_SUMMARIZED_CONTENT import android.app.Person import android.content.pm.LauncherApps import android.content.pm.launcherApps import android.graphics.drawable.Icon import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.testing.TestableLooper.RunWithLooper import android.text.SpannableStringBuilder import android.text.style.ImageSpan import android.text.style.StyleSpan import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.testScope import com.android.systemui.res.R import com.android.systemui.statusbar.RankingBuilder import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.NotificationRowContentBinderLogger import com.android.systemui.statusbar.notification.row.NotificationTestHelper import com.android.systemui.statusbar.notification.row.notificationRowContentBinderLogger import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) @RunWithLooper class ConversationNotificationProcessorTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private lateinit var conversationNotificationProcessor: ConversationNotificationProcessor private lateinit var testHelper: NotificationTestHelper private lateinit var launcherApps: LauncherApps private lateinit var logger: NotificationRowContentBinderLogger private lateinit var conversationNotificationManager: ConversationNotificationManager @Before fun setup() { launcherApps = kosmos.launcherApps conversationNotificationManager = kosmos.conversationNotificationManager logger = kosmos.notificationRowContentBinderLogger testHelper = NotificationTestHelper(mContext, mDependency) conversationNotificationProcessor = ConversationNotificationProcessor( context, launcherApps, conversationNotificationManager, ) } @Test fun processNotification_notMessagingStyle() { val nb = Notification.Builder(mContext).setSmallIcon(R.drawable.ic_person) val newRow: ExpandableNotificationRow = testHelper.createRow(nb.build()) assertThat(conversationNotificationProcessor.processNotification(newRow.entry, nb, logger)) .isNull() } @Test @DisableFlags(Flags.FLAG_NM_SUMMARIZATION, Flags.FLAG_NM_SUMMARIZATION_UI) fun processNotification_messagingStyleWithSummarization_flagOff() { val summarization = "hello" val nb = getMessagingNotification() val newRow: ExpandableNotificationRow = testHelper.createRow(nb.build()) newRow.entry.setRanking( RankingBuilder(newRow.entry.ranking).setSummarization(summarization).build() ) assertThat(conversationNotificationProcessor.processNotification(newRow.entry, nb, logger)) .isNotNull() assertThat(nb.build().extras.getCharSequence(EXTRA_SUMMARIZED_CONTENT)).isNull() } @Test @EnableFlags(Flags.FLAG_NM_SUMMARIZATION) fun processNotification_messagingStyleWithSummarization() { val summarization = "hello" val nb = getMessagingNotification() val newRow: ExpandableNotificationRow = testHelper.createRow(nb.build()) newRow.entry.setRanking( RankingBuilder(newRow.entry.ranking).setSummarization(summarization).build() ) assertThat(conversationNotificationProcessor.processNotification(newRow.entry, nb, logger)) .isNotNull() val processedSummary = nb.build().extras.getCharSequence(EXTRA_SUMMARIZED_CONTENT) assertThat(processedSummary.toString()).isEqualTo("x$summarization") val checkSpans = SpannableStringBuilder(processedSummary) assertThat( checkSpans.getSpans( /* queryStart = */ 0, /* queryEnd = */ 1, /* kind = */ ImageSpan::class.java, ) ) .isNotNull() assertThat( processedSummary?.let { checkSpans.getSpans( /* queryStart = */ 0, /* queryEnd = */ it.length, /* kind = */ StyleSpan::class.java, ) } ) .isNotNull() } @Test @EnableFlags(Flags.FLAG_NM_SUMMARIZATION) fun processNotification_messagingStyleWithoutSummarization() { val nb = getMessagingNotification() val newRow: ExpandableNotificationRow = testHelper.createRow(nb.build()) assertThat(conversationNotificationProcessor.processNotification(newRow.entry, nb, logger)) .isNotNull() assertThat(nb.build().extras.getCharSequence(EXTRA_SUMMARIZED_CONTENT)).isNull() } private fun getMessagingNotification(): Notification.Builder { val displayName = "Display Name" val messageText = "Message Text" val personIcon = Icon.createWithResource(mContext, R.drawable.ic_person) val testPerson = Person.Builder().setName(displayName).setIcon(personIcon).build() val messagingStyle = Notification.MessagingStyle(testPerson) messagingStyle.addMessage( Notification.MessagingStyle.Message(messageText, System.currentTimeMillis(), testPerson) ) return Notification.Builder(mContext) .setSmallIcon(R.drawable.ic_person) .setStyle(messagingStyle) } }