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