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.ILayoutLog; 20 import com.android.ide.common.rendering.api.LayoutlibCallback; 21 import com.android.ide.common.rendering.api.RenderResources; 22 import com.android.ide.common.rendering.api.ResourceValue; 23 import com.android.ide.common.rendering.api.StyleResourceValue; 24 import com.android.layoutlib.bridge.android.BridgeContext; 25 import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; 26 import com.android.layoutlib.common.util.ReflectionUtils.ReflectionException; 27 import com.android.resources.ResourceType; 28 import com.android.tools.layoutlib.annotations.NotNull; 29 30 import org.xmlpull.v1.XmlPullParser; 31 import org.xmlpull.v1.XmlPullParserException; 32 33 import android.annotation.NonNull; 34 import android.annotation.Nullable; 35 import android.content.Context; 36 import android.view.ContextThemeWrapper; 37 import android.view.View; 38 import android.view.ViewGroup; 39 import android.widget.LinearLayout; 40 import android.widget.LinearLayout.LayoutParams; 41 import android.widget.ScrollView; 42 43 import java.io.IOException; 44 import java.lang.reflect.Method; 45 import java.util.ArrayList; 46 47 import static com.android.layoutlib.bridge.Bridge.getLog; 48 import static com.android.layoutlib.common.util.ReflectionUtils.getAccessibleMethod; 49 import static com.android.layoutlib.common.util.ReflectionUtils.getClassInstance; 50 import static com.android.layoutlib.common.util.ReflectionUtils.getMethod; 51 import static com.android.layoutlib.common.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) throws Throwable { 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 Throwable t = e; 312 while (t.getCause() != null) { 313 t = t.getCause(); 314 } 315 if (t instanceof ClassNotFoundException) { 316 String message = t.getMessage(); 317 if (message != null && !message.contains(preferencePackageName)) { 318 // If the class not found is not part of the preference library, then it 319 // must be a custom preference. Log the error and throw the exception, which 320 // will prevent trying to inflate with the Android framework preference 321 // installer 322 getLog().error(ILayoutLog.TAG_INFLATE, t.getMessage(), null, null); 323 throw t; 324 } 325 } 326 return null; 327 } 328 } 329 330 /** 331 * Returns true if the given root tag is any of the support library {@code PreferenceScreen} 332 * tags. 333 */ isSupportRootTag(@ullable String rootTag)334 public static boolean isSupportRootTag(@Nullable String rootTag) { 335 if (rootTag != null) { 336 for (String supportPrefix : PREFERENCES_PKG_NAMES) { 337 if (rootTag.equals(supportPrefix + ".PreferenceScreen")) { 338 return true; 339 } 340 } 341 } 342 343 return false; 344 } 345 } 346