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 
17 package com.android.layoutlib.bridge.android.support;
18 
19 import com.android.ide.common.rendering.api.LayoutlibCallback;
20 import com.android.ide.common.rendering.api.RenderResources;
21 import com.android.ide.common.rendering.api.ResourceNamespace;
22 import com.android.ide.common.rendering.api.ResourceReference;
23 import com.android.ide.common.rendering.api.ResourceValue;
24 import com.android.ide.common.rendering.api.StyleResourceValue;
25 import com.android.layoutlib.bridge.android.BridgeContext;
26 import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
27 import com.android.layoutlib.bridge.util.ReflectionUtils.ReflectionException;
28 import com.android.resources.ResourceType;
29 import com.android.tools.layoutlib.annotations.NotNull;
30 
31 import org.xmlpull.v1.XmlPullParser;
32 import org.xmlpull.v1.XmlPullParserException;
33 
34 import android.annotation.NonNull;
35 import android.annotation.Nullable;
36 import android.content.Context;
37 import android.view.ContextThemeWrapper;
38 import android.view.View;
39 import android.view.ViewGroup;
40 import android.widget.LinearLayout;
41 import android.widget.LinearLayout.LayoutParams;
42 import android.widget.ScrollView;
43 
44 import java.io.IOException;
45 import java.lang.reflect.Method;
46 import java.util.ArrayList;
47 
48 import static com.android.layoutlib.bridge.util.ReflectionUtils.getAccessibleMethod;
49 import static com.android.layoutlib.bridge.util.ReflectionUtils.getClassInstance;
50 import static com.android.layoutlib.bridge.util.ReflectionUtils.getMethod;
51 import static com.android.layoutlib.bridge.util.ReflectionUtils.invoke;
52 
53 /**
54  * Class with utility methods to instantiate Preferences provided by the support library.
55  * This class uses reflection to access the support preference objects so it heavily depends on
56  * the API being stable.
57  */
58 public class SupportPreferencesUtil {
59     private static final String[] PREFERENCES_PKG_NAMES = {
60             "android.support.v7.preference",
61             "androidx.preference"
62     };
63 
SupportPreferencesUtil()64     private SupportPreferencesUtil() {
65     }
66 
67     @NonNull
instantiateClass(@onNull LayoutlibCallback callback, @NonNull String className, @Nullable Class[] constructorSignature, @Nullable Object[] constructorArgs)68     private static Object instantiateClass(@NonNull LayoutlibCallback callback,
69             @NonNull String className, @Nullable Class[] constructorSignature,
70             @Nullable Object[] constructorArgs) throws ReflectionException {
71         try {
72             Object instance = callback.loadClass(className, constructorSignature, constructorArgs);
73             if (instance == null) {
74                 throw new ClassNotFoundException(className + " class not found");
75             }
76             return instance;
77         } catch (ClassNotFoundException e) {
78             throw new ReflectionException(e);
79         }
80     }
81 
82     @NonNull
createPreferenceGroupAdapter(@onNull LayoutlibCallback callback, @NonNull String preferenceGroupClassName, @NonNull String preferenceGroupAdapterClassName, @NonNull Object preferenceScreen)83     private static Object createPreferenceGroupAdapter(@NonNull LayoutlibCallback callback,
84             @NonNull String preferenceGroupClassName,
85             @NonNull String preferenceGroupAdapterClassName, @NonNull Object preferenceScreen)
86             throws ReflectionException {
87         Class<?> preferenceGroupClass =
88                 getClassInstance(preferenceScreen, preferenceGroupClassName);
89 
90         return instantiateClass(callback, preferenceGroupAdapterClassName,
91                 new Class[]{preferenceGroupClass}, new Object[]{preferenceScreen});
92     }
93 
94     @NonNull
createInflatedPreference(@onNull LayoutlibCallback callback, @NonNull String preferenceGroupClassName, @NonNull String preferenceInflaterClassName, @NonNull Context context, @NonNull XmlPullParser parser, @NonNull Object preferenceScreen, @NonNull Object preferenceManager)95     private static Object createInflatedPreference(@NonNull LayoutlibCallback callback,
96             @NonNull String preferenceGroupClassName, @NonNull String preferenceInflaterClassName,
97             @NonNull Context context, @NonNull XmlPullParser parser,
98             @NonNull Object preferenceScreen, @NonNull Object preferenceManager)
99             throws ReflectionException {
100         Class<?> preferenceGroupClass =
101                 getClassInstance(preferenceScreen, preferenceGroupClassName);
102         Object preferenceInflater = instantiateClass(callback, preferenceInflaterClassName,
103                 new Class[]{Context.class, preferenceManager.getClass()},
104                 new Object[]{context, preferenceManager});
105         Object inflatedPreference =
106                 invoke(getAccessibleMethod(preferenceInflater.getClass(), "inflate",
107                         XmlPullParser.class, preferenceGroupClass), preferenceInflater, parser,
108                         null);
109 
110         if (inflatedPreference == null) {
111             throw new ReflectionException("inflate method returned null");
112         }
113 
114         return inflatedPreference;
115     }
116 
117     /**
118      * Returns a themed wrapper context of {@link BridgeContext} with the theme specified in
119      * ?attr/preferenceTheme applied to it.
120      */
121     @NotNull
getThemedContext(@onNull BridgeContext bridgeContext)122     private static Context getThemedContext(@NonNull BridgeContext bridgeContext) {
123         RenderResources resources = bridgeContext.getRenderResources();
124         ResourceValue preferenceTheme = resources.findItemInTheme(
125                 bridgeContext.createAppCompatAttrReference("preferenceTheme"));
126 
127         if (preferenceTheme != null) {
128             // resolve it, if needed.
129             preferenceTheme = resources.resolveResValue(preferenceTheme);
130         } else {
131             // The current theme does not define "preferenceTheme" so we will use the default
132             // "PreferenceThemeOverlay" if available.
133             preferenceTheme = resources.getStyle(
134                     bridgeContext.createAppCompatResourceReference(ResourceType.STYLE,
135                             "PreferenceThemeOverlay"));
136         }
137         if (preferenceTheme instanceof StyleResourceValue) {
138             int styleId = bridgeContext.getDynamicIdByStyle(((StyleResourceValue) preferenceTheme));
139             if (styleId != 0) {
140                 return new ContextThemeWrapper(bridgeContext, styleId);
141             }
142         }
143 
144         // We were not able to find any preferences theme so return the original Context without
145         // any theme wrapping
146         return bridgeContext;
147     }
148 
149     /**
150      * Returns a {@link LinearLayout} containing all the UI widgets representing the preferences
151      * passed in the group adapter.
152      */
153     @Nullable
setUpPreferencesListView(@onNull BridgeContext bridgeContext, @NonNull Context themedContext, @NonNull ArrayList<Object> viewCookie, @NonNull Object preferenceGroupAdapter)154     private static LinearLayout setUpPreferencesListView(@NonNull BridgeContext bridgeContext,
155             @NonNull Context themedContext, @NonNull ArrayList<Object> viewCookie,
156             @NonNull Object preferenceGroupAdapter) throws ReflectionException {
157         // Setup the LinearLayout that will contain the preferences
158         LinearLayout listView = new LinearLayout(themedContext);
159         listView.setOrientation(LinearLayout.VERTICAL);
160         listView.setLayoutParams(
161                 new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
162 
163         if (!viewCookie.isEmpty()) {
164             bridgeContext.addViewKey(listView, viewCookie.get(0));
165         }
166 
167         // Get all the preferences and add them to the LinearLayout
168         Integer preferencesCount =
169                 (Integer) invoke(getMethod(preferenceGroupAdapter.getClass(), "getItemCount"),
170                         preferenceGroupAdapter);
171         if (preferencesCount == null) {
172             return listView;
173         }
174 
175         Method getItemId = getMethod(preferenceGroupAdapter.getClass(), "getItemId", int.class);
176         Method getItemViewType =
177                 getMethod(preferenceGroupAdapter.getClass(), "getItemViewType", int.class);
178         Method onCreateViewHolder =
179                 getMethod(preferenceGroupAdapter.getClass(), "onCreateViewHolder", ViewGroup.class,
180                         int.class);
181         for (int i = 0; i < preferencesCount; i++) {
182             Long id = (Long) invoke(getItemId, preferenceGroupAdapter, i);
183             if (id == null) {
184                 continue;
185             }
186 
187             // Get the type of the preference layout and bind it to a newly created view holder
188             Integer type = (Integer) invoke(getItemViewType, preferenceGroupAdapter, i);
189             Object viewHolder = invoke(onCreateViewHolder, preferenceGroupAdapter, listView, type);
190             if (viewHolder == null) {
191                 continue;
192             }
193             invoke(getMethod(preferenceGroupAdapter.getClass(), "onBindViewHolder",
194                     viewHolder.getClass(), int.class), preferenceGroupAdapter, viewHolder, i);
195 
196             try {
197                 // Get the view from the view holder and add it to our layout
198                 View itemView = (View) viewHolder.getClass().getField("itemView").get(viewHolder);
199 
200                 int arrayPosition = id.intValue() - 1; // IDs are 1 based
201                 if (arrayPosition >= 0 && arrayPosition < viewCookie.size()) {
202                     bridgeContext.addViewKey(itemView, viewCookie.get(arrayPosition));
203                 }
204                 listView.addView(itemView);
205             } catch (IllegalAccessException | NoSuchFieldException ignored) {
206             }
207         }
208 
209         return listView;
210     }
211 
212     /**
213      * Inflates a preferences layout using the support library. If the support library is not
214      * available, this method will return null without advancing the parsers.
215      */
216     @Nullable
inflatePreference(@onNull BridgeContext bridgeContext, @NonNull XmlPullParser parser, @Nullable ViewGroup root)217     public static View inflatePreference(@NonNull BridgeContext bridgeContext,
218             @NonNull XmlPullParser parser, @Nullable ViewGroup root) {
219         String preferencePackageName = null;
220         String preferenceManagerClassName = null;
221         // Find the correct package for the classes
222         for (int i = PREFERENCES_PKG_NAMES.length - 1; i >= 0; i--) {
223             preferencePackageName = PREFERENCES_PKG_NAMES[i];
224             preferenceManagerClassName = preferencePackageName + ".PreferenceManager";
225             try {
226                 bridgeContext.getLayoutlibCallback().findClass(preferenceManagerClassName);
227                 break;
228             } catch (ClassNotFoundException ignore) {
229             }
230         }
231 
232         assert preferencePackageName != null;
233         String preferenceGroupClassName = preferencePackageName + ".PreferenceGroup";
234         String preferenceGroupAdapterClassName = preferencePackageName + ".PreferenceGroupAdapter";
235         String preferenceInflaterClassName = preferencePackageName + ".PreferenceInflater";
236 
237         try {
238             LayoutlibCallback callback = bridgeContext.getLayoutlibCallback();
239             Context context = getThemedContext(bridgeContext);
240 
241             // Create PreferenceManager
242             Object preferenceManager = instantiateClass(callback, preferenceManagerClassName,
243                     new Class[]{Context.class}, new Object[]{context});
244 
245             // From this moment on, we can assume that we found the support library and that
246             // nothing should fail
247 
248             // Create PreferenceScreen
249             Object preferenceScreen =
250                     invoke(getMethod(preferenceManager.getClass(), "createPreferenceScreen",
251                             Context.class), preferenceManager, context);
252             if (preferenceScreen == null) {
253                 return null;
254             }
255 
256             // Setup a parser that stores the list of cookies in the same order as the preferences
257             // are inflated. That way we can later reconstruct the list using the preference id
258             // since they are sequential and start in 1.
259             ArrayList<Object> viewCookie = new ArrayList<>();
260             if (parser instanceof BridgeXmlBlockParser) {
261                 // Setup a parser that stores the XmlTag
262                 parser =
263                         new BridgeXmlBlockParser(
264                                 parser,
265                                 null,
266                                 ((BridgeXmlBlockParser) parser).getFileResourceNamespace()) {
267                             @Override
268                             public Object getViewCookie() {
269                                 return ((BridgeXmlBlockParser) getParser()).getViewCookie();
270                             }
271 
272                             @Override
273                             public int next() throws XmlPullParserException, IOException {
274                                 int ev = super.next();
275                                 if (ev == XmlPullParser.START_TAG) {
276                                     viewCookie.add(this.getViewCookie());
277                                 }
278 
279                                 return ev;
280                             }
281                         };
282             }
283 
284             // Create the PreferenceInflater
285             Object inflatedPreference = createInflatedPreference(callback, preferenceGroupClassName,
286                     preferenceInflaterClassName, context, parser, preferenceScreen,
287                     preferenceManager);
288 
289             // Setup the RecyclerView (set adapter and layout manager)
290             Object preferenceGroupAdapter =
291                     createPreferenceGroupAdapter(callback, preferenceGroupClassName,
292                             preferenceGroupAdapterClassName, inflatedPreference);
293 
294             // Instead of just setting the group adapter as adapter for a RecyclerView, we manually
295             // get all the items and add them to a LinearLayout. This allows us to set the view
296             // cookies so the preferences are correctly linked to their XML.
297             LinearLayout listView = setUpPreferencesListView(bridgeContext, context, viewCookie,
298                     preferenceGroupAdapter);
299 
300             ScrollView scrollView = new ScrollView(context);
301             scrollView.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,
302                     LayoutParams.MATCH_PARENT));
303             scrollView.addView(listView);
304 
305             if (root != null) {
306                 root.addView(scrollView);
307             }
308 
309             return scrollView;
310         } catch (ReflectionException e) {
311             return null;
312         }
313     }
314 
315     /**
316      * Returns true if the given root tag is any of the support library {@code PreferenceScreen}
317      * tags.
318      */
isSupportRootTag(@ullable String rootTag)319     public static boolean isSupportRootTag(@Nullable String rootTag) {
320         if (rootTag != null) {
321             for (String supportPrefix : PREFERENCES_PKG_NAMES) {
322                 if (rootTag.equals(supportPrefix + ".PreferenceScreen")) {
323                     return true;
324                 }
325             }
326         }
327 
328         return false;
329     }
330 }
331