Merge "Set LayoutInflatorFactory to Notification RemoteViews" into udc-qpr-dev

This commit is contained in:
Ibrahim Yilmaz
2023-07-10 16:50:49 +00:00
committed by Android (Google) Code Review
7 changed files with 316 additions and 13 deletions

View File

@@ -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)
}
}

View File

@@ -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?
}

View File

@@ -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,

View File

@@ -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;
}
}

View File

@@ -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
}
}
}

View File

@@ -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

View File

@@ -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),