1/* 2 * Copyright (C) 2015 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 android.support.v7.preference; 18 19import android.content.Context; 20import android.content.Intent; 21import android.content.res.XmlResourceParser; 22import android.os.Build; 23import android.support.annotation.NonNull; 24import android.support.annotation.Nullable; 25import android.util.AttributeSet; 26import android.util.Xml; 27import android.view.InflateException; 28 29import org.xmlpull.v1.XmlPullParser; 30import org.xmlpull.v1.XmlPullParserException; 31 32import java.io.IOException; 33import java.lang.reflect.Constructor; 34import java.util.HashMap; 35 36/** 37 * The {@link PreferenceInflater} is used to inflate preference hierarchies from 38 * XML files. 39 */ 40class PreferenceInflater { 41 private static final String TAG = "PreferenceInflater"; 42 43 private static final Class<?>[] CONSTRUCTOR_SIGNATURE = new Class[] { 44 Context.class, AttributeSet.class}; 45 46 private static final HashMap<String, Constructor> CONSTRUCTOR_MAP = new HashMap<>(); 47 48 private final Context mContext; 49 50 private final Object[] mConstructorArgs = new Object[2]; 51 52 private PreferenceManager mPreferenceManager; 53 54 private String[] mDefaultPackages; 55 56 private static final String INTENT_TAG_NAME = "intent"; 57 private static final String EXTRA_TAG_NAME = "extra"; 58 59 public PreferenceInflater(Context context, PreferenceManager preferenceManager) { 60 mContext = context; 61 init(preferenceManager); 62 } 63 64 private void init(PreferenceManager preferenceManager) { 65 mPreferenceManager = preferenceManager; 66 if (Build.VERSION.SDK_INT >= 14) { 67 setDefaultPackages(new String[] {"android.support.v14.preference.", 68 "android.support.v7.preference."}); 69 } else { 70 setDefaultPackages(new String[] {"android.support.v7.preference."}); 71 } 72 } 73 74 /** 75 * Sets the default package that will be searched for classes to construct 76 * for tag names that have no explicit package. 77 * 78 * @param defaultPackage The default package. This will be prepended to the 79 * tag name, so it should end with a period. 80 */ 81 public void setDefaultPackages(String[] defaultPackage) { 82 mDefaultPackages = defaultPackage; 83 } 84 85 /** 86 * Returns the default package, or null if it is not set. 87 * 88 * @see #setDefaultPackages(String[]) 89 * @return The default package. 90 */ 91 public String[] getDefaultPackages() { 92 return mDefaultPackages; 93 } 94 95 /** 96 * Return the context we are running in, for access to resources, class 97 * loader, etc. 98 */ 99 public Context getContext() { 100 return mContext; 101 } 102 103 /** 104 * Inflate a new item hierarchy from the specified xml resource. Throws 105 * InflaterException if there is an error. 106 * 107 * @param resource ID for an XML resource to load (e.g., 108 * <code>R.layout.main_page</code>) 109 * @param root Optional parent of the generated hierarchy. 110 * @return The root of the inflated hierarchy. If root was supplied, 111 * this is the root item; otherwise it is the root of the inflated 112 * XML file. 113 */ 114 public Preference inflate(int resource, @Nullable PreferenceGroup root) { 115 XmlResourceParser parser = getContext().getResources().getXml(resource); 116 try { 117 return inflate(parser, root); 118 } finally { 119 parser.close(); 120 } 121 } 122 123 /** 124 * Inflate a new hierarchy from the specified XML node. Throws 125 * InflaterException if there is an error. 126 * <p> 127 * <em><strong>Important</strong></em> For performance 128 * reasons, inflation relies heavily on pre-processing of XML files 129 * that is done at build time. Therefore, it is not currently possible to 130 * use inflater with an XmlPullParser over a plain XML file at runtime. 131 * 132 * @param parser XML dom node containing the description of the 133 * hierarchy. 134 * @param root Optional to be the parent of the generated hierarchy (if 135 * <em>attachToRoot</em> is true), or else simply an object that 136 * provides a set of values for root of the returned 137 * hierarchy (if <em>attachToRoot</em> is false.) 138 * @return The root of the inflated hierarchy. If root was supplied, 139 * this is root; otherwise it is the root of 140 * the inflated XML file. 141 */ 142 public Preference inflate(XmlPullParser parser, @Nullable PreferenceGroup root) { 143 synchronized (mConstructorArgs) { 144 final AttributeSet attrs = Xml.asAttributeSet(parser); 145 mConstructorArgs[0] = mContext; 146 final Preference result; 147 148 try { 149 // Look for the root node. 150 int type; 151 do { 152 type = parser.next(); 153 } while (type != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT); 154 155 if (type != XmlPullParser.START_TAG) { 156 throw new InflateException(parser.getPositionDescription() 157 + ": No start tag found!"); 158 } 159 160 // Temp is the root that was found in the xml 161 Preference xmlRoot = createItemFromTag(parser.getName(), 162 attrs); 163 164 result = onMergeRoots(root, (PreferenceGroup) xmlRoot); 165 166 // Inflate all children under temp 167 rInflate(parser, result, attrs); 168 169 } catch (InflateException e) { 170 throw e; 171 } catch (XmlPullParserException e) { 172 final InflateException ex = new InflateException(e.getMessage()); 173 ex.initCause(e); 174 throw ex; 175 } catch (IOException e) { 176 final InflateException ex = new InflateException( 177 parser.getPositionDescription() 178 + ": " + e.getMessage()); 179 ex.initCause(e); 180 throw ex; 181 } 182 183 return result; 184 } 185 } 186 187 private @NonNull PreferenceGroup onMergeRoots(PreferenceGroup givenRoot, 188 @NonNull PreferenceGroup xmlRoot) { 189 // If we were given a Preferences, use it as the root (ignoring the root 190 // Preferences from the XML file). 191 if (givenRoot == null) { 192 xmlRoot.onAttachedToHierarchy(mPreferenceManager); 193 return xmlRoot; 194 } else { 195 return givenRoot; 196 } 197 } 198 199 /** 200 * Low-level function for instantiating by name. This attempts to 201 * instantiate class of the given <var>name</var> found in this 202 * inflater's ClassLoader. 203 * 204 * <p> 205 * There are two things that can happen in an error case: either the 206 * exception describing the error will be thrown, or a null will be 207 * returned. You must deal with both possibilities -- the former will happen 208 * the first time createItem() is called for a class of a particular name, 209 * the latter every time there-after for that class name. 210 * 211 * @param name The full name of the class to be instantiated. 212 * @param attrs The XML attributes supplied for this instance. 213 * 214 * @return The newly instantied item, or null. 215 */ 216 private Preference createItem(@NonNull String name, @Nullable String[] prefixes, 217 AttributeSet attrs) 218 throws ClassNotFoundException, InflateException { 219 Constructor constructor = CONSTRUCTOR_MAP.get(name); 220 221 try { 222 if (constructor == null) { 223 // Class not found in the cache, see if it's real, 224 // and try to add it 225 final ClassLoader classLoader = mContext.getClassLoader(); 226 Class<?> clazz = null; 227 if (prefixes == null || prefixes.length == 0) { 228 clazz = classLoader.loadClass(name); 229 } else { 230 ClassNotFoundException notFoundException = null; 231 for (final String prefix : prefixes) { 232 try { 233 clazz = classLoader.loadClass(prefix + name); 234 break; 235 } catch (final ClassNotFoundException e) { 236 notFoundException = e; 237 } 238 } 239 if (clazz == null) { 240 if (notFoundException == null) { 241 throw new InflateException(attrs 242 .getPositionDescription() 243 + ": Error inflating class " + name); 244 } else { 245 throw notFoundException; 246 } 247 } 248 } 249 constructor = clazz.getConstructor(CONSTRUCTOR_SIGNATURE); 250 constructor.setAccessible(true); 251 CONSTRUCTOR_MAP.put(name, constructor); 252 } 253 254 Object[] args = mConstructorArgs; 255 args[1] = attrs; 256 return (Preference) constructor.newInstance(args); 257 258 } catch (ClassNotFoundException e) { 259 // If loadClass fails, we should propagate the exception. 260 throw e; 261 } catch (Exception e) { 262 final InflateException ie = new InflateException(attrs 263 .getPositionDescription() + ": Error inflating class " + name); 264 ie.initCause(e); 265 throw ie; 266 } 267 } 268 269 /** 270 * This routine is responsible for creating the correct subclass of item 271 * given the xml element name. Override it to handle custom item objects. If 272 * you override this in your subclass be sure to call through to 273 * super.onCreateItem(name) for names you do not recognize. 274 * 275 * @param name The fully qualified class name of the item to be create. 276 * @param attrs An AttributeSet of attributes to apply to the item. 277 * @return The item created. 278 */ 279 protected Preference onCreateItem(String name, AttributeSet attrs) 280 throws ClassNotFoundException { 281 return createItem(name, mDefaultPackages, attrs); 282 } 283 284 private Preference createItemFromTag(String name, 285 AttributeSet attrs) { 286 try { 287 final Preference item; 288 289 if (-1 == name.indexOf('.')) { 290 item = onCreateItem(name, attrs); 291 } else { 292 item = createItem(name, null, attrs); 293 } 294 295 return item; 296 297 } catch (InflateException e) { 298 throw e; 299 300 } catch (ClassNotFoundException e) { 301 final InflateException ie = new InflateException(attrs 302 .getPositionDescription() 303 + ": Error inflating class (not found)" + name); 304 ie.initCause(e); 305 throw ie; 306 307 } catch (Exception e) { 308 final InflateException ie = new InflateException(attrs 309 .getPositionDescription() 310 + ": Error inflating class " + name); 311 ie.initCause(e); 312 throw ie; 313 } 314 } 315 316 /** 317 * Recursive method used to descend down the xml hierarchy and instantiate 318 * items, instantiate their children, and then call onFinishInflate(). 319 */ 320 private void rInflate(XmlPullParser parser, Preference parent, final AttributeSet attrs) 321 throws XmlPullParserException, IOException { 322 final int depth = parser.getDepth(); 323 324 int type; 325 while (((type = parser.next()) != XmlPullParser.END_TAG || 326 parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { 327 328 if (type != XmlPullParser.START_TAG) { 329 continue; 330 } 331 332 final String name = parser.getName(); 333 334 if (INTENT_TAG_NAME.equals(name)) { 335 final Intent intent; 336 337 try { 338 intent = Intent.parseIntent(getContext().getResources(), parser, attrs); 339 } catch (IOException e) { 340 XmlPullParserException ex = new XmlPullParserException( 341 "Error parsing preference"); 342 ex.initCause(e); 343 throw ex; 344 } 345 346 parent.setIntent(intent); 347 } else if (EXTRA_TAG_NAME.equals(name)) { 348 getContext().getResources().parseBundleExtra(EXTRA_TAG_NAME, attrs, 349 parent.getExtras()); 350 try { 351 skipCurrentTag(parser); 352 } catch (IOException e) { 353 XmlPullParserException ex = new XmlPullParserException( 354 "Error parsing preference"); 355 ex.initCause(e); 356 throw ex; 357 } 358 } else { 359 final Preference item = createItemFromTag(name, attrs); 360 ((PreferenceGroup) parent).addItemFromInflater(item); 361 rInflate(parser, item, attrs); 362 } 363 } 364 365 } 366 367 private static void skipCurrentTag(XmlPullParser parser) 368 throws XmlPullParserException, IOException { 369 int outerDepth = parser.getDepth(); 370 int type; 371 do { 372 type = parser.next(); 373 } while (type != XmlPullParser.END_DOCUMENT 374 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)); 375 } 376 377} 378