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
This commit is contained in:
Jernej Virag
2023-06-19 16:41:34 +02:00
parent 2c674c03bc
commit 4e2fb11dab
3 changed files with 108 additions and 1 deletions

View File

@@ -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));
@@ -431,6 +438,29 @@ public class RemoteViews implements Parcelable, Filter {
mActions.add(new SetRemoteInputsAction(viewId, remoteInputs));
}
/**
* 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.
*
@@ -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);

View File

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

View File

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