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