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