SupportPreferencesUtil.java revision 8e8071bac1992e20df7e0cefcc9a45d0e1a5218f
1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.layoutlib.bridge.android.support;
18
19import com.android.ide.common.rendering.api.LayoutlibCallback;
20import com.android.ide.common.rendering.api.RenderResources;
21import com.android.ide.common.rendering.api.ResourceValue;
22import com.android.ide.common.rendering.api.StyleResourceValue;
23import com.android.layoutlib.bridge.android.BridgeContext;
24import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
25import com.android.layoutlib.bridge.util.ReflectionUtils.ReflectionException;
26
27import org.xmlpull.v1.XmlPullParser;
28import org.xmlpull.v1.XmlPullParserException;
29
30import android.annotation.NonNull;
31import android.annotation.Nullable;
32import android.content.Context;
33import android.view.ContextThemeWrapper;
34import android.view.View;
35import android.view.ViewGroup;
36import android.widget.LinearLayout;
37import android.widget.LinearLayout.LayoutParams;
38import android.widget.ScrollView;
39
40import java.io.IOException;
41import java.lang.reflect.Method;
42import java.util.ArrayList;
43
44import static com.android.layoutlib.bridge.util.ReflectionUtils.getClassInstance;
45import static com.android.layoutlib.bridge.util.ReflectionUtils.getMethod;
46import static com.android.layoutlib.bridge.util.ReflectionUtils.invoke;
47
48/**
49 * Class with utility methods to instantiate Preferences provided by the support library.
50 * This class uses reflection to access the support preference objects so it heavily depends on
51 * the API being stable.
52 */
53public class SupportPreferencesUtil {
54    private static final String PREFERENCE_PKG = "android.support.v7.preference";
55    private static final String PREFERENCE_MANAGER = PREFERENCE_PKG + ".PreferenceManager";
56    private static final String PREFERENCE_GROUP = PREFERENCE_PKG + ".PreferenceGroup";
57    private static final String PREFERENCE_GROUP_ADAPTER =
58      PREFERENCE_PKG + ".PreferenceGroupAdapter";
59    private static final String PREFERENCE_INFLATER = PREFERENCE_PKG + ".PreferenceInflater";
60
61    private SupportPreferencesUtil() {
62    }
63
64    @NonNull
65    private static Object instantiateClass(@NonNull LayoutlibCallback callback,
66            @NonNull String className, @Nullable Class[] constructorSignature,
67            @Nullable Object[] constructorArgs) throws ReflectionException {
68        try {
69            Object instance = callback.loadClass(className, constructorSignature, constructorArgs);
70            if (instance == null) {
71                throw new ClassNotFoundException(className + " class not found");
72            }
73            return instance;
74        } catch (ClassNotFoundException e) {
75            throw new ReflectionException(e);
76        }
77    }
78
79    @NonNull
80    private static Object createPreferenceGroupAdapter(@NonNull LayoutlibCallback callback,
81            @NonNull Object preferenceScreen) throws ReflectionException {
82        Class<?> preferenceGroupClass = getClassInstance(preferenceScreen, PREFERENCE_GROUP);
83
84        return instantiateClass(callback, PREFERENCE_GROUP_ADAPTER,
85                new Class[]{preferenceGroupClass}, new Object[]{preferenceScreen});
86    }
87
88    @NonNull
89    private static Object createInflatedPreference(@NonNull LayoutlibCallback callback,
90      @NonNull Context context, @NonNull XmlPullParser parser, @NonNull Object preferenceScreen,
91      @NonNull Object preferenceManager) throws ReflectionException {
92        Class<?> preferenceGroupClass = getClassInstance(preferenceScreen, PREFERENCE_GROUP);
93        Object preferenceInflater = instantiateClass(callback, PREFERENCE_INFLATER,
94          new Class[]{Context.class, preferenceManager.getClass()},
95          new Object[]{context, preferenceManager});
96        Object inflatedPreference = invoke(
97          getMethod(preferenceInflater.getClass(), "inflate", XmlPullParser.class,
98            preferenceGroupClass), preferenceInflater, parser, null);
99
100        if (inflatedPreference == null) {
101            throw new ReflectionException("inflate method returned null");
102        }
103
104        return inflatedPreference;
105    }
106
107    /**
108     * Returns a themed wrapper context of {@link BridgeContext} with the theme specified in
109     * ?attr/preferenceTheme applied to it.
110     */
111    @Nullable
112    private static Context getThemedContext(@NonNull BridgeContext bridgeContext) {
113        RenderResources resources = bridgeContext.getRenderResources();
114        ResourceValue preferenceTheme = resources.findItemInTheme("preferenceTheme", false);
115
116        if (preferenceTheme != null) {
117            // resolve it, if needed.
118            preferenceTheme = resources.resolveResValue(preferenceTheme);
119        }
120        if (preferenceTheme instanceof StyleResourceValue) {
121            int styleId = bridgeContext.getDynamicIdByStyle(((StyleResourceValue) preferenceTheme));
122            if (styleId != 0) {
123                return new ContextThemeWrapper(bridgeContext, styleId);
124            }
125        }
126
127        return null;
128    }
129
130    /**
131     * Returns a {@link LinearLayout} containing all the UI widgets representing the preferences
132     * passed in the group adapter.
133     */
134    @Nullable
135    private static LinearLayout setUpPreferencesListView(@NonNull BridgeContext bridgeContext,
136            @NonNull Context themedContext, @NonNull ArrayList<Object> viewCookie,
137            @NonNull Object preferenceGroupAdapter) throws ReflectionException {
138        // Setup the LinearLayout that will contain the preferences
139        LinearLayout listView = new LinearLayout(themedContext);
140        listView.setOrientation(LinearLayout.VERTICAL);
141        listView.setLayoutParams(
142                new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
143
144        if (!viewCookie.isEmpty()) {
145            bridgeContext.addViewKey(listView, viewCookie.get(0));
146        }
147
148        // Get all the preferences and add them to the LinearLayout
149        Integer preferencesCount =
150                (Integer) invoke(getMethod(preferenceGroupAdapter.getClass(), "getItemCount"),
151                        preferenceGroupAdapter);
152        if (preferencesCount == null) {
153            return listView;
154        }
155
156        Method getItemId = getMethod(preferenceGroupAdapter.getClass(), "getItemId", int.class);
157        Method getItemViewType =
158                getMethod(preferenceGroupAdapter.getClass(), "getItemViewType", int.class);
159        Method onCreateViewHolder =
160                getMethod(preferenceGroupAdapter.getClass(), "onCreateViewHolder", ViewGroup.class,
161                        int.class);
162        for (int i = 0; i < preferencesCount; i++) {
163            Long id = (Long) invoke(getItemId, preferenceGroupAdapter, i);
164            if (id == null) {
165                continue;
166            }
167
168            // Get the type of the preference layout and bind it to a newly created view holder
169            Integer type = (Integer) invoke(getItemViewType, preferenceGroupAdapter, i);
170            Object viewHolder =
171                    invoke(onCreateViewHolder, preferenceGroupAdapter, listView, type);
172            if (viewHolder == null) {
173                continue;
174            }
175            invoke(getMethod(preferenceGroupAdapter.getClass(), "onBindViewHolder",
176                    viewHolder.getClass(), int.class), preferenceGroupAdapter, viewHolder, i);
177
178            try {
179                // Get the view from the view holder and add it to our layout
180                View itemView =
181                        (View) viewHolder.getClass().getField("itemView").get(viewHolder);
182
183                int arrayPosition = id.intValue() - 1; // IDs are 1 based
184                if (arrayPosition >= 0 && arrayPosition < viewCookie.size()) {
185                    bridgeContext.addViewKey(itemView, viewCookie.get(arrayPosition));
186                }
187                listView.addView(itemView);
188            } catch (IllegalAccessException | NoSuchFieldException ignored) {
189            }
190        }
191
192        return listView;
193    }
194
195    /**
196     * Inflates a preferences layout using the support library. If the support library is not
197     * available, this method will return null without advancing the parsers.
198     */
199    @Nullable
200    public static View inflatePreference(@NonNull BridgeContext bridgeContext,
201            @NonNull XmlPullParser parser, @Nullable ViewGroup root) {
202        try {
203            LayoutlibCallback callback = bridgeContext.getLayoutlibCallback();
204
205            Context context = getThemedContext(bridgeContext);
206            if (context == null) {
207                // Probably we couldn't find the "preferenceTheme" in the theme
208                return null;
209            }
210
211            // Create PreferenceManager
212            Object preferenceManager =
213                    instantiateClass(callback, PREFERENCE_MANAGER, new Class[]{Context.class},
214                            new Object[]{context});
215
216            // From this moment on, we can assume that we found the support library and that
217            // nothing should fail
218
219            // Create PreferenceScreen
220            Object preferenceScreen =
221                    invoke(getMethod(preferenceManager.getClass(), "createPreferenceScreen",
222                            Context.class), preferenceManager, context);
223            if (preferenceScreen == null) {
224                return null;
225            }
226
227            // Setup a parser that stores the list of cookies in the same order as the preferences
228            // are inflated. That way we can later reconstruct the list using the preference id
229            // since they are sequential and start in 1.
230            ArrayList<Object> viewCookie = new ArrayList<>();
231            if (parser instanceof BridgeXmlBlockParser) {
232                // Setup a parser that stores the XmlTag
233                parser = new BridgeXmlBlockParser(parser, null, false) {
234                    @Override
235                    public Object getViewCookie() {
236                        return ((BridgeXmlBlockParser) getParser()).getViewCookie();
237                    }
238
239                    @Override
240                    public int next() throws XmlPullParserException, IOException {
241                        int ev = super.next();
242                        if (ev == XmlPullParser.START_TAG) {
243                            viewCookie.add(this.getViewCookie());
244                        }
245
246                        return ev;
247                    }
248                };
249            }
250
251            // Create the PreferenceInflater
252            Object inflatedPreference =
253              createInflatedPreference(callback, context, parser, preferenceScreen,
254                preferenceManager);
255
256            // Setup the RecyclerView (set adapter and layout manager)
257            Object preferenceGroupAdapter =
258                    createPreferenceGroupAdapter(callback, inflatedPreference);
259
260            // Instead of just setting the group adapter as adapter for a RecyclerView, we manually
261            // get all the items and add them to a LinearLayout. This allows us to set the view
262            // cookies so the preferences are correctly linked to their XML.
263            LinearLayout listView = setUpPreferencesListView(bridgeContext, context, viewCookie,
264                    preferenceGroupAdapter);
265
266            ScrollView scrollView = new ScrollView(context);
267            scrollView.setLayoutParams(
268              new ViewGroup.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
269            scrollView.addView(listView);
270
271            if (root != null) {
272                root.addView(scrollView);
273            }
274
275            return scrollView;
276        } catch (ReflectionException e) {
277            return null;
278        }
279    }
280}
281