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