Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4e2fb11d authored by Jernej Virag's avatar Jernej Virag
Browse files

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
parent 2c674c03
Loading
Loading
Loading
Loading
+33 −0
Original line number Diff line number Diff line
@@ -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);
+72 −0
Original line number Diff line number Diff line
@@ -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;
            }
        };
    }
}
+3 −1
Original line number Diff line number Diff line
@@ -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.