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