diff options
7 files changed, 316 insertions, 13 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifLayoutInflaterFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifLayoutInflaterFactory.kt new file mode 100644 index 000000000000..4429939a515c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifLayoutInflaterFactory.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row + +import android.content.Context +import android.util.AttributeSet +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import com.android.systemui.Dumpable +import com.android.systemui.dump.DumpManager +import com.android.systemui.statusbar.notification.row.NotificationRowModule.NOTIF_REMOTEVIEWS_FACTORIES +import com.android.systemui.util.asIndenting +import com.android.systemui.util.withIncreasedIndent +import java.io.PrintWriter +import javax.inject.Inject +import javax.inject.Named + +/** + * Implementation of [NotifLayoutInflaterFactory]. This class uses a set of + * [NotifRemoteViewsFactory] objects to create replacement views for Notification RemoteViews. + */ +open class NotifLayoutInflaterFactory +@Inject +constructor( + dumpManager: DumpManager, + @Named(NOTIF_REMOTEVIEWS_FACTORIES) + private val remoteViewsFactories: Set<@JvmSuppressWildcards NotifRemoteViewsFactory> +) : LayoutInflater.Factory2, Dumpable { + init { + dumpManager.registerNormalDumpable(TAG, this) + } + + override fun onCreateView( + parent: View?, + name: String, + context: Context, + attrs: AttributeSet + ): View? { + var view: View? = null + var handledFactory: NotifRemoteViewsFactory? = null + for (layoutFactory in remoteViewsFactories) { + view = layoutFactory.instantiate(parent, name, context, attrs) + if (view != null) { + check(handledFactory == null) { + "${layoutFactory.javaClass.name} tries to produce view. However, " + + "${handledFactory?.javaClass?.name} produced view for $name before." + } + handledFactory = layoutFactory + } + } + logOnCreateView(name, view, handledFactory) + return view + } + + override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? = + onCreateView(null, name, context, attrs) + + override fun dump(pw: PrintWriter, args: Array<out String>) { + val indentingPW = pw.asIndenting() + + indentingPW.appendLine("$TAG ReplacementFactories:") + indentingPW.withIncreasedIndent { + remoteViewsFactories.forEach { indentingPW.appendLine(it.javaClass.simpleName) } + } + } + + private fun logOnCreateView( + name: String, + replacedView: View?, + factory: NotifRemoteViewsFactory? + ) { + if (SPEW && replacedView != null && factory != null) { + Log.d(TAG, "$factory produced view for $name: $replacedView") + } + } + + private companion object { + private const val TAG = "NotifLayoutInflaterFac" + private val SPEW = Log.isLoggable(TAG, Log.VERBOSE) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifRemoteViewsFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifRemoteViewsFactory.kt new file mode 100644 index 000000000000..eebd4d4e955f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifRemoteViewsFactory.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row + +import android.content.Context +import android.util.AttributeSet +import android.view.View + +/** Interface used to create replacement view instances in Notification RemoteViews. */ +interface NotifRemoteViewsFactory { + + /** return the replacement view instance for the given view name */ + fun instantiate(parent: View?, name: String, context: Context, attrs: AttributeSet): View? +} 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 13d1978ec8ff..0ad77bdd866b 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 @@ -79,6 +79,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder private final ConversationNotificationProcessor mConversationProcessor; private final Executor mBgExecutor; private final SmartReplyStateInflater mSmartReplyStateInflater; + private final NotifLayoutInflaterFactory mNotifLayoutInflaterFactory; @Inject NotificationContentInflater( @@ -87,13 +88,15 @@ public class NotificationContentInflater implements NotificationRowContentBinder ConversationNotificationProcessor conversationProcessor, MediaFeatureFlag mediaFeatureFlag, @Background Executor bgExecutor, - SmartReplyStateInflater smartRepliesInflater) { + SmartReplyStateInflater smartRepliesInflater, + NotifLayoutInflaterFactory notifLayoutInflaterFactory) { mRemoteViewCache = remoteViewCache; mRemoteInputManager = remoteInputManager; mConversationProcessor = conversationProcessor; mIsMediaInQS = mediaFeatureFlag.getEnabled(); mBgExecutor = bgExecutor; mSmartReplyStateInflater = smartRepliesInflater; + mNotifLayoutInflaterFactory = notifLayoutInflaterFactory; } @Override @@ -137,7 +140,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder callback, mRemoteInputManager.getRemoteViewsOnClickHandler(), mIsMediaInQS, - mSmartReplyStateInflater); + mSmartReplyStateInflater, + mNotifLayoutInflaterFactory); if (mInflateSynchronously) { task.onPostExecute(task.doInBackground()); } else { @@ -160,7 +164,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder bindParams.isLowPriority, bindParams.usesIncreasedHeight, bindParams.usesIncreasedHeadsUpHeight, - packageContext); + packageContext, + mNotifLayoutInflaterFactory); result = inflateSmartReplyViews(result, reInflateFlags, entry, row.getContext(), packageContext, @@ -298,7 +303,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder private static InflationProgress createRemoteViews(@InflationFlag int reInflateFlags, Notification.Builder builder, boolean isLowPriority, boolean usesIncreasedHeight, - boolean usesIncreasedHeadsUpHeight, Context packageContext) { + boolean usesIncreasedHeadsUpHeight, Context packageContext, + NotifLayoutInflaterFactory notifLayoutInflaterFactory) { InflationProgress result = new InflationProgress(); if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) { @@ -316,7 +322,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) { result.newPublicView = builder.makePublicContentView(isLowPriority); } - + setNotifsViewsInflaterFactory(result, notifLayoutInflaterFactory); result.packageContext = packageContext; result.headsUpStatusBarText = builder.getHeadsUpStatusBarText(false /* showingPublic */); result.headsUpStatusBarTextPublic = builder.getHeadsUpStatusBarText( @@ -324,6 +330,22 @@ public class NotificationContentInflater implements NotificationRowContentBinder return result; } + private static void setNotifsViewsInflaterFactory(InflationProgress result, + NotifLayoutInflaterFactory notifLayoutInflaterFactory) { + setRemoteViewsInflaterFactory(result.newContentView, notifLayoutInflaterFactory); + setRemoteViewsInflaterFactory(result.newExpandedView, + notifLayoutInflaterFactory); + setRemoteViewsInflaterFactory(result.newHeadsUpView, notifLayoutInflaterFactory); + setRemoteViewsInflaterFactory(result.newPublicView, notifLayoutInflaterFactory); + } + + private static void setRemoteViewsInflaterFactory(RemoteViews remoteViews, + NotifLayoutInflaterFactory notifLayoutInflaterFactory) { + if (remoteViews != null) { + remoteViews.setLayoutInflaterFactory(notifLayoutInflaterFactory); + } + } + private static CancellationSignal apply( Executor bgExecutor, boolean inflateSynchronously, @@ -348,7 +370,6 @@ public class NotificationContentInflater implements NotificationRowContentBinder public void setResultView(View v) { result.inflatedContentView = v; } - @Override public RemoteViews getRemoteView() { return result.newContentView; @@ -356,7 +377,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder }; applyRemoteView(bgExecutor, inflateSynchronously, result, reInflateFlags, flag, remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback, - privateLayout, privateLayout.getContractedChild(), + privateLayout, privateLayout.getContractedChild(), privateLayout.getVisibleWrapper( NotificationContentView.VISIBLE_TYPE_CONTRACTED), runningInflations, applyCallback); @@ -758,8 +779,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder * @param oldView The old view that was applied to the existing view before * @return {@code true} if the RemoteViews are the same and the view can be reused to reapply. */ - @VisibleForTesting - static boolean canReapplyRemoteView(final RemoteViews newView, + @VisibleForTesting + static boolean canReapplyRemoteView(final RemoteViews newView, final RemoteViews oldView) { return (newView == null && oldView == null) || (newView != null && oldView != null @@ -800,6 +821,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder private final ConversationNotificationProcessor mConversationProcessor; private final boolean mIsMediaInQS; private final SmartReplyStateInflater mSmartRepliesInflater; + private final NotifLayoutInflaterFactory mNotifLayoutInflaterFactory; private AsyncInflationTask( Executor bgExecutor, @@ -815,7 +837,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder InflationCallback callback, RemoteViews.InteractionHandler remoteViewClickHandler, boolean isMediaFlagEnabled, - SmartReplyStateInflater smartRepliesInflater) { + SmartReplyStateInflater smartRepliesInflater, + NotifLayoutInflaterFactory notifLayoutInflaterFactory) { mEntry = entry; mRow = row; mBgExecutor = bgExecutor; @@ -831,6 +854,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder mCallback = callback; mConversationProcessor = conversationProcessor; mIsMediaInQS = isMediaFlagEnabled; + mNotifLayoutInflaterFactory = notifLayoutInflaterFactory; entry.setInflationTask(this); } @@ -874,7 +898,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder } InflationProgress inflationProgress = createRemoteViews(mReInflateFlags, recoveredBuilder, mIsLowPriority, mUsesIncreasedHeight, - mUsesIncreasedHeadsUpHeight, packageContext); + mUsesIncreasedHeadsUpHeight, packageContext, + mNotifLayoutInflaterFactory); InflatedSmartReplyState previousSmartReplyState = mRow.getExistingSmartReplyState(); InflationProgress result = inflateSmartReplyViews( inflationProgress, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java index 111b5756b6e8..b2a3780c1024 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java @@ -17,15 +17,26 @@ package com.android.systemui.statusbar.notification.row; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.flags.FeatureFlags; import dagger.Binds; import dagger.Module; +import dagger.Provides; +import dagger.multibindings.ElementsIntoSet; + +import java.util.HashSet; +import java.util.Set; + +import javax.inject.Named; /** * Dagger Module containing notification row and view inflation implementations. */ @Module public abstract class NotificationRowModule { + public static final String NOTIF_REMOTEVIEWS_FACTORIES = + "notif_remoteviews_factories"; + /** * Provides notification row content binder instance. */ @@ -41,4 +52,15 @@ public abstract class NotificationRowModule { @SysUISingleton public abstract NotifRemoteViewCache provideNotifRemoteViewCache( NotifRemoteViewCacheImpl cacheImpl); + + /** Provides view factories to be inflated in notification content. */ + @Provides + @ElementsIntoSet + @Named(NOTIF_REMOTEVIEWS_FACTORIES) + static Set<NotifRemoteViewsFactory> provideNotifRemoteViewsFactories( + FeatureFlags featureFlags + ) { + final Set<NotifRemoteViewsFactory> replacementFactories = new HashSet<>(); + return replacementFactories; + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotifLayoutInflaterFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotifLayoutInflaterFactoryTest.kt new file mode 100644 index 000000000000..d5612e8e8007 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotifLayoutInflaterFactoryTest.kt @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.statusbar.notification.row + +import android.content.Context +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import android.widget.LinearLayout +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.DumpManager +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertNotNull +import junit.framework.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +/** Tests for [NotifLayoutInflaterFactory] */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +class NotifLayoutInflaterFactoryTest : SysuiTestCase() { + + @Mock private lateinit var attrs: AttributeSet + + @Before + fun before() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun onCreateView_notMatchingViews_returnNull() { + // GIVEN + val layoutInflaterFactory = + createNotifLayoutInflaterFactoryImpl( + setOf( + createReplacementViewFactory("TextView") { context, attrs -> + FrameLayout(context) + } + ) + ) + + // WHEN + val createView = layoutInflaterFactory.onCreateView("ImageView", mContext, attrs) + + // THEN + assertNull(createView) + } + + @Test + fun onCreateView_matchingViews_returnReplacementView() { + // GIVEN + val layoutInflaterFactory = + createNotifLayoutInflaterFactoryImpl( + setOf( + createReplacementViewFactory("TextView") { context, attrs -> + FrameLayout(context) + } + ) + ) + + // WHEN + val createView = layoutInflaterFactory.onCreateView("TextView", mContext, attrs) + + // THEN + assertNotNull(createView) + assertEquals(requireNotNull(createView)::class.java, FrameLayout::class.java) + } + + @Test(expected = IllegalStateException::class) + fun onCreateView_multipleFactory_throwIllegalStateException() { + // GIVEN + val layoutInflaterFactory = + createNotifLayoutInflaterFactoryImpl( + setOf( + createReplacementViewFactory("TextView") { context, attrs -> + FrameLayout(context) + }, + createReplacementViewFactory("TextView") { context, attrs -> + LinearLayout(context) + } + ) + ) + + // WHEN + layoutInflaterFactory.onCreateView("TextView", mContext, attrs) + } + + private fun createNotifLayoutInflaterFactoryImpl( + replacementViewFactories: Set<@JvmSuppressWildcards NotifRemoteViewsFactory> + ) = NotifLayoutInflaterFactory(DumpManager(), replacementViewFactories) + + private fun createReplacementViewFactory( + replacementName: String, + createView: (context: Context, attrs: AttributeSet) -> View + ) = + object : NotifRemoteViewsFactory { + override fun instantiate( + parent: View?, + name: String, + context: Context, + attrs: AttributeSet + ): View? = + if (replacementName == name) { + createView(context, attrs) + } else { + null + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java index 3face350526a..f55b0a8ff4da 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java @@ -92,6 +92,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase { @Mock private ConversationNotificationProcessor mConversationNotificationProcessor; @Mock private InflatedSmartReplyState mInflatedSmartReplyState; @Mock private InflatedSmartReplyViewHolder mInflatedSmartReplies; + @Mock private NotifLayoutInflaterFactory mNotifLayoutInflaterFactory; private final SmartReplyStateInflater mSmartReplyStateInflater = new SmartReplyStateInflater() { @@ -130,7 +131,8 @@ public class NotificationContentInflaterTest extends SysuiTestCase { mConversationNotificationProcessor, mock(MediaFeatureFlag.class), mock(Executor.class), - mSmartReplyStateInflater); + mSmartReplyStateInflater, + mNotifLayoutInflaterFactory); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java index df470717faeb..1a644d3540b0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java @@ -158,7 +158,8 @@ public class NotificationTestHelper { mock(ConversationNotificationProcessor.class), mock(MediaFeatureFlag.class), mock(Executor.class), - new MockSmartReplyInflater()); + new MockSmartReplyInflater(), + mock(NotifLayoutInflaterFactory.class)); contentBinder.setInflateSynchronously(true); mBindStage = new RowContentBindStage(contentBinder, mock(NotifInflationErrorManager.class), |