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 17package com.android.setupwizardlib.items; 18 19import java.io.IOException; 20import java.lang.reflect.Constructor; 21import java.util.HashMap; 22 23import org.xmlpull.v1.XmlPullParser; 24import org.xmlpull.v1.XmlPullParserException; 25 26import android.content.Context; 27import android.content.res.XmlResourceParser; 28import android.util.AttributeSet; 29import android.util.Log; 30import android.util.Xml; 31import android.view.ContextThemeWrapper; 32import android.view.InflateException; 33 34/** 35 * Generic XML inflater. This class is modeled after {@code android.preference.GenericInflater}, 36 * which is in turn modeled after {@code LayoutInflater}. This can be used to recursively inflate a 37 * hierarchy of items. All items in the hierarchy must inherit the generic type {@code T}, and the 38 * specific implementation is expected to handle inserting child items into the parent, by 39 * implementing {@link #onAddChildItem(Object, Object)}. 40 * 41 * @param <T> Type of the items to inflate 42 */ 43public abstract class GenericInflater<T> { 44 45 private static final String TAG = "GenericInflater"; 46 private static 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<String, Constructor<?>> sConstructorMap = new HashMap<>(); 60 61 private String mDefaultPackage; 62 63 public interface Factory<T> { 64 /** 65 * Hook you can supply that is called when inflating from a 66 * inflater. You can use this to customize the tag 67 * names available in your XML files. 68 * <p> 69 * Note that it is good practice to prefix these custom names with your 70 * package (i.e., com.coolcompany.apps) to avoid conflicts with system 71 * names. 72 * 73 * @param name Tag name to be inflated. 74 * @param context The context the item is being created in. 75 * @param attrs Inflation attributes as specified in XML file. 76 * @return Newly created item. Return null for the default behavior. 77 */ 78 T onCreateItem(String name, Context context, AttributeSet attrs); 79 } 80 81 private static class FactoryMerger<T> implements Factory<T> { 82 private final Factory<T> mF1, mF2; 83 84 FactoryMerger(Factory<T> f1, Factory<T> f2) { 85 mF1 = f1; 86 mF2 = f2; 87 } 88 89 public T onCreateItem(String name, Context context, AttributeSet attrs) { 90 T v = mF1.onCreateItem(name, context, attrs); 91 if (v != null) return v; 92 return mF2.onCreateItem(name, context, attrs); 93 } 94 } 95 96 /** 97 * Create a new inflater instance associated with a 98 * particular Context. 99 * 100 * @param context The Context in which this inflater will 101 * create its items; most importantly, this supplies the theme 102 * from which the default values for their attributes are 103 * retrieved. 104 */ 105 protected GenericInflater(Context context) { 106 mContext = context; 107 } 108 109 /** 110 * Create a new inflater instance that is a copy of an 111 * existing inflater, optionally with its Context 112 * changed. For use in implementing {@link #cloneInContext}. 113 * 114 * @param original The original inflater to copy. 115 * @param newContext The new Context to use. 116 */ 117 protected GenericInflater(GenericInflater<T> original, Context newContext) { 118 mContext = newContext; 119 mFactory = original.mFactory; 120 } 121 122 /** 123 * Create a copy of the existing inflater object, with the copy 124 * pointing to a different Context than the original. This is used by 125 * {@link ContextThemeWrapper} to create a new inflater to go along 126 * with the new Context theme. 127 * 128 * @param newContext The new Context to associate with the new inflater. 129 * May be the same as the original Context if desired. 130 * 131 * @return Returns a brand spanking new inflater object associated with 132 * the given Context. 133 */ 134 public abstract GenericInflater cloneInContext(Context newContext); 135 136 /** 137 * Sets the default package that will be searched for classes to construct 138 * for tag names that have no explicit package. 139 * 140 * @param defaultPackage The default package. This will be prepended to the 141 * tag name, so it should end with a period. 142 */ 143 public void setDefaultPackage(String defaultPackage) { 144 mDefaultPackage = defaultPackage; 145 } 146 147 /** 148 * Returns the default package, or null if it is not set. 149 * 150 * @see #setDefaultPackage(String) 151 * @return The default package. 152 */ 153 public String getDefaultPackage() { 154 return mDefaultPackage; 155 } 156 157 /** 158 * Return the context we are running in, for access to resources, class 159 * loader, etc. 160 */ 161 public Context getContext() { 162 return mContext; 163 } 164 165 /** 166 * Return the current factory (or null). This is called on each element 167 * name. If the factory returns an item, add that to the hierarchy. If it 168 * returns null, proceed to call onCreateItem(name). 169 */ 170 public final Factory<T> getFactory() { 171 return mFactory; 172 } 173 174 /** 175 * Attach a custom Factory interface for creating items while using this 176 * inflater. This must not be null, and can only be set 177 * once; after setting, you can not change the factory. This is called on 178 * each element name as the XML is parsed. If the factory returns an item, 179 * that is added to the hierarchy. If it returns null, the next factory 180 * default {@link #onCreateItem} method is called. 181 * <p> 182 * If you have an existing inflater and want to add your 183 * own factory to it, use {@link #cloneInContext} to clone the existing 184 * instance and then you can use this function (once) on the returned new 185 * instance. This will merge your own factory with whatever factory the 186 * original instance is using. 187 */ 188 public void setFactory(Factory<T> factory) { 189 if (mFactorySet) { 190 throw new IllegalStateException("" + 191 "A factory has already been set on this inflater"); 192 } 193 if (factory == null) { 194 throw new NullPointerException("Given factory can not be null"); 195 } 196 mFactorySet = true; 197 if (mFactory == null) { 198 mFactory = factory; 199 } else { 200 mFactory = new FactoryMerger<>(factory, mFactory); 201 } 202 } 203 204 public T inflate(int resource) { 205 return inflate(resource, null); 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 */ 220 public T inflate(int resource, T 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 */ 240 public T inflate(XmlPullParser parser, T 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 */ 260 public T inflate(int resource, T root, boolean attachToRoot) { 261 if (DEBUG) Log.v(TAG, "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 */ 291 public T inflate(XmlPullParser parser, T root, boolean attachToRoot) { 292 synchronized (mConstructorArgs) { 293 final AttributeSet attrs = Xml.asAttributeSet(parser); 294 mConstructorArgs[0] = mContext; 295 T result; 296 297 try { 298 // Look for the root node. 299 int type; 300 while ((type = parser.next()) != XmlPullParser.START_TAG 301 && type != XmlPullParser.END_DOCUMENT) { 302 } 303 304 if (type != XmlPullParser.START_TAG) { 305 throw new InflateException(parser.getPositionDescription() 306 + ": No start tag found!"); 307 } 308 309 if (DEBUG) { 310 Log.v(TAG, "**************************"); 311 Log.v(TAG, "Creating root: " 312 + parser.getName()); 313 Log.v(TAG, "**************************"); 314 } 315 // Temp is the root that was found in the xml 316 T xmlRoot = createItemFromTag(parser, parser.getName(), attrs); 317 318 result = onMergeRoots(root, attachToRoot, xmlRoot); 319 320 if (DEBUG) Log.v(TAG, "-----> start inflating children"); 321 // Inflate all children under temp 322 rInflate(parser, result, attrs); 323 if (DEBUG) Log.v(TAG, "-----> done inflating children"); 324 } catch (XmlPullParserException e) { 325 InflateException ex = new InflateException(e.getMessage()); 326 ex.initCause(e); 327 throw ex; 328 } catch (IOException e) { 329 InflateException ex = new InflateException( 330 parser.getPositionDescription() 331 + ": " + e.getMessage()); 332 ex.initCause(e); 333 throw ex; 334 } 335 336 return result; 337 } 338 } 339 340 /** 341 * Low-level function for instantiating by name. This attempts to 342 * instantiate class of the given <var>name</var> found in this 343 * inflater's ClassLoader. 344 * 345 * <p> 346 * There are two things that can happen in an error case: either the 347 * exception describing the error will be thrown, or a null will be 348 * returned. You must deal with both possibilities -- the former will happen 349 * the first time createItem() is called for a class of a particular name, 350 * the latter every time there-after for that class name. 351 * 352 * @param name The full name of the class to be instantiated. 353 * @param attrs The XML attributes supplied for this instance. 354 * 355 * @return The newly instantiated item, or null. 356 */ 357 public final T createItem(String name, String prefix, AttributeSet attrs) 358 throws ClassNotFoundException, InflateException { 359 Constructor constructor = sConstructorMap.get(name); 360 361 try { 362 if (constructor == null) { 363 // Class not found in the cache, see if it's real, 364 // and try to add it 365 Class<?> clazz = mContext.getClassLoader().loadClass( 366 prefix != null ? (prefix + name) : name); 367 constructor = clazz.getConstructor(mConstructorSignature); 368 constructor.setAccessible(true); 369 sConstructorMap.put(name, constructor); 370 } 371 372 Object[] args = mConstructorArgs; 373 args[1] = attrs; 374 //noinspection unchecked 375 return (T) constructor.newInstance(args); 376 } catch (NoSuchMethodException e) { 377 InflateException ie = new InflateException(attrs.getPositionDescription() 378 + ": Error inflating class " 379 + (prefix != null ? (prefix + name) : name)); 380 ie.initCause(e); 381 throw ie; 382 383 } catch (ClassNotFoundException e) { 384 // If loadClass fails, we should propagate the exception. 385 throw e; 386 } catch (Exception e) { 387 InflateException ie = new InflateException(attrs.getPositionDescription() 388 + ": Error inflating class " 389 + (prefix != null ? (prefix + name) : name)); 390 ie.initCause(e); 391 throw ie; 392 } 393 } 394 395 /** 396 * This routine is responsible for creating the correct subclass of item 397 * given the xml element name. Override it to handle custom item objects. If 398 * you override this in your subclass be sure to call through to 399 * super.onCreateItem(name) for names you do not recognize. 400 * 401 * @param name The fully qualified class name of the item to be create. 402 * @param attrs An AttributeSet of attributes to apply to the item. 403 * @return The item created. 404 */ 405 protected T onCreateItem(String name, AttributeSet attrs) throws ClassNotFoundException { 406 return createItem(name, mDefaultPackage, attrs); 407 } 408 409 private T createItemFromTag(XmlPullParser parser, String name, AttributeSet attrs) { 410 if (DEBUG) Log.v(TAG, "******** Creating item: " + name); 411 412 try { 413 T item = (mFactory == null) ? null : mFactory.onCreateItem(name, mContext, attrs); 414 415 if (item == null) { 416 if (-1 == name.indexOf('.')) { 417 item = onCreateItem(name, attrs); 418 } else { 419 item = createItem(name, null, attrs); 420 } 421 } 422 423 if (DEBUG) Log.v(TAG, "Created item is: " + item); 424 return item; 425 426 } catch (InflateException e) { 427 throw e; 428 429 } catch (Exception e) { 430 InflateException ie = new InflateException(attrs 431 .getPositionDescription() 432 + ": Error inflating class " + name); 433 ie.initCause(e); 434 throw ie; 435 } 436 } 437 438 /** 439 * Recursive method used to descend down the xml hierarchy and instantiate 440 * items, instantiate their children, and then call onFinishInflate(). 441 */ 442 private void rInflate(XmlPullParser parser, T node, final AttributeSet attrs) 443 throws XmlPullParserException, IOException { 444 final int depth = parser.getDepth(); 445 446 int type; 447 while (((type = parser.next()) != XmlPullParser.END_TAG || 448 parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { 449 450 if (type != XmlPullParser.START_TAG) { 451 continue; 452 } 453 454 if (onCreateCustomFromTag(parser, node, attrs)) { 455 continue; 456 } 457 458 if (DEBUG) Log.v(TAG, "Now inflating tag: " + parser.getName()); 459 String name = parser.getName(); 460 461 T item = createItemFromTag(parser, name, attrs); 462 463 if (DEBUG) Log.v(TAG, "Creating params from parent: " + node); 464 465 onAddChildItem(node, item); 466 467 468 if (DEBUG) Log.v(TAG, "-----> start inflating children"); 469 rInflate(parser, item, attrs); 470 if (DEBUG) Log.v(TAG, "-----> done inflating children"); 471 } 472 } 473 474 /** 475 * Before this inflater tries to create an item from the tag, this method 476 * will be called. The parser will be pointing to the start of a tag, you 477 * must stop parsing and return when you reach the end of this element! 478 * 479 * @param parser XML dom node containing the description of the hierarchy. 480 * @param node The item that should be the parent of whatever you create. 481 * @param attrs An AttributeSet of attributes to apply to the item. 482 * @return Whether you created a custom object (true), or whether this 483 * inflater should proceed to create an item. 484 */ 485 protected boolean onCreateCustomFromTag(XmlPullParser parser, T node, 486 final AttributeSet attrs) throws XmlPullParserException { 487 return false; 488 } 489 490 protected abstract void onAddChildItem(T parent, T child); 491 492 protected T onMergeRoots(T givenRoot, boolean attachToGivenRoot, T xmlRoot) { 493 return xmlRoot; 494 } 495} 496