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