diff options
| author | 2023-06-19 16:41:34 +0200 | |
|---|---|---|
| committer | 2023-06-20 17:58:27 +0200 | |
| commit | 4e2fb11dab6e7ed996d2a54352bd39c97e621bab (patch) | |
| tree | 4b2088acad5c0f51a22fec068913671a10cf80ed | |
| parent | 2c674c03bc3726b2ef9a3bab5811e349b7f28b1e (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
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. |