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

Commit ae65e9fb authored by Diego Perez's avatar Diego Perez Committed by Android (Google) Code Review
Browse files

Merge "Add support for appcompat preferences rendering"

parents e775cd26 8e8071ba
Loading
Loading
Loading
Loading
+10 −1
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;

import android.content.Context;
import android.util.AttributeSet;
import android.view.InflateException;

public class BridgePreferenceInflater extends PreferenceInflater {

@@ -42,7 +43,15 @@ public class BridgePreferenceInflater extends PreferenceInflater {
            viewKey = ((BridgeXmlBlockParser) attrs).getViewCookie();
        }

        Preference preference = super.onCreateItem(name, attrs);
        Preference preference = null;
        try {
            preference = super.onCreateItem(name, attrs);
        } catch (ClassNotFoundException | InflateException exception) {
            // name is probably not a valid preference type
            if ("SwitchPreferenceCompat".equals(name)) {
                preference = super.onCreateItem("SwitchPreference", attrs);
            }
        }

        if (viewKey != null && bc != null) {
            bc.addCookie(preference, viewKey);
+2 −19
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import com.android.ide.common.rendering.api.LayoutlibCallback;
import com.android.layoutlib.bridge.Bridge;
import com.android.layoutlib.bridge.android.BridgeContext;
import com.android.layoutlib.bridge.android.RenderParamsFlags;
import com.android.layoutlib.bridge.util.ReflectionUtils;
import com.android.layoutlib.bridge.util.ReflectionUtils.ReflectionException;

import android.annotation.NonNull;
@@ -116,7 +117,7 @@ public class RecyclerViewUtil {
    private static void setProperty(@NonNull Object object, @NonNull String propertyClassName,
      @NonNull Object propertyValue, @NonNull String propertySetter)
            throws ReflectionException {
        Class<?> propertyClass = getClassInstance(propertyValue, propertyClassName);
        Class<?> propertyClass = ReflectionUtils.getClassInstance(propertyValue, propertyClassName);
        setProperty(object, propertyClass, propertyValue, propertySetter);
    }

@@ -126,22 +127,4 @@ public class RecyclerViewUtil {
        invoke(getMethod(object.getClass(), propertySetter, propertyClass), object, propertyValue);
    }

    /**
     * Looks through the class hierarchy of {@code object} at runtime and returns the class matching
     * the name {@code className}.
     * <p/>
     * This is used when we cannot use Class.forName() since the class we want was loaded from a
     * different ClassLoader.
     */
    @NonNull
    private static Class<?> getClassInstance(@NonNull Object object, @NonNull String className) {
        Class<?> superClass = object.getClass();
        while (superClass != null) {
            if (className.equals(superClass.getName())) {
                return superClass;
            }
            superClass = superClass.getSuperclass();
        }
        throw new RuntimeException("invalid object/classname combination.");
    }
}
+280 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 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.layoutlib.bridge.android.support;

import com.android.ide.common.rendering.api.LayoutlibCallback;
import com.android.ide.common.rendering.api.RenderResources;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.rendering.api.StyleResourceValue;
import com.android.layoutlib.bridge.android.BridgeContext;
import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
import com.android.layoutlib.bridge.util.ReflectionUtils.ReflectionException;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.view.ContextThemeWrapper;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;
import android.widget.ScrollView;

import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;

import static com.android.layoutlib.bridge.util.ReflectionUtils.getClassInstance;
import static com.android.layoutlib.bridge.util.ReflectionUtils.getMethod;
import static com.android.layoutlib.bridge.util.ReflectionUtils.invoke;

/**
 * Class with utility methods to instantiate Preferences provided by the support library.
 * This class uses reflection to access the support preference objects so it heavily depends on
 * the API being stable.
 */
public class SupportPreferencesUtil {
    private static final String PREFERENCE_PKG = "android.support.v7.preference";
    private static final String PREFERENCE_MANAGER = PREFERENCE_PKG + ".PreferenceManager";
    private static final String PREFERENCE_GROUP = PREFERENCE_PKG + ".PreferenceGroup";
    private static final String PREFERENCE_GROUP_ADAPTER =
      PREFERENCE_PKG + ".PreferenceGroupAdapter";
    private static final String PREFERENCE_INFLATER = PREFERENCE_PKG + ".PreferenceInflater";

    private SupportPreferencesUtil() {
    }

    @NonNull
    private static Object instantiateClass(@NonNull LayoutlibCallback callback,
            @NonNull String className, @Nullable Class[] constructorSignature,
            @Nullable Object[] constructorArgs) throws ReflectionException {
        try {
            Object instance = callback.loadClass(className, constructorSignature, constructorArgs);
            if (instance == null) {
                throw new ClassNotFoundException(className + " class not found");
            }
            return instance;
        } catch (ClassNotFoundException e) {
            throw new ReflectionException(e);
        }
    }

    @NonNull
    private static Object createPreferenceGroupAdapter(@NonNull LayoutlibCallback callback,
            @NonNull Object preferenceScreen) throws ReflectionException {
        Class<?> preferenceGroupClass = getClassInstance(preferenceScreen, PREFERENCE_GROUP);

        return instantiateClass(callback, PREFERENCE_GROUP_ADAPTER,
                new Class[]{preferenceGroupClass}, new Object[]{preferenceScreen});
    }

    @NonNull
    private static Object createInflatedPreference(@NonNull LayoutlibCallback callback,
      @NonNull Context context, @NonNull XmlPullParser parser, @NonNull Object preferenceScreen,
      @NonNull Object preferenceManager) throws ReflectionException {
        Class<?> preferenceGroupClass = getClassInstance(preferenceScreen, PREFERENCE_GROUP);
        Object preferenceInflater = instantiateClass(callback, PREFERENCE_INFLATER,
          new Class[]{Context.class, preferenceManager.getClass()},
          new Object[]{context, preferenceManager});
        Object inflatedPreference = invoke(
          getMethod(preferenceInflater.getClass(), "inflate", XmlPullParser.class,
            preferenceGroupClass), preferenceInflater, parser, null);

        if (inflatedPreference == null) {
            throw new ReflectionException("inflate method returned null");
        }

        return inflatedPreference;
    }

    /**
     * Returns a themed wrapper context of {@link BridgeContext} with the theme specified in
     * ?attr/preferenceTheme applied to it.
     */
    @Nullable
    private static Context getThemedContext(@NonNull BridgeContext bridgeContext) {
        RenderResources resources = bridgeContext.getRenderResources();
        ResourceValue preferenceTheme = resources.findItemInTheme("preferenceTheme", false);

        if (preferenceTheme != null) {
            // resolve it, if needed.
            preferenceTheme = resources.resolveResValue(preferenceTheme);
        }
        if (preferenceTheme instanceof StyleResourceValue) {
            int styleId = bridgeContext.getDynamicIdByStyle(((StyleResourceValue) preferenceTheme));
            if (styleId != 0) {
                return new ContextThemeWrapper(bridgeContext, styleId);
            }
        }

        return null;
    }

    /**
     * Returns a {@link LinearLayout} containing all the UI widgets representing the preferences
     * passed in the group adapter.
     */
    @Nullable
    private static LinearLayout setUpPreferencesListView(@NonNull BridgeContext bridgeContext,
            @NonNull Context themedContext, @NonNull ArrayList<Object> viewCookie,
            @NonNull Object preferenceGroupAdapter) throws ReflectionException {
        // Setup the LinearLayout that will contain the preferences
        LinearLayout listView = new LinearLayout(themedContext);
        listView.setOrientation(LinearLayout.VERTICAL);
        listView.setLayoutParams(
                new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));

        if (!viewCookie.isEmpty()) {
            bridgeContext.addViewKey(listView, viewCookie.get(0));
        }

        // Get all the preferences and add them to the LinearLayout
        Integer preferencesCount =
                (Integer) invoke(getMethod(preferenceGroupAdapter.getClass(), "getItemCount"),
                        preferenceGroupAdapter);
        if (preferencesCount == null) {
            return listView;
        }

        Method getItemId = getMethod(preferenceGroupAdapter.getClass(), "getItemId", int.class);
        Method getItemViewType =
                getMethod(preferenceGroupAdapter.getClass(), "getItemViewType", int.class);
        Method onCreateViewHolder =
                getMethod(preferenceGroupAdapter.getClass(), "onCreateViewHolder", ViewGroup.class,
                        int.class);
        for (int i = 0; i < preferencesCount; i++) {
            Long id = (Long) invoke(getItemId, preferenceGroupAdapter, i);
            if (id == null) {
                continue;
            }

            // Get the type of the preference layout and bind it to a newly created view holder
            Integer type = (Integer) invoke(getItemViewType, preferenceGroupAdapter, i);
            Object viewHolder =
                    invoke(onCreateViewHolder, preferenceGroupAdapter, listView, type);
            if (viewHolder == null) {
                continue;
            }
            invoke(getMethod(preferenceGroupAdapter.getClass(), "onBindViewHolder",
                    viewHolder.getClass(), int.class), preferenceGroupAdapter, viewHolder, i);

            try {
                // Get the view from the view holder and add it to our layout
                View itemView =
                        (View) viewHolder.getClass().getField("itemView").get(viewHolder);

                int arrayPosition = id.intValue() - 1; // IDs are 1 based
                if (arrayPosition >= 0 && arrayPosition < viewCookie.size()) {
                    bridgeContext.addViewKey(itemView, viewCookie.get(arrayPosition));
                }
                listView.addView(itemView);
            } catch (IllegalAccessException | NoSuchFieldException ignored) {
            }
        }

        return listView;
    }

    /**
     * Inflates a preferences layout using the support library. If the support library is not
     * available, this method will return null without advancing the parsers.
     */
    @Nullable
    public static View inflatePreference(@NonNull BridgeContext bridgeContext,
            @NonNull XmlPullParser parser, @Nullable ViewGroup root) {
        try {
            LayoutlibCallback callback = bridgeContext.getLayoutlibCallback();

            Context context = getThemedContext(bridgeContext);
            if (context == null) {
                // Probably we couldn't find the "preferenceTheme" in the theme
                return null;
            }

            // Create PreferenceManager
            Object preferenceManager =
                    instantiateClass(callback, PREFERENCE_MANAGER, new Class[]{Context.class},
                            new Object[]{context});

            // From this moment on, we can assume that we found the support library and that
            // nothing should fail

            // Create PreferenceScreen
            Object preferenceScreen =
                    invoke(getMethod(preferenceManager.getClass(), "createPreferenceScreen",
                            Context.class), preferenceManager, context);
            if (preferenceScreen == null) {
                return null;
            }

            // Setup a parser that stores the list of cookies in the same order as the preferences
            // are inflated. That way we can later reconstruct the list using the preference id
            // since they are sequential and start in 1.
            ArrayList<Object> viewCookie = new ArrayList<>();
            if (parser instanceof BridgeXmlBlockParser) {
                // Setup a parser that stores the XmlTag
                parser = new BridgeXmlBlockParser(parser, null, false) {
                    @Override
                    public Object getViewCookie() {
                        return ((BridgeXmlBlockParser) getParser()).getViewCookie();
                    }

                    @Override
                    public int next() throws XmlPullParserException, IOException {
                        int ev = super.next();
                        if (ev == XmlPullParser.START_TAG) {
                            viewCookie.add(this.getViewCookie());
                        }

                        return ev;
                    }
                };
            }

            // Create the PreferenceInflater
            Object inflatedPreference =
              createInflatedPreference(callback, context, parser, preferenceScreen,
                preferenceManager);

            // Setup the RecyclerView (set adapter and layout manager)
            Object preferenceGroupAdapter =
                    createPreferenceGroupAdapter(callback, inflatedPreference);

            // Instead of just setting the group adapter as adapter for a RecyclerView, we manually
            // get all the items and add them to a LinearLayout. This allows us to set the view
            // cookies so the preferences are correctly linked to their XML.
            LinearLayout listView = setUpPreferencesListView(bridgeContext, context, viewCookie,
                    preferenceGroupAdapter);

            ScrollView scrollView = new ScrollView(context);
            scrollView.setLayoutParams(
              new ViewGroup.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
            scrollView.addView(listView);

            if (root != null) {
                root.addView(scrollView);
            }

            return scrollView;
        } catch (ReflectionException e) {
            return null;
        }
    }
}
+8 −1
Original line number Diff line number Diff line
@@ -45,6 +45,7 @@ import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
import com.android.layoutlib.bridge.android.RenderParamsFlags;
import com.android.layoutlib.bridge.android.graphics.NopCanvas;
import com.android.layoutlib.bridge.android.support.DesignLibUtil;
import com.android.layoutlib.bridge.android.support.SupportPreferencesUtil;
import com.android.layoutlib.bridge.impl.binding.FakeAdapter;
import com.android.layoutlib.bridge.impl.binding.FakeExpandableAdapter;
import com.android.resources.ResourceType;
@@ -326,8 +327,14 @@ public class RenderSessionImpl extends RenderAction<SessionParams> {
            boolean isPreference = "PreferenceScreen".equals(rootTag);
            View view;
            if (isPreference) {
                // First try to use the support library inflater. If something fails, fallback
                // to the system preference inflater.
                view = SupportPreferencesUtil.inflatePreference(getContext(), mBlockParser,
                        mContentRoot);
                if (view == null) {
                    view = Preference_Delegate.inflatePreference(getContext(), mBlockParser,
                            mContentRoot);
                }
            } else {
                view = mInflater.inflate(mBlockParser, mContentRoot);
            }
+28 −0
Original line number Diff line number Diff line
@@ -37,6 +37,15 @@ public class ReflectionUtils {
        }
    }

    @NonNull
    public static Method getAccessibleMethod(@NonNull Class<?> clazz, @NonNull String name,
      @Nullable Class<?>... params) throws ReflectionException {
        Method method = getMethod(clazz, name, params);
        method.setAccessible(true);

        return method;
    }

    @Nullable
    public static Object invoke(@NonNull Method method, @Nullable Object object,
            @Nullable Object... args) throws ReflectionException {
@@ -73,6 +82,25 @@ public class ReflectionUtils {
        return cause == null ? throwable : cause;
    }

    /**
     * Looks through the class hierarchy of {@code object} at runtime and returns the class matching
     * the name {@code className}.
     * <p>
     * This is used when we cannot use Class.forName() since the class we want was loaded from a
     * different ClassLoader.
     */
    @NonNull
    public static Class<?> getClassInstance(@NonNull Object object, @NonNull String className) {
        Class<?> superClass = object.getClass();
        while (superClass != null) {
            if (className.equals(superClass.getName())) {
                return superClass;
            }
            superClass = superClass.getSuperclass();
        }
        throw new RuntimeException("invalid object/classname combination.");
    }

    /**
     * Wraps all reflection related exceptions. Created since ReflectiveOperationException was
     * introduced in 1.7 and we are still on 1.6