diff options
16 files changed, 783 insertions, 24 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorTest.kt new file mode 100644 index 000000000000..6736ccf739ce --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorTest.kt @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2024 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.promoted + +import android.app.Notification +import android.app.Notification.BigPictureStyle +import android.app.Notification.BigTextStyle +import android.app.Notification.CallStyle +import android.app.Notification.MessagingStyle +import android.app.Notification.ProgressStyle +import android.app.Notification.ProgressStyle.Segment +import android.app.PendingIntent +import android.app.Person +import android.content.Intent +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class PromotedNotificationContentExtractorTest : SysuiTestCase() { + private val kosmos = testKosmos() + + private val provider = + FakePromotedNotificationsProvider().also { kosmos.promotedNotificationsProvider = it } + + private val underTest = kosmos.promotedNotificationContentExtractor + + @Test + @DisableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) + fun shouldNotExtract_bothFlagsDisabled() { + val notif = createEntry().also { provider.promotedEntries.add(it) } + val content = extractContent(notif) + assertThat(content).isNull() + } + + @Test + @EnableFlags(PromotedNotificationUi.FLAG_NAME) + @DisableFlags(StatusBarNotifChips.FLAG_NAME) + fun shouldExtract_promotedNotificationUiFlagEnabled() { + val entry = createEntry().also { provider.promotedEntries.add(it) } + val content = extractContent(entry) + assertThat(content).isNotNull() + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + @DisableFlags(PromotedNotificationUi.FLAG_NAME) + fun shouldExtract_statusBarNotifChipsFlagEnabled() { + val entry = createEntry().also { provider.promotedEntries.add(it) } + val content = extractContent(entry) + assertThat(content).isNotNull() + } + + @Test + @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) + fun shouldExtract_bothFlagsEnabled() { + val entry = createEntry().also { provider.promotedEntries.add(it) } + val content = extractContent(entry) + assertThat(content).isNotNull() + } + + @Test + @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) + fun shouldNotExtract_providerDidNotPromote() { + val entry = createEntry().also { provider.promotedEntries.remove(it) } + val content = extractContent(entry) + assertThat(content).isNull() + } + + @Test + @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) + fun extractContent_commonFields() { + val entry = + createEntry { + setSubText(TEST_SUB_TEXT) + setContentTitle(TEST_CONTENT_TITLE) + setContentText(TEST_CONTENT_TEXT) + } + .also { provider.promotedEntries.add(it) } + + val content = extractContent(entry) + + assertThat(content).isNotNull() + assertThat(content?.subText).isEqualTo(TEST_SUB_TEXT) + assertThat(content?.title).isEqualTo(TEST_CONTENT_TITLE) + assertThat(content?.text).isEqualTo(TEST_CONTENT_TEXT) + } + + @Test + @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) + fun extractContent_fromBigPictureStyle() { + val entry = + createEntry { setStyle(BigPictureStyle()) }.also { provider.promotedEntries.add(it) } + + val content = extractContent(entry) + + assertThat(content).isNotNull() + assertThat(content?.style).isEqualTo(Style.BigPicture) + } + + @Test + @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) + fun extractContent_fromBigTextStyle() { + val entry = + createEntry { setStyle(BigTextStyle()) }.also { provider.promotedEntries.add(it) } + + val content = extractContent(entry) + + assertThat(content).isNotNull() + assertThat(content?.style).isEqualTo(Style.BigText) + } + + @Test + @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) + fun extractContent_fromCallStyle() { + val hangUpIntent = + PendingIntent.getBroadcast(context, 0, Intent("hangup"), PendingIntent.FLAG_IMMUTABLE) + + val entry = + createEntry { setStyle(CallStyle.forOngoingCall(TEST_PERSON, hangUpIntent)) } + .also { provider.promotedEntries.add(it) } + + val content = extractContent(entry) + + assertThat(content).isNotNull() + assertThat(content?.style).isEqualTo(Style.Call) + } + + @Test + @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) + fun extractContent_fromProgressStyle() { + val entry = + createEntry { + setStyle(ProgressStyle().addProgressSegment(Segment(100)).setProgress(75)) + } + .also { provider.promotedEntries.add(it) } + + val content = extractContent(entry) + + assertThat(content).isNotNull() + assertThat(content?.style).isEqualTo(Style.Progress) + assertThat(content?.progress).isNotNull() + assertThat(content?.progress?.progress).isEqualTo(75) + assertThat(content?.progress?.progressMax).isEqualTo(100) + } + + @Test + @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) + fun extractContent_fromIneligibleStyle() { + val entry = + createEntry { + setStyle( + MessagingStyle(TEST_PERSON).addMessage("message text", 0L, TEST_PERSON) + ) + } + .also { provider.promotedEntries.add(it) } + + val content = extractContent(entry) + + assertThat(content).isNotNull() + assertThat(content?.style).isEqualTo(Style.Ineligible) + } + + private fun extractContent(entry: NotificationEntry): PromotedNotificationContentModel? { + val recoveredBuilder = Notification.Builder(context, entry.sbn.notification) + return underTest.extractContent(entry, recoveredBuilder) + } + + private fun createEntry(builderBlock: Notification.Builder.() -> Unit = {}): NotificationEntry { + val notif = Notification.Builder(context, "a").also(builderBlock).build() + return NotificationEntryBuilder().setNotification(notif).build() + } + + companion object { + private const val TEST_SUB_TEXT = "sub text" + private const val TEST_CONTENT_TITLE = "content title" + private const val TEST_CONTENT_TEXT = "content text" + + private const val TEST_PERSON_NAME = "person name" + private const val TEST_PERSON_KEY = "person key" + private val TEST_PERSON = + Person.Builder().setKey(TEST_PERSON_KEY).setName(TEST_PERSON_NAME).build() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java index b278f1a48b3d..6eb2764165b5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java @@ -30,6 +30,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -42,6 +43,7 @@ import android.os.CancellationSignal; import android.os.Handler; import android.os.Looper; import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; import android.util.TypedValue; @@ -56,8 +58,12 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.media.controls.util.MediaFeatureFlag; import com.android.systemui.statusbar.NotificationRemoteInputManager; +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; import com.android.systemui.statusbar.notification.ConversationNotificationProcessor; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor; +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi; +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationCallback; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag; @@ -99,6 +105,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase { @Mock private NotifLayoutInflaterFactory.Provider mNotifLayoutInflaterFactoryProvider; @Mock private HeadsUpStyleProvider mHeadsUpStyleProvider; @Mock private NotifLayoutInflaterFactory mNotifLayoutInflaterFactory; + @Mock private PromotedNotificationContentExtractor mPromotedNotificationContentExtractor; private final SmartReplyStateInflater mSmartReplyStateInflater = new SmartReplyStateInflater() { @@ -142,6 +149,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase { mSmartReplyStateInflater, mNotifLayoutInflaterFactoryProvider, mHeadsUpStyleProvider, + mPromotedNotificationContentExtractor, mock(NotificationRowContentBinderLogger.class)); } @@ -382,6 +390,75 @@ public class NotificationContentInflaterTest extends SysuiTestCase { verify(mRow, times(0)).onNotificationUpdated(); } + @Test + @DisableFlags({PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME}) + public void testExtractsPromotedContent_notWhenBothFlagsDisabled() throws Exception { + final PromotedNotificationContentModel content = + new PromotedNotificationContentModel.Builder("key").build(); + when(mPromotedNotificationContentExtractor.extractContent(any(), any())) + .thenReturn(content); + + inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow); + + verify(mPromotedNotificationContentExtractor, never()).extractContent(any(), any()); + } + + @Test + @EnableFlags(PromotedNotificationUi.FLAG_NAME) + @DisableFlags(StatusBarNotifChips.FLAG_NAME) + public void testExtractsPromotedContent_whenPromotedNotificationUiFlagEnabled() + throws Exception { + final PromotedNotificationContentModel content = + new PromotedNotificationContentModel.Builder("key").build(); + when(mPromotedNotificationContentExtractor.extractContent(any(), any())) + .thenReturn(content); + + inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow); + + verify(mPromotedNotificationContentExtractor, times(1)).extractContent(any(), any()); + assertEquals(content, mRow.getEntry().getPromotedNotificationContentModel()); + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + @DisableFlags(PromotedNotificationUi.FLAG_NAME) + public void testExtractsPromotedContent_whenStatusBarNotifChipsFlagEnabled() throws Exception { + final PromotedNotificationContentModel content = + new PromotedNotificationContentModel.Builder("key").build(); + when(mPromotedNotificationContentExtractor.extractContent(any(), any())) + .thenReturn(content); + + inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow); + + verify(mPromotedNotificationContentExtractor, times(1)).extractContent(any(), any()); + assertEquals(content, mRow.getEntry().getPromotedNotificationContentModel()); + } + + @Test + @EnableFlags({PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME}) + public void testExtractsPromotedContent_whenBothFlagsEnabled() throws Exception { + final PromotedNotificationContentModel content = + new PromotedNotificationContentModel.Builder("key").build(); + when(mPromotedNotificationContentExtractor.extractContent(any(), any())) + .thenReturn(content); + + inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow); + + verify(mPromotedNotificationContentExtractor, times(1)).extractContent(any(), any()); + assertEquals(content, mRow.getEntry().getPromotedNotificationContentModel()); + } + + @Test + @EnableFlags({PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME}) + public void testExtractsPromotedContent_null() throws Exception { + when(mPromotedNotificationContentExtractor.extractContent(any(), any())).thenReturn(null); + + inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow); + + verify(mPromotedNotificationContentExtractor, times(1)).extractContent(any(), any()); + assertNull(mRow.getEntry().getPromotedNotificationContentModel()); + } + private static void inflateAndWait(NotificationContentInflater inflater, @InflationFlag int contentToInflate, ExpandableNotificationRow row) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt index 48608ebd6de0..18517998096a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt @@ -21,6 +21,7 @@ import android.content.Context import android.os.AsyncTask import android.os.Build import android.os.CancellationSignal +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.testing.TestableLooper.RunWithLooper import android.util.TypedValue @@ -33,8 +34,12 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.res.R +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.notification.ConversationNotificationProcessor import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED @@ -62,6 +67,7 @@ import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.spy import org.mockito.kotlin.times import org.mockito.kotlin.verify @@ -82,7 +88,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { object : NotifLayoutInflaterFactory.Provider { override fun provide( row: ExpandableNotificationRow, - layoutType: Int + layoutType: Int, ): NotifLayoutInflaterFactory = mock() } private val smartReplyStateInflater: SmartReplyStateInflater = @@ -95,7 +101,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { notifPackageContext: Context, entry: NotificationEntry, existingSmartReplyState: InflatedSmartReplyState?, - newSmartReplyState: InflatedSmartReplyState + newSmartReplyState: InflatedSmartReplyState, ): InflatedSmartReplyViewHolder { return inflatedSmartReplies } @@ -104,6 +110,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { return inflatedSmartReplyState } } + private val promotedNotificationContentExtractor: PromotedNotificationContentExtractor = mock() @Before fun setUp() { @@ -125,7 +132,8 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { smartReplyStateInflater, layoutInflaterFactoryProvider, mock<HeadsUpStyleProvider>(), - mock() + promotedNotificationContentExtractor, + mock(), ) } @@ -142,7 +150,8 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { FLAG_CONTENT_VIEW_ALL, builder, mContext, - smartReplyStateInflater + smartReplyStateInflater, + mock(), ) verify(builder).createHeadsUpContentView(true) } @@ -160,7 +169,8 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { FLAG_CONTENT_VIEW_ALL, builder, mContext, - smartReplyStateInflater + smartReplyStateInflater, + mock(), ) verify(builder).createContentView(true) } @@ -187,7 +197,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { true /* expectingException */, notificationInflater, FLAG_CONTENT_VIEW_ALL, - row + row, ) Assert.assertTrue(row.privateLayout.childCount == 0) verify(row, times(0)).onNotificationUpdated() @@ -210,7 +220,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { FLAG_CONTENT_VIEW_ALL, BindParams(), false /* forceInflate */, - null /* callback */ + null, /* callback */ ) Assert.assertNull(row.entry.runningTask) } @@ -223,7 +233,8 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { NotificationRowContentBinderImpl.InflationProgress( packageContext = mContext, remoteViews = NewRemoteViews(), - contentModel = NotificationContentModel(headsUpStatusBarModel) + contentModel = NotificationContentModel(headsUpStatusBarModel), + extractedPromotedNotificationContentModel = null, ) val countDownLatch = CountDownLatch(1) NotificationRowContentBinderImpl.applyRemoteView( @@ -261,7 +272,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { get() = AsyncFailRemoteView( mContext.packageName, - com.android.systemui.tests.R.layout.custom_view_dark + com.android.systemui.tests.R.layout.custom_view_dark, ) }, logger = mock(), @@ -280,7 +291,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { val decoratedMediaView = builder.createContentView() Assert.assertFalse( "The decorated media style doesn't allow a view to be reapplied!", - NotificationRowContentBinderImpl.canReapplyRemoteView(mediaView, decoratedMediaView) + NotificationRowContentBinderImpl.canReapplyRemoteView(mediaView, decoratedMediaView), ) } @@ -304,7 +315,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { Assert.assertEquals( "Binder inflated a new view even though the old one was cached and usable.", view, - row.privateLayout.contractedChild + row.privateLayout.contractedChild, ) } @@ -327,7 +338,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { Assert.assertNotEquals( "Binder (somehow) used the same view when inflating.", view, - row.privateLayout.contractedChild + row.privateLayout.contractedChild, ) } @@ -396,7 +407,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { private fun getValidationError( measuredHeightDp: Float, targetSdk: Int, - contentView: RemoteViews? + contentView: RemoteViews?, ): String? { val view: View = mock() whenever(view.measuredHeight) @@ -404,7 +415,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { TypedValue.applyDimension( COMPLEX_UNIT_SP, measuredHeightDp, - mContext.resources.displayMetrics + mContext.resources.displayMetrics, ) .toInt() ) @@ -419,7 +430,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { row.entry.sbn.notification.contentView = RemoteViews( mContext.packageName, - com.android.systemui.tests.R.layout.invalid_notification_height + com.android.systemui.tests.R.layout.invalid_notification_height, ) inflateAndWait(true, notificationInflater, FLAG_CONTENT_VIEW_ALL, row) Assert.assertEquals(0, row.privateLayout.childCount.toLong()) @@ -451,7 +462,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { false, notificationInflater, FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE, - messagingRow + messagingRow, ) Assert.assertNotNull(messagingRow.publicLayout.mSingleLineView) // assert this is the conversation layout @@ -460,6 +471,59 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { ) } + @Test + @DisableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) + fun testExtractsPromotedContent_notWhenBothFlagsDisabled() { + val content = PromotedNotificationContentModel.Builder("key").build() + whenever(promotedNotificationContentExtractor.extractContent(any(), any())) + .thenReturn(content) + + inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row) + + verify(promotedNotificationContentExtractor, never()).extractContent(any(), any()) + } + + @Test + @EnableFlags(PromotedNotificationUi.FLAG_NAME) + @DisableFlags(StatusBarNotifChips.FLAG_NAME) + fun testExtractsPromotedContent_whenPromotedNotificationUiFlagEnabled() { + val content = PromotedNotificationContentModel.Builder("key").build() + whenever(promotedNotificationContentExtractor.extractContent(any(), any())) + .thenReturn(content) + + inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row) + + verify(promotedNotificationContentExtractor, times(1)).extractContent(any(), any()) + Assert.assertEquals(content, row.entry.promotedNotificationContentModel) + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + @DisableFlags(PromotedNotificationUi.FLAG_NAME) + fun testExtractsPromotedContent_whenStatusBarNotifChipsFlagEnabled() { + val content = PromotedNotificationContentModel.Builder("key").build() + whenever(promotedNotificationContentExtractor.extractContent(any(), any())) + .thenReturn(content) + + inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row) + + verify(promotedNotificationContentExtractor, times(1)).extractContent(any(), any()) + Assert.assertEquals(content, row.entry.promotedNotificationContentModel) + } + + @Test + @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) + fun testExtractsPromotedContent_whenBothFlagsEnabled() { + val content = PromotedNotificationContentModel.Builder("key").build() + whenever(promotedNotificationContentExtractor.extractContent(any(), any())) + .thenReturn(content) + + inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row) + + verify(promotedNotificationContentExtractor, times(1)).extractContent(any(), any()) + Assert.assertEquals(content, row.entry.promotedNotificationContentModel) + } + private class ExceptionHolder { var exception: Exception? = null } @@ -476,7 +540,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { parent: ViewGroup, executor: Executor, listener: OnViewAppliedListener, - handler: InteractionHandler? + handler: InteractionHandler?, ): CancellationSignal { executor.execute { listener.onError(RuntimeException("Failed to inflate async")) } return CancellationSignal() @@ -486,7 +550,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { context: Context, parent: ViewGroup, executor: Executor, - listener: OnViewAppliedListener + listener: OnViewAppliedListener, ): CancellationSignal { return applyAsync(context, parent, executor, listener, null) } @@ -496,7 +560,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { private fun inflateAndWait( inflater: NotificationRowContentBinderImpl, @InflationFlag contentToInflate: Int, - row: ExpandableNotificationRow + row: ExpandableNotificationRow, ) { inflateAndWait(false /* expectingException */, inflater, contentToInflate, row) } @@ -535,7 +599,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { contentToInflate, BindParams(), false /* forceInflate */, - callback /* callback */ + callback, /* callback */ ) Assert.assertTrue(countDownLatch.await(500, TimeUnit.MILLISECONDS)) exceptionHolder.exception?.let { throw it } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java index 5f492970f0e7..080ac3f8c697 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java @@ -82,6 +82,7 @@ import com.android.systemui.statusbar.notification.collection.render.GroupMember import com.android.systemui.statusbar.notification.icon.IconBuilder; import com.android.systemui.statusbar.notification.icon.IconManager; import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.ExpandableNotificationRowLogger; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.OnExpandClickListener; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag; @@ -204,6 +205,7 @@ public class NotificationTestHelper { new MockSmartReplyInflater(), mock(NotifLayoutInflaterFactory.Provider.class), mock(HeadsUpStyleProvider.class), + mock(PromotedNotificationContentExtractor.class), mock(NotificationRowContentBinderLogger.class)) : new NotificationContentInflater( mock(NotifRemoteViewCache.class), @@ -214,6 +216,7 @@ public class NotificationTestHelper { new MockSmartReplyInflater(), mock(NotifLayoutInflaterFactory.Provider.class), mock(HeadsUpStyleProvider.class), + mock(PromotedNotificationContentExtractor.class), mock(NotificationRowContentBinderLogger.class)); contentBinder.setInflateSynchronously(true); mBindStage = new RowContentBindStage(contentBinder, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index 6d4d40c43e85..6b84b6d07702 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -1076,7 +1076,7 @@ public final class NotificationEntry extends ListEntry { if (PromotedNotificationContentModel.featureFlagEnabled()) { return mPromotedNotificationContentModel; } else { - Log.wtf(TAG, "getting promoted content without feature flag enabled"); + Log.wtf(TAG, "getting promoted content without feature flag enabled", new Throwable()); return null; } } @@ -1090,7 +1090,7 @@ public final class NotificationEntry extends ListEntry { if (PromotedNotificationContentModel.featureFlagEnabled()) { this.mPromotedNotificationContentModel = promotedNotificationContentModel; } else { - Log.wtf(TAG, "setting promoted content without feature flag enabled"); + Log.wtf(TAG, "setting promoted content without feature flag enabled", new Throwable()); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java index a16afe9dcef0..8a1371f1c415 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java @@ -77,6 +77,10 @@ import com.android.systemui.statusbar.notification.interruption.VisualInterrupti import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger; import com.android.systemui.statusbar.notification.logging.NotificationPanelLoggerImpl; import com.android.systemui.statusbar.notification.logging.dagger.NotificationsLogModule; +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor; +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationLogger; +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationsProvider; +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel; import com.android.systemui.statusbar.notification.row.NotificationEntryProcessorFactory; import com.android.systemui.statusbar.notification.row.NotificationEntryProcessorFactoryLooperImpl; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; @@ -101,6 +105,8 @@ import kotlin.coroutines.CoroutineContext; import kotlinx.coroutines.CoroutineScope; +import java.util.Optional; + import javax.inject.Provider; /** @@ -308,4 +314,22 @@ public interface NotificationsModule { @IntoMap @ClassKey(ZenModesCleanupStartable.class) CoreStartable bindsZenModesCleanup(ZenModesCleanupStartable zenModesCleanup); + + /** + * Provides {@link + * com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor} if + * one of the relevant feature flags is enabled. + */ + @Provides + @SysUISingleton + static Optional<PromotedNotificationContentExtractor> + providePromotedNotificationContentExtractor( + PromotedNotificationsProvider provider, Context context, + PromotedNotificationLogger logger) { + if (PromotedNotificationContentModel.featureFlagEnabled()) { + return Optional.of(new PromotedNotificationContentExtractor(provider, context, logger)); + } else { + return Optional.empty(); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt new file mode 100644 index 000000000000..f400d605cb43 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2024 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.promoted + +import android.app.Notification +import android.app.Notification.BigPictureStyle +import android.app.Notification.BigTextStyle +import android.app.Notification.CallStyle +import android.app.Notification.EXTRA_CHRONOMETER_COUNT_DOWN +import android.app.Notification.EXTRA_SUB_TEXT +import android.app.Notification.EXTRA_TEXT +import android.app.Notification.EXTRA_TITLE +import android.app.Notification.ProgressStyle +import android.content.Context +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.When +import javax.inject.Inject + +@SysUISingleton +class PromotedNotificationContentExtractor +@Inject +constructor( + private val promotedNotificationsProvider: PromotedNotificationsProvider, + private val context: Context, + private val logger: PromotedNotificationLogger, +) { + fun extractContent( + entry: NotificationEntry, + recoveredBuilder: Notification.Builder, + ): PromotedNotificationContentModel? { + if (!PromotedNotificationContentModel.featureFlagEnabled()) { + logger.logExtractionSkipped(entry, "feature flags disabled") + return null + } + + if (!promotedNotificationsProvider.shouldPromote(entry)) { + logger.logExtractionSkipped(entry, "shouldPromote returned false") + return null + } + + val notification = entry.sbn.notification + if (notification == null) { + logger.logExtractionFailed(entry, "entry.sbn.notification is null") + return null + } + + val contentBuilder = PromotedNotificationContentModel.Builder(entry.key) + + // TODO: Pitch a fit if style is unsupported or mandatory fields are missing once + // FLAG_PROMOTED_ONGOING is set reliably and we're not testing status bar chips. + + contentBuilder.skeletonSmallIcon = entry.icons.aodIcon?.sourceIcon + contentBuilder.appName = notification.loadHeaderAppName(context) + contentBuilder.subText = notification.subText() + contentBuilder.time = notification.extractWhen() + contentBuilder.lastAudiblyAlertedMs = entry.lastAudiblyAlertedMs + contentBuilder.profileBadgeResId = null // TODO + contentBuilder.title = notification.title() + contentBuilder.text = notification.text() + contentBuilder.skeletonLargeIcon = null // TODO + + recoveredBuilder.style?.extractContent(contentBuilder) + ?: run { contentBuilder.style = Style.Ineligible } + + return contentBuilder.build().also { logger.logExtractionSucceeded(entry, it) } + } +} + +private fun Notification.title(): CharSequence? = extras?.getCharSequence(EXTRA_TITLE) + +private fun Notification.text(): CharSequence? = extras?.getCharSequence(EXTRA_TEXT) + +private fun Notification.subText(): String? = extras?.getString(EXTRA_SUB_TEXT) + +private fun Notification.chronometerCountDown(): Boolean = + extras?.getBoolean(EXTRA_CHRONOMETER_COUNT_DOWN, /* defaultValue= */ false) ?: false + +private fun Notification.extractWhen(): When? { + val time = `when` + val showsTime = showsTime() + val showsChronometer = showsChronometer() + val countDown = chronometerCountDown() + + return when { + showsTime -> When(time, When.Mode.Absolute) + showsChronometer -> When(time, if (countDown) When.Mode.CountDown else When.Mode.CountUp) + else -> null + } +} + +private fun Notification.Style.extractContent( + contentBuilder: PromotedNotificationContentModel.Builder +) { + contentBuilder.style = + when (this) { + is BigPictureStyle -> { + extractContent(contentBuilder) + Style.BigPicture + } + + is BigTextStyle -> { + extractContent(contentBuilder) + Style.BigText + } + + is CallStyle -> { + extractContent(contentBuilder) + Style.Call + } + + is ProgressStyle -> { + extractContent(contentBuilder) + Style.Progress + } + + else -> Style.Ineligible + } +} + +private fun BigPictureStyle.extractContent( + contentBuilder: PromotedNotificationContentModel.Builder +) { + // TODO? +} + +private fun BigTextStyle.extractContent(contentBuilder: PromotedNotificationContentModel.Builder) { + // TODO? +} + +private fun CallStyle.extractContent(contentBuilder: PromotedNotificationContentModel.Builder) { + contentBuilder.personIcon = null // TODO + contentBuilder.personName = null // TODO + contentBuilder.verificationIcon = null // TODO + contentBuilder.verificationText = null // TODO +} + +private fun ProgressStyle.extractContent(contentBuilder: PromotedNotificationContentModel.Builder) { + // TODO: Create NotificationProgressModel.toSkeleton, or something similar. + contentBuilder.progress = createProgressModel(0xffffffff.toInt(), 0x00000000) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt new file mode 100644 index 000000000000..13ad1413e89d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024 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.promoted + +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.LogLevel.ERROR +import com.android.systemui.log.core.LogLevel.INFO +import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.logKey +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import javax.inject.Inject + +class PromotedNotificationLogger +@Inject +constructor(@NotificationLog private val buffer: LogBuffer) { + fun logExtractionSkipped(entry: NotificationEntry, reason: String) { + buffer.log( + EXTRACTION_TAG, + INFO, + { + str1 = entry.logKey + str2 = reason + }, + { "extraction skipped: $str2 for $str1" }, + ) + } + + fun logExtractionFailed(entry: NotificationEntry, reason: String) { + buffer.log( + EXTRACTION_TAG, + ERROR, + { + str1 = entry.logKey + str2 = reason + }, + { "extraction failed: $str2 for $str1" }, + ) + } + + fun logExtractionSucceeded( + entry: NotificationEntry, + content: PromotedNotificationContentModel, + ) { + buffer.log( + EXTRACTION_TAG, + INFO, + { + str1 = entry.logKey + str2 = content.toString() + }, + { "extraction succeeded: $str2 for $str1" }, + ) + } +} + +private const val EXTRACTION_TAG = "PromotedNotificationContentExtractor" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationsProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationsProvider.kt index 691dc6f5ccac..947d9e3e9547 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationsProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationsProvider.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.promoted import android.app.Notification.FLAG_PROMOTED_ONGOING import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import javax.inject.Inject /** A provider for making decisions on which notifications should be promoted. */ @@ -30,7 +31,7 @@ interface PromotedNotificationsProvider { @SysUISingleton open class PromotedNotificationsProviderImpl @Inject constructor() : PromotedNotificationsProvider { override fun shouldPromote(entry: NotificationEntry): Boolean { - if (!PromotedNotificationUi.isEnabled) { + if (!PromotedNotificationContentModel.featureFlagEnabled()) { return false } return (entry.sbn.notification.flags and FLAG_PROMOTED_ONGOING) != 0 diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index 41abac1d47f7..6e05e8e8b80e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -54,6 +54,8 @@ import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.notification.ConversationNotificationProcessor; import com.android.systemui.statusbar.notification.InflationException; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor; +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel; import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation; import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedaction; @@ -92,6 +94,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder private final SmartReplyStateInflater mSmartReplyStateInflater; private final NotifLayoutInflaterFactory.Provider mNotifLayoutInflaterFactoryProvider; private final HeadsUpStyleProvider mHeadsUpStyleProvider; + private final PromotedNotificationContentExtractor mPromotedNotificationContentExtractor; private final NotificationRowContentBinderLogger mLogger; @@ -105,6 +108,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder SmartReplyStateInflater smartRepliesInflater, NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider, HeadsUpStyleProvider headsUpStyleProvider, + PromotedNotificationContentExtractor promotedNotificationContentExtractor, NotificationRowContentBinderLogger logger) { NotificationRowContentBinderRefactor.assertInLegacyMode(); mRemoteViewCache = remoteViewCache; @@ -115,6 +119,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder mSmartReplyStateInflater = smartRepliesInflater; mNotifLayoutInflaterFactoryProvider = notifLayoutInflaterFactoryProvider; mHeadsUpStyleProvider = headsUpStyleProvider; + mPromotedNotificationContentExtractor = promotedNotificationContentExtractor; mLogger = logger; } @@ -165,6 +170,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder mSmartReplyStateInflater, mNotifLayoutInflaterFactoryProvider, mHeadsUpStyleProvider, + mPromotedNotificationContentExtractor, mLogger); if (mInflateSynchronously) { task.onPostExecute(task.doInBackground()); @@ -913,6 +919,11 @@ public class NotificationContentInflater implements NotificationRowContentBinder NotificationContentView privateLayout = row.getPrivateLayout(); NotificationContentView publicLayout = row.getPublicLayout(); logger.logAsyncTaskProgress(entry, "finishing"); + + if (PromotedNotificationContentModel.featureFlagEnabled()) { + entry.setPromotedNotificationContentModel(result.mExtractedPromotedNotificationContent); + } + boolean setRepliesAndActions = true; if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) { if (result.inflatedContentView != null) { @@ -1123,6 +1134,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder private final SmartReplyStateInflater mSmartRepliesInflater; private final NotifLayoutInflaterFactory.Provider mNotifLayoutInflaterFactoryProvider; private final HeadsUpStyleProvider mHeadsUpStyleProvider; + private final PromotedNotificationContentExtractor mPromotedNotificationContentExtractor; private final NotificationRowContentBinderLogger mLogger; private AsyncInflationTask( @@ -1142,6 +1154,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder SmartReplyStateInflater smartRepliesInflater, NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider, HeadsUpStyleProvider headsUpStyleProvider, + PromotedNotificationContentExtractor promotedNotificationContentExtractor, NotificationRowContentBinderLogger logger) { mEntry = entry; mRow = row; @@ -1160,6 +1173,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder mIsMediaInQS = isMediaFlagEnabled; mNotifLayoutInflaterFactoryProvider = notifLayoutInflaterFactoryProvider; mHeadsUpStyleProvider = headsUpStyleProvider; + mPromotedNotificationContentExtractor = promotedNotificationContentExtractor; mLogger = logger; entry.setInflationTask(this); } @@ -1276,6 +1290,14 @@ public class NotificationContentInflater implements NotificationRowContentBinder ); } + if (PromotedNotificationContentModel.featureFlagEnabled()) { + mLogger.logAsyncTaskProgress(mEntry, "extracting promoted notification content"); + result.mExtractedPromotedNotificationContent = mPromotedNotificationContentExtractor + .extractContent(mEntry, recoveredBuilder); + mLogger.logAsyncTaskProgress(mEntry, "extracted promoted notification content: " + + result.mExtractedPromotedNotificationContent); + } + mLogger.logAsyncTaskProgress(mEntry, "getting row image resolver (on wrong thread!)"); final NotificationInlineImageResolver imageResolver = mRow.getImageResolver(); @@ -1377,6 +1399,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder @VisibleForTesting static class InflationProgress { + PromotedNotificationContentModel mExtractedPromotedNotificationContent; + private RemoteViews newContentView; private RemoteViews newHeadsUpView; private RemoteViews newExpandedView; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt index d0c033bb10b0..c7d80e9d03ce 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.row import android.annotation.SuppressLint +import android.app.Flags import android.app.Notification import android.content.Context import android.content.ContextWrapper @@ -46,6 +47,8 @@ import com.android.systemui.statusbar.NotificationRemoteInputManager import com.android.systemui.statusbar.notification.ConversationNotificationProcessor import com.android.systemui.statusbar.notification.InflationException import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP @@ -95,6 +98,7 @@ constructor( private val smartReplyStateInflater: SmartReplyStateInflater, private val notifLayoutInflaterFactoryProvider: NotifLayoutInflaterFactory.Provider, private val headsUpStyleProvider: HeadsUpStyleProvider, + private val promotedNotificationContentExtractor: PromotedNotificationContentExtractor, private val logger: NotificationRowContentBinderLogger, ) : NotificationRowContentBinder { @@ -147,6 +151,7 @@ constructor( /* isMediaFlagEnabled = */ smartReplyStateInflater, notifLayoutInflaterFactoryProvider, headsUpStyleProvider, + promotedNotificationContentExtractor, logger, ) if (inflateSynchronously) { @@ -166,6 +171,7 @@ constructor( builder: Notification.Builder, packageContext: Context, smartRepliesInflater: SmartReplyStateInflater, + promotedNotificationContentExtractor: PromotedNotificationContentExtractor, ): InflationProgress { val systemUIContext = row.context val result = @@ -182,6 +188,7 @@ constructor( notifLayoutInflaterFactoryProvider = notifLayoutInflaterFactoryProvider, headsUpStyleProvider = headsUpStyleProvider, conversationProcessor = conversationProcessor, + promotedNotificationContentExtractor = promotedNotificationContentExtractor, logger = logger, ) inflateSmartReplyViews( @@ -372,6 +379,7 @@ constructor( private val smartRepliesInflater: SmartReplyStateInflater, private val notifLayoutInflaterFactoryProvider: NotifLayoutInflaterFactory.Provider, private val headsUpStyleProvider: HeadsUpStyleProvider, + private val promotedNotificationContentExtractor: PromotedNotificationContentExtractor, private val logger: NotificationRowContentBinderLogger, ) : AsyncTask<Void, Void, Result<InflationProgress>>(), InflationCallback, InflationTask { private val context: Context @@ -442,6 +450,7 @@ constructor( notifLayoutInflaterFactoryProvider = notifLayoutInflaterFactoryProvider, headsUpStyleProvider = headsUpStyleProvider, conversationProcessor = conversationProcessor, + promotedNotificationContentExtractor = promotedNotificationContentExtractor, logger = logger, ) logger.logAsyncTaskProgress( @@ -582,6 +591,7 @@ constructor( @VisibleForTesting val packageContext: Context, val remoteViews: NewRemoteViews, val contentModel: NotificationContentModel, + val extractedPromotedNotificationContentModel: PromotedNotificationContentModel?, ) { var inflatedContentView: View? = null @@ -670,8 +680,23 @@ constructor( notifLayoutInflaterFactoryProvider: NotifLayoutInflaterFactory.Provider, headsUpStyleProvider: HeadsUpStyleProvider, conversationProcessor: ConversationNotificationProcessor, + promotedNotificationContentExtractor: PromotedNotificationContentExtractor, logger: NotificationRowContentBinderLogger, ): InflationProgress { + val promoted = + if (PromotedNotificationContentModel.featureFlagEnabled()) { + logger.logAsyncTaskProgress(entry, "extracting promoted notification content") + val extracted = + promotedNotificationContentExtractor.extractContent(entry, builder) + logger.logAsyncTaskProgress( + entry, + "extracted promoted notification content: {extracted}", + ) + extracted + } else { + null + } + // process conversations and extract the messaging style val messagingStyle = if (entry.ranking.isConversation) { @@ -734,6 +759,7 @@ constructor( packageContext = packageContext, remoteViews = remoteViews, contentModel = contentModel, + extractedPromotedNotificationContentModel = promoted, ) } @@ -1393,6 +1419,11 @@ constructor( logger.logAsyncTaskProgress(entry, "finishing") entry.setContentModel(result.contentModel) + if (PromotedNotificationContentModel.featureFlagEnabled()) { + entry.promotedNotificationContentModel = + result.extractedPromotedNotificationContentModel + } + result.inflatedSmartReplyState?.let { row.privateLayout.setInflatedSmartReplyState(it) } setContentViewsFromRemoteViews( diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationsProvider.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationsProvider.kt new file mode 100644 index 000000000000..88caf6e2ba30 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationsProvider.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 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.promoted + +import com.android.systemui.statusbar.notification.collection.NotificationEntry + +class FakePromotedNotificationsProvider : PromotedNotificationsProvider { + val promotedEntries = mutableSetOf<NotificationEntry>() + + override fun shouldPromote(entry: NotificationEntry): Boolean { + return promotedEntries.contains(entry) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt new file mode 100644 index 000000000000..5e9f12b4b1cc --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 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.promoted + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos + +var Kosmos.promotedNotificationContentExtractor by + Kosmos.Fixture { + PromotedNotificationContentExtractor( + promotedNotificationsProvider, + applicationContext, + promotedNotificationLogger, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLoggerKosmos.kt new file mode 100644 index 000000000000..2805d1e02356 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLoggerKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 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.promoted + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.log.logcatLogBuffer + +val Kosmos.promotedNotificationLogger by + Kosmos.Fixture { PromotedNotificationLogger(logcatLogBuffer("PromotedNotifLog")) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationsProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationsProviderKosmos.kt index a7aa0b41a7aa..580f617a3cdf 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationsProviderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationsProviderKosmos.kt @@ -18,4 +18,5 @@ package com.android.systemui.statusbar.notification.promoted import com.android.systemui.kosmos.Kosmos -val Kosmos.promotedNotificationsProvider by Kosmos.Fixture { PromotedNotificationsProviderImpl() } +var Kosmos.promotedNotificationsProvider: PromotedNotificationsProvider by + Kosmos.Fixture { PromotedNotificationsProviderImpl() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt index 6f24f2a00556..2d3f68faf4b7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt @@ -36,6 +36,7 @@ import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.log.logcatLogBuffer import com.android.systemui.media.controls.util.MediaFeatureFlag import com.android.systemui.media.dialog.MediaOutputDialogManager import com.android.systemui.plugins.ActivityStarter @@ -64,6 +65,9 @@ import com.android.systemui.statusbar.notification.headsup.HeadsUpManager import com.android.systemui.statusbar.notification.icon.IconBuilder import com.android.systemui.statusbar.notification.icon.IconManager import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationLogger +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationsProviderImpl import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.CoordinateOnClickListener import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.ExpandableNotificationRowLogger import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.OnExpandClickListener @@ -221,6 +225,17 @@ class ExpandableNotificationRowBuilder( Mockito.mock(ConversationNotificationManager::class.java, STUB_ONLY), ) + val promotedNotificationsProvider = PromotedNotificationsProviderImpl() + val promotedNotificationLog = logcatLogBuffer("PromotedNotifLog") + val promotedNotificationLogger = PromotedNotificationLogger(promotedNotificationLog) + + val promotedNotificationContentExtractor = + PromotedNotificationContentExtractor( + promotedNotificationsProvider, + context, + promotedNotificationLogger, + ) + mContentBinder = if (NotificationRowContentBinderRefactor.isEnabled) NotificationRowContentBinderImpl( @@ -231,6 +246,7 @@ class ExpandableNotificationRowBuilder( smartReplyStateInflater, notifLayoutInflaterFactoryProvider, Mockito.mock(HeadsUpStyleProvider::class.java, STUB_ONLY), + promotedNotificationContentExtractor, Mockito.mock(NotificationRowContentBinderLogger::class.java, STUB_ONLY), ) else @@ -243,6 +259,7 @@ class ExpandableNotificationRowBuilder( smartReplyStateInflater, notifLayoutInflaterFactoryProvider, Mockito.mock(HeadsUpStyleProvider::class.java, STUB_ONLY), + promotedNotificationContentExtractor, Mockito.mock(NotificationRowContentBinderLogger::class.java, STUB_ONLY), ) mContentBinder.setInflateSynchronously(true) |