1 /* 2 * Copyright (C) 2007 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 android.preference; 18 19 import android.annotation.XmlRes; 20 import android.content.Context; 21 import android.content.res.XmlResourceParser; 22 import android.util.AttributeSet; 23 import android.util.Xml; 24 import android.view.ContextThemeWrapper; 25 import android.view.InflateException; 26 import android.view.LayoutInflater; 27 28 import org.xmlpull.v1.XmlPullParser; 29 import org.xmlpull.v1.XmlPullParserException; 30 31 import java.io.IOException; 32 import java.lang.reflect.Constructor; 33 import java.util.HashMap; 34 35 // TODO: fix generics 36 /** 37 * Generic XML inflater. This has been adapted from {@link LayoutInflater} and 38 * quickly passed over to use generics. 39 * 40 * @hide 41 * @param T The type of the items to inflate 42 * @param P The type of parents (that is those items that contain other items). 43 * Must implement {@link GenericInflater.Parent} 44 * 45 * @deprecated Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a> 46 * <a href="{@docRoot}reference/androidx/preference/package-summary.html"> 47 * Preference Library</a> for consistent behavior across all devices. For more information on 48 * using the AndroidX Preference Library see 49 * <a href="{@docRoot}guide/topics/ui/settings.html">Settings</a>. 50 */ 51 @Deprecated 52 abstract class GenericInflater<T, P extends GenericInflater.Parent> { 53 private final boolean DEBUG = false; 54 55 protected final Context mContext; 56 57 // these are optional, set by the caller 58 private boolean mFactorySet; 59 private Factory<T> mFactory; 60 61 private final Object[] mConstructorArgs = new Object[2]; 62 63 private static final Class[] mConstructorSignature = new Class[] { 64 Context.class, AttributeSet.class}; 65 66 private static final HashMap sConstructorMap = new HashMap(); 67 68 private String mDefaultPackage; 69 70 public interface Parent<T> { addItemFromInflater(T child)71 public void addItemFromInflater(T child); 72 } 73 74 public interface Factory<T> { 75 /** 76 * Hook you can supply that is called when inflating from a 77 * inflater. You can use this to customize the tag 78 * names available in your XML files. 79 * <p> 80 * Note that it is good practice to prefix these custom names with your 81 * package (i.e., com.coolcompany.apps) to avoid conflicts with system 82 * names. 83 * 84 * @param name Tag name to be inflated. 85 * @param context The context the item is being created in. 86 * @param attrs Inflation attributes as specified in XML file. 87 * @return Newly created item. Return null for the default behavior. 88 */ onCreateItem(String name, Context context, AttributeSet attrs)89 public T onCreateItem(String name, Context context, AttributeSet attrs); 90 } 91 92 private static class FactoryMerger<T> implements Factory<T> { 93 private final Factory<T> mF1, mF2; 94 FactoryMerger(Factory<T> f1, Factory<T> f2)95 FactoryMerger(Factory<T> f1, Factory<T> f2) { 96 mF1 = f1; 97 mF2 = f2; 98 } 99 onCreateItem(String name, Context context, AttributeSet attrs)100 public T onCreateItem(String name, Context context, AttributeSet attrs) { 101 T v = mF1.onCreateItem(name, context, attrs); 102 if (v != null) return v; 103 return mF2.onCreateItem(name, context, attrs); 104 } 105 } 106 107 /** 108 * Create a new inflater instance associated with a 109 * particular Context. 110 * 111 * @param context The Context in which this inflater will 112 * create its items; most importantly, this supplies the theme 113 * from which the default values for their attributes are 114 * retrieved. 115 */ GenericInflater(Context context)116 protected GenericInflater(Context context) { 117 mContext = context; 118 } 119 120 /** 121 * Create a new inflater instance that is a copy of an 122 * existing inflater, optionally with its Context 123 * changed. For use in implementing {@link #cloneInContext}. 124 * 125 * @param original The original inflater to copy. 126 * @param newContext The new Context to use. 127 */ GenericInflater(GenericInflater<T,P> original, Context newContext)128 protected GenericInflater(GenericInflater<T,P> original, Context newContext) { 129 mContext = newContext; 130 mFactory = original.mFactory; 131 } 132 133 /** 134 * Create a copy of the existing inflater object, with the copy 135 * pointing to a different Context than the original. This is used by 136 * {@link ContextThemeWrapper} to create a new inflater to go along 137 * with the new Context theme. 138 * 139 * @param newContext The new Context to associate with the new inflater. 140 * May be the same as the original Context if desired. 141 * 142 * @return Returns a brand spanking new inflater object associated with 143 * the given Context. 144 */ cloneInContext(Context newContext)145 public abstract GenericInflater cloneInContext(Context newContext); 146 147 /** 148 * Sets the default package that will be searched for classes to construct 149 * for tag names that have no explicit package. 150 * 151 * @param defaultPackage The default package. This will be prepended to the 152 * tag name, so it should end with a period. 153 */ setDefaultPackage(String defaultPackage)154 public void setDefaultPackage(String defaultPackage) { 155 mDefaultPackage = defaultPackage; 156 } 157 158 /** 159 * Returns the default package, or null if it is not set. 160 * 161 * @see #setDefaultPackage(String) 162 * @return The default package. 163 */ getDefaultPackage()164 public String getDefaultPackage() { 165 return mDefaultPackage; 166 } 167 168 /** 169 * Return the context we are running in, for access to resources, class 170 * loader, etc. 171 */ getContext()172 public Context getContext() { 173 return mContext; 174 } 175 176 /** 177 * Return the current factory (or null). This is called on each element 178 * name. If the factory returns an item, add that to the hierarchy. If it 179 * returns null, proceed to call onCreateItem(name). 180 */ getFactory()181 public final Factory<T> getFactory() { 182 return mFactory; 183 } 184 185 /** 186 * Attach a custom Factory interface for creating items while using this 187 * inflater. This must not be null, and can only be set 188 * once; after setting, you can not change the factory. This is called on 189 * each element name as the XML is parsed. If the factory returns an item, 190 * that is added to the hierarchy. If it returns null, the next factory 191 * default {@link #onCreateItem} method is called. 192 * <p> 193 * If you have an existing inflater and want to add your 194 * own factory to it, use {@link #cloneInContext} to clone the existing 195 * instance and then you can use this function (once) on the returned new 196 * instance. This will merge your own factory with whatever factory the 197 * original instance is using. 198 */ setFactory(Factory<T> factory)199 public void setFactory(Factory<T> factory) { 200 if (mFactorySet) { 201 throw new IllegalStateException("" + 202 "A factory has already been set on this inflater"); 203 } 204 if (factory == null) { 205 throw new NullPointerException("Given factory can not be null"); 206 } 207 mFactorySet = true; 208 if (mFactory == null) { 209 mFactory = factory; 210 } else { 211 mFactory = new FactoryMerger<T>(factory, mFactory); 212 } 213 } 214 215 216 /** 217 * Inflate a new item hierarchy from the specified xml resource. Throws 218 * InflaterException if there is an error. 219 * 220 * @param resource ID for an XML resource to load (e.g., 221 * <code>R.layout.main_page</code>) 222 * @param root Optional parent of the generated hierarchy. 223 * @return The root of the inflated hierarchy. If root was supplied, 224 * this is the root item; otherwise it is the root of the inflated 225 * XML file. 226 */ inflate(@mlRes int resource, P root)227 public T inflate(@XmlRes int resource, P root) { 228 return inflate(resource, root, root != null); 229 } 230 231 /** 232 * Inflate a new hierarchy from the specified xml node. Throws 233 * InflaterException if there is an error. * 234 * <p> 235 * <em><strong>Important</strong></em> For performance 236 * reasons, inflation relies heavily on pre-processing of XML files 237 * that is done at build time. Therefore, it is not currently possible to 238 * use inflater with an XmlPullParser over a plain XML file at runtime. 239 * 240 * @param parser XML dom node containing the description of the 241 * hierarchy. 242 * @param root Optional parent of the generated hierarchy. 243 * @return The root of the inflated hierarchy. If root was supplied, 244 * this is the that; otherwise it is the root of the inflated 245 * XML file. 246 */ inflate(XmlPullParser parser, P root)247 public T inflate(XmlPullParser parser, P root) { 248 return inflate(parser, root, root != null); 249 } 250 251 /** 252 * Inflate a new hierarchy from the specified xml resource. Throws 253 * InflaterException if there is an error. 254 * 255 * @param resource ID for an XML resource to load (e.g., 256 * <code>R.layout.main_page</code>) 257 * @param root Optional root to be the parent of the generated hierarchy (if 258 * <em>attachToRoot</em> is true), or else simply an object that 259 * provides a set of values for root of the returned 260 * hierarchy (if <em>attachToRoot</em> is false.) 261 * @param attachToRoot Whether the inflated hierarchy should be attached to 262 * the root parameter? 263 * @return The root of the inflated hierarchy. If root was supplied and 264 * attachToRoot is true, this is root; otherwise it is the root of 265 * the inflated XML file. 266 */ inflate(@mlRes int resource, P root, boolean attachToRoot)267 public T inflate(@XmlRes int resource, P root, boolean attachToRoot) { 268 if (DEBUG) System.out.println("INFLATING from resource: " + resource); 269 XmlResourceParser parser = getContext().getResources().getXml(resource); 270 try { 271 return inflate(parser, root, attachToRoot); 272 } finally { 273 parser.close(); 274 } 275 } 276 277 /** 278 * Inflate a new hierarchy from the specified XML node. Throws 279 * InflaterException if there is an error. 280 * <p> 281 * <em><strong>Important</strong></em> For performance 282 * reasons, inflation relies heavily on pre-processing of XML files 283 * that is done at build time. Therefore, it is not currently possible to 284 * use inflater with an XmlPullParser over a plain XML file at runtime. 285 * 286 * @param parser XML dom node containing the description of the 287 * hierarchy. 288 * @param root Optional to be the parent of the generated hierarchy (if 289 * <em>attachToRoot</em> is true), or else simply an object that 290 * provides a set of values for root of the returned 291 * hierarchy (if <em>attachToRoot</em> is false.) 292 * @param attachToRoot Whether the inflated hierarchy should be attached to 293 * the root parameter? 294 * @return The root of the inflated hierarchy. If root was supplied and 295 * attachToRoot is true, this is root; otherwise it is the root of 296 * the inflated XML file. 297 */ inflate(XmlPullParser parser, P root, boolean attachToRoot)298 public T inflate(XmlPullParser parser, P root, 299 boolean attachToRoot) { 300 synchronized (mConstructorArgs) { 301 final AttributeSet attrs = Xml.asAttributeSet(parser); 302 mConstructorArgs[0] = mContext; 303 T result = (T) root; 304 305 try { 306 // Look for the root node. 307 int type; 308 while ((type = parser.next()) != parser.START_TAG 309 && type != parser.END_DOCUMENT) { 310 ; 311 } 312 313 if (type != parser.START_TAG) { 314 throw new InflateException(parser.getPositionDescription() 315 + ": No start tag found!"); 316 } 317 318 if (DEBUG) { 319 System.out.println("**************************"); 320 System.out.println("Creating root: " 321 + parser.getName()); 322 System.out.println("**************************"); 323 } 324 // Temp is the root that was found in the xml 325 T xmlRoot = createItemFromTag(parser, parser.getName(), 326 attrs); 327 328 result = (T) onMergeRoots(root, attachToRoot, (P) xmlRoot); 329 330 if (DEBUG) { 331 System.out.println("-----> start inflating children"); 332 } 333 // Inflate all children under temp 334 rInflate(parser, result, attrs); 335 if (DEBUG) { 336 System.out.println("-----> done inflating children"); 337 } 338 339 } catch (InflateException e) { 340 throw e; 341 342 } catch (XmlPullParserException e) { 343 InflateException ex = new InflateException(e.getMessage()); 344 ex.initCause(e); 345 throw ex; 346 } catch (IOException e) { 347 InflateException ex = new InflateException( 348 parser.getPositionDescription() 349 + ": " + e.getMessage()); 350 ex.initCause(e); 351 throw ex; 352 } 353 354 return result; 355 } 356 } 357 358 /** 359 * Low-level function for instantiating by name. This attempts to 360 * instantiate class of the given <var>name</var> found in this 361 * inflater's ClassLoader. 362 * 363 * <p> 364 * There are two things that can happen in an error case: either the 365 * exception describing the error will be thrown, or a null will be 366 * returned. You must deal with both possibilities -- the former will happen 367 * the first time createItem() is called for a class of a particular name, 368 * the latter every time there-after for that class name. 369 * 370 * @param name The full name of the class to be instantiated. 371 * @param attrs The XML attributes supplied for this instance. 372 * 373 * @return The newly instantied item, or null. 374 */ createItem(String name, String prefix, AttributeSet attrs)375 public final T createItem(String name, String prefix, AttributeSet attrs) 376 throws ClassNotFoundException, InflateException { 377 Constructor constructor = (Constructor) sConstructorMap.get(name); 378 379 try { 380 if (null == constructor) { 381 // Class not found in the cache, see if it's real, 382 // and try to add it 383 Class clazz = mContext.getClassLoader().loadClass( 384 prefix != null ? (prefix + name) : name); 385 constructor = clazz.getConstructor(mConstructorSignature); 386 constructor.setAccessible(true); 387 sConstructorMap.put(name, constructor); 388 } 389 390 Object[] args = mConstructorArgs; 391 args[1] = attrs; 392 return (T) constructor.newInstance(args); 393 394 } catch (NoSuchMethodException e) { 395 InflateException ie = new InflateException(attrs 396 .getPositionDescription() 397 + ": Error inflating class " 398 + (prefix != null ? (prefix + name) : name)); 399 ie.initCause(e); 400 throw ie; 401 402 } catch (ClassNotFoundException e) { 403 // If loadClass fails, we should propagate the exception. 404 throw e; 405 } catch (Exception e) { 406 InflateException ie = new InflateException(attrs 407 .getPositionDescription() 408 + ": Error inflating class " 409 + constructor.getDeclaringClass().getName()); 410 ie.initCause(e); 411 throw ie; 412 } 413 } 414 415 /** 416 * This routine is responsible for creating the correct subclass of item 417 * given the xml element name. Override it to handle custom item objects. If 418 * you override this in your subclass be sure to call through to 419 * super.onCreateItem(name) for names you do not recognize. 420 * 421 * @param name The fully qualified class name of the item to be create. 422 * @param attrs An AttributeSet of attributes to apply to the item. 423 * @return The item created. 424 */ onCreateItem(String name, AttributeSet attrs)425 protected T onCreateItem(String name, AttributeSet attrs) throws ClassNotFoundException { 426 return createItem(name, mDefaultPackage, attrs); 427 } 428 createItemFromTag(XmlPullParser parser, String name, AttributeSet attrs)429 private final T createItemFromTag(XmlPullParser parser, String name, AttributeSet attrs) { 430 if (DEBUG) System.out.println("******** Creating item: " + name); 431 432 try { 433 T item = (mFactory == null) ? null : mFactory.onCreateItem(name, mContext, attrs); 434 435 if (item == null) { 436 if (-1 == name.indexOf('.')) { 437 item = onCreateItem(name, attrs); 438 } else { 439 item = createItem(name, null, attrs); 440 } 441 } 442 443 if (DEBUG) System.out.println("Created item is: " + item); 444 return item; 445 446 } catch (InflateException e) { 447 throw e; 448 449 } catch (ClassNotFoundException e) { 450 InflateException ie = new InflateException(attrs 451 .getPositionDescription() 452 + ": Error inflating class " + name); 453 ie.initCause(e); 454 throw ie; 455 456 } catch (Exception e) { 457 InflateException ie = new InflateException(attrs 458 .getPositionDescription() 459 + ": Error inflating class " + name); 460 ie.initCause(e); 461 throw ie; 462 } 463 } 464 465 /** 466 * Recursive method used to descend down the xml hierarchy and instantiate 467 * items, instantiate their children, and then call onFinishInflate(). 468 */ rInflate(XmlPullParser parser, T parent, final AttributeSet attrs)469 private void rInflate(XmlPullParser parser, T parent, final AttributeSet attrs) 470 throws XmlPullParserException, IOException { 471 final int depth = parser.getDepth(); 472 473 int type; 474 while (((type = parser.next()) != parser.END_TAG || 475 parser.getDepth() > depth) && type != parser.END_DOCUMENT) { 476 477 if (type != parser.START_TAG) { 478 continue; 479 } 480 481 if (onCreateCustomFromTag(parser, parent, attrs)) { 482 continue; 483 } 484 485 if (DEBUG) { 486 System.out.println("Now inflating tag: " + parser.getName()); 487 } 488 String name = parser.getName(); 489 490 T item = createItemFromTag(parser, name, attrs); 491 492 if (DEBUG) { 493 System.out 494 .println("Creating params from parent: " + parent); 495 } 496 497 ((P) parent).addItemFromInflater(item); 498 499 if (DEBUG) { 500 System.out.println("-----> start inflating children"); 501 } 502 rInflate(parser, item, attrs); 503 if (DEBUG) { 504 System.out.println("-----> done inflating children"); 505 } 506 } 507 508 } 509 510 /** 511 * Before this inflater tries to create an item from the tag, this method 512 * will be called. The parser will be pointing to the start of a tag, you 513 * must stop parsing and return when you reach the end of this element! 514 * 515 * @param parser XML dom node containing the description of the hierarchy. 516 * @param parent The item that should be the parent of whatever you create. 517 * @param attrs An AttributeSet of attributes to apply to the item. 518 * @return Whether you created a custom object (true), or whether this 519 * inflater should proceed to create an item. 520 */ onCreateCustomFromTag(XmlPullParser parser, T parent, final AttributeSet attrs)521 protected boolean onCreateCustomFromTag(XmlPullParser parser, T parent, 522 final AttributeSet attrs) throws XmlPullParserException { 523 return false; 524 } 525 onMergeRoots(P givenRoot, boolean attachToGivenRoot, P xmlRoot)526 protected P onMergeRoots(P givenRoot, boolean attachToGivenRoot, P xmlRoot) { 527 return xmlRoot; 528 } 529 } 530