summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Jernej Virag <jernej@google.com> 2023-06-19 16:41:34 +0200
committer Jernej Virag <jernej@google.com> 2023-06-20 17:58:27 +0200
commit4e2fb11dab6e7ed996d2a54352bd39c97e621bab (patch)
tree4b2088acad5c0f51a22fec068913671a10cf80ed
parent2c674c03bc3726b2ef9a3bab5811e349b7f28b1e (diff)
Allow setting of LayoutInflater.Factory in RemoteViews
This allows the inflating code to provide a LayoutInflater.Factory to the RemoteViews instance and apply performance and memory optimizations via View replacement. Approach is similar to how androidx libraries apply AppCompat* views when apps inflate their views. Bug: 287960719 Test: newly written unit tests Change-Id: I2c21aee69158ba0a49178ff5384eb3848dbeb99a
-rw-r--r--core/java/android/widget/RemoteViews.java33
-rw-r--r--core/tests/coretests/src/android/widget/RemoteViewsTest.java72
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java4
3 files changed, 108 insertions, 1 deletions
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 3be8c3d6b502..bd7f5a0924cc 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -413,6 +413,13 @@ public class RemoteViews implements Parcelable, Filter {
/** Class cookies of the Parcel this instance was read from. */
private Map<Class, Object> mClassCookies;
+ /**
+ * {@link LayoutInflater.Factory2} which will be passed into a {@link LayoutInflater} instance
+ * used by this class.
+ */
+ @Nullable
+ private LayoutInflater.Factory2 mLayoutInflaterFactory2;
+
private static final InteractionHandler DEFAULT_INTERACTION_HANDLER =
(view, pendingIntent, response) ->
startPendingIntent(view, pendingIntent, response.getLaunchOptions(view));
@@ -432,6 +439,29 @@ public class RemoteViews implements Parcelable, Filter {
}
/**
+ * Sets {@link LayoutInflater.Factory2} to be passed into {@link LayoutInflater} used
+ * by this class instance. It has to be set before the views are inflated to have any effect.
+ *
+ * The factory callbacks will be called on the background thread so the implementation needs
+ * to be thread safe.
+ *
+ * @hide
+ */
+ public void setLayoutInflaterFactory(@Nullable LayoutInflater.Factory2 factory) {
+ mLayoutInflaterFactory2 = factory;
+ }
+
+ /**
+ * Returns currently set {@link LayoutInflater.Factory2}.
+ *
+ * @hide
+ */
+ @Nullable
+ public LayoutInflater.Factory2 getLayoutInflaterFactory() {
+ return mLayoutInflaterFactory2;
+ }
+
+ /**
* Reduces all images and ensures that they are all below the given sizes.
*
* @param maxWidth the maximum width allowed
@@ -5659,6 +5689,9 @@ public class RemoteViews implements Parcelable, Filter {
// we don't add a filter to the static version returned by getSystemService.
inflater = inflater.cloneInContext(inflationContext);
inflater.setFilter(shouldUseStaticFilter() ? INFLATER_FILTER : this);
+ if (mLayoutInflaterFactory2 != null) {
+ inflater.setFactory2(mLayoutInflaterFactory2);
+ }
View v = inflater.inflate(rv.getLayoutId(), parent, false);
if (mViewId != View.NO_ID) {
v.setId(mViewId);
diff --git a/core/tests/coretests/src/android/widget/RemoteViewsTest.java b/core/tests/coretests/src/android/widget/RemoteViewsTest.java
index 73aa93603e56..c4427555078f 100644
--- a/core/tests/coretests/src/android/widget/RemoteViewsTest.java
+++ b/core/tests/coretests/src/android/widget/RemoteViewsTest.java
@@ -22,6 +22,7 @@ import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.spy;
@@ -39,11 +40,15 @@ import android.os.AsyncTask;
import android.os.Binder;
import android.os.Looper;
import android.os.Parcel;
+import android.util.AttributeSet;
import android.util.SizeF;
import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -827,4 +832,71 @@ public class RemoteViewsTest {
verify(visitor, times(1)).accept(eq(icon3S.getUri()));
verify(visitor, times(1)).accept(eq(icon4S.getUri()));
}
+
+ @Test
+ public void layoutInflaterFactory_nothingSet_returnsNull() {
+ final RemoteViews rv = new RemoteViews(mPackage, R.layout.remote_views_test);
+ assertNull(rv.getLayoutInflaterFactory());
+ }
+
+ @Test
+ public void layoutInflaterFactory_replacesImageView_viewReplaced() {
+ final RemoteViews rv = new RemoteViews(mPackage, R.layout.remote_views_test);
+ final View replacement = new FrameLayout(mContext);
+ replacement.setId(1337);
+
+ LayoutInflater.Factory2 factory = createLayoutInflaterFactory("ImageView", replacement);
+ rv.setLayoutInflaterFactory(factory);
+
+ // Now inflate the views.
+ View inflated = rv.apply(mContext, mContainer);
+
+ assertEquals(factory, rv.getLayoutInflaterFactory());
+ View replacedFrameLayout = inflated.findViewById(1337);
+ assertNotNull(replacedFrameLayout);
+ assertEquals(replacement, replacedFrameLayout);
+ // ImageView should be fully replaced.
+ assertNull(inflated.findViewById(R.id.image));
+ }
+
+ @Test
+ public void layoutInflaterFactory_replacesImageView_settersStillFunctional() {
+ final RemoteViews rv = new RemoteViews(mPackage, R.layout.remote_views_test);
+ final TextView replacement = new TextView(mContext);
+ replacement.setId(R.id.text);
+ final String testText = "testText";
+ rv.setLayoutInflaterFactory(createLayoutInflaterFactory("TextView", replacement));
+ rv.setTextViewText(R.id.text, testText);
+
+
+ // Now inflate the views.
+ View inflated = rv.apply(mContext, mContainer);
+
+ TextView replacedTextView = inflated.findViewById(R.id.text);
+ assertSame(replacement, replacedTextView);
+ assertEquals(testText, replacedTextView.getText());
+ }
+
+ private static LayoutInflater.Factory2 createLayoutInflaterFactory(String viewTypeToReplace,
+ View replacementView) {
+ return new LayoutInflater.Factory2() {
+ @Nullable
+ @Override
+ public View onCreateView(@Nullable View parent, @NonNull String name,
+ @NonNull Context context, @NonNull AttributeSet attrs) {
+ if (viewTypeToReplace.equals(name)) {
+ return replacementView;
+ }
+
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull String name, @NonNull Context context,
+ @NonNull AttributeSet attrs) {
+ return null;
+ }
+ };
+ }
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
index a88ab1863671..d32289d5ba6e 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
@@ -34,6 +34,7 @@ import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcel;
import android.util.Log;
+import android.view.LayoutInflater;
import android.view.View;
import android.widget.RemoteViews;
@@ -95,7 +96,8 @@ public class NotificationVisitUrisTest extends UiServiceTestCase {
// Types that we can't really produce. No methods receiving these parameters will be invoked.
private static final ImmutableSet<Class<?>> UNUSABLE_TYPES =
ImmutableSet.of(Consumer.class, IBinder.class, MediaSession.Token.class, Parcel.class,
- PrintWriter.class, Resources.Theme.class, View.class);
+ PrintWriter.class, Resources.Theme.class, View.class,
+ LayoutInflater.Factory2.class);
// Maximum number of times we allow generating the same class recursively.
// E.g. new RemoteViews.addView(new RemoteViews()) but stop there.