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