1 /*
2  * Copyright (C) 2015 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.graphics.drawable;
18 
19 import org.xmlpull.v1.XmlPullParser;
20 import org.xmlpull.v1.XmlPullParserException;
21 
22 import android.annotation.DrawableRes;
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.content.Context;
26 import android.content.res.Resources;
27 import android.content.res.Resources.Theme;
28 import android.util.AttributeSet;
29 import android.view.InflateException;
30 
31 import java.io.IOException;
32 import java.lang.reflect.Constructor;
33 import java.util.HashMap;
34 
35 /**
36  * Instantiates a drawable XML file into its corresponding
37  * {@link android.graphics.drawable.Drawable} objects.
38  * <p>
39  * For performance reasons, inflation relies heavily on pre-processing of
40  * XML files that is done at build time. Therefore, it is not currently possible
41  * to use this inflater with an XmlPullParser over a plain XML file at runtime;
42  * it only works with an XmlPullParser returned from a compiled resource (R.
43  * <em>something</em> file.)
44  *
45  * @hide Pending API finalization.
46  */
47 public final class DrawableInflater {
48     private static final HashMap<String, Constructor<? extends Drawable>> CONSTRUCTOR_MAP =
49             new HashMap<>();
50 
51     private final Resources mRes;
52     private final ClassLoader mClassLoader;
53 
54     /**
55      * Loads the drawable resource with the specified identifier.
56      *
57      * @param context the context in which the drawable should be loaded
58      * @param id the identifier of the drawable resource
59      * @return a drawable, or {@code null} if the drawable failed to load
60      */
61     @Nullable
loadDrawable(@onNull Context context, @DrawableRes int id)62     public static Drawable loadDrawable(@NonNull Context context, @DrawableRes int id) {
63         return loadDrawable(context.getResources(), context.getTheme(), id);
64     }
65 
66     /**
67      * Loads the drawable resource with the specified identifier.
68      *
69      * @param resources the resources from which the drawable should be loaded
70      * @param theme the theme against which the drawable should be inflated
71      * @param id the identifier of the drawable resource
72      * @return a drawable, or {@code null} if the drawable failed to load
73      */
74     @Nullable
loadDrawable( @onNull Resources resources, @Nullable Theme theme, @DrawableRes int id)75     public static Drawable loadDrawable(
76             @NonNull Resources resources, @Nullable Theme theme, @DrawableRes int id) {
77         return resources.getDrawable(id, theme);
78     }
79 
80     /**
81      * Constructs a new drawable inflater using the specified resources and
82      * class loader.
83      *
84      * @param res the resources used to resolve resource identifiers
85      * @param classLoader the class loader used to load custom drawables
86      * @hide
87      */
DrawableInflater(@onNull Resources res, @NonNull ClassLoader classLoader)88     public DrawableInflater(@NonNull Resources res, @NonNull ClassLoader classLoader) {
89         mRes = res;
90         mClassLoader = classLoader;
91     }
92 
93     /**
94      * Inflates a drawable from inside an XML document using an optional
95      * {@link Theme}.
96      * <p>
97      * This method should be called on a parser positioned at a tag in an XML
98      * document defining a drawable resource. It will attempt to create a
99      * Drawable from the tag at the current position.
100      *
101      * @param name the name of the tag at the current position
102      * @param parser an XML parser positioned at the drawable tag
103      * @param attrs an attribute set that wraps the parser
104      * @param theme the theme against which the drawable should be inflated, or
105      *              {@code null} to not inflate against a theme
106      * @return a drawable
107      *
108      * @throws XmlPullParserException
109      * @throws IOException
110      */
111     @NonNull
inflateFromXml(@onNull String name, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)112     public Drawable inflateFromXml(@NonNull String name, @NonNull XmlPullParser parser,
113             @NonNull AttributeSet attrs, @Nullable Theme theme)
114             throws XmlPullParserException, IOException {
115         return inflateFromXmlForDensity(name, parser, attrs, 0, theme);
116     }
117 
118     /**
119      * Version of {@link #inflateFromXml(String, XmlPullParser, AttributeSet, Theme)} that accepts
120      * an override density.
121      */
122     @NonNull
inflateFromXmlForDensity(@onNull String name, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, int density, @Nullable Theme theme)123     Drawable inflateFromXmlForDensity(@NonNull String name, @NonNull XmlPullParser parser,
124             @NonNull AttributeSet attrs, int density, @Nullable Theme theme)
125             throws XmlPullParserException, IOException {
126         // Inner classes must be referenced as Outer$Inner, but XML tag names
127         // can't contain $, so the <drawable> tag allows developers to specify
128         // the class in an attribute. We'll still run it through inflateFromTag
129         // to stay consistent with how LayoutInflater works.
130         if (name.equals("drawable")) {
131             name = attrs.getAttributeValue(null, "class");
132             if (name == null) {
133                 throw new InflateException("<drawable> tag must specify class attribute");
134             }
135         }
136 
137         Drawable drawable = inflateFromTag(name);
138         if (drawable == null) {
139             drawable = inflateFromClass(name);
140         }
141         drawable.setSrcDensityOverride(density);
142         drawable.inflate(mRes, parser, attrs, theme);
143         return drawable;
144     }
145 
146     @NonNull
147     @SuppressWarnings("deprecation")
inflateFromTag(@onNull String name)148     private Drawable inflateFromTag(@NonNull String name) {
149         switch (name) {
150             case "selector":
151                 return new StateListDrawable();
152             case "animated-selector":
153                 return new AnimatedStateListDrawable();
154             case "level-list":
155                 return new LevelListDrawable();
156             case "layer-list":
157                 return new LayerDrawable();
158             case "transition":
159                 return new TransitionDrawable();
160             case "ripple":
161                 return new RippleDrawable();
162             case "adaptive-icon":
163                 return new AdaptiveIconDrawable();
164             case "color":
165                 return new ColorDrawable();
166             case "shape":
167                 return new GradientDrawable();
168             case "vector":
169                 return new VectorDrawable();
170             case "animated-vector":
171                 return new AnimatedVectorDrawable();
172             case "scale":
173                 return new ScaleDrawable();
174             case "clip":
175                 return new ClipDrawable();
176             case "rotate":
177                 return new RotateDrawable();
178             case "animated-rotate":
179                 return new AnimatedRotateDrawable();
180             case "animation-list":
181                 return new AnimationDrawable();
182             case "inset":
183                 return new InsetDrawable();
184             case "bitmap":
185                 return new BitmapDrawable();
186             case "nine-patch":
187                 return new NinePatchDrawable();
188             default:
189                 return null;
190         }
191     }
192 
193     @NonNull
inflateFromClass(@onNull String className)194     private Drawable inflateFromClass(@NonNull String className) {
195         try {
196             Constructor<? extends Drawable> constructor;
197             synchronized (CONSTRUCTOR_MAP) {
198                 constructor = CONSTRUCTOR_MAP.get(className);
199                 if (constructor == null) {
200                     final Class<? extends Drawable> clazz =
201                             mClassLoader.loadClass(className).asSubclass(Drawable.class);
202                     constructor = clazz.getConstructor();
203                     CONSTRUCTOR_MAP.put(className, constructor);
204                 }
205             }
206             return constructor.newInstance();
207         } catch (NoSuchMethodException e) {
208             final InflateException ie = new InflateException(
209                     "Error inflating class " + className);
210             ie.initCause(e);
211             throw ie;
212         } catch (ClassCastException e) {
213             // If loaded class is not a Drawable subclass.
214             final InflateException ie = new InflateException(
215                     "Class is not a Drawable " + className);
216             ie.initCause(e);
217             throw ie;
218         } catch (ClassNotFoundException e) {
219             // If loadClass fails, we should propagate the exception.
220             final InflateException ie = new InflateException(
221                     "Class not found " + className);
222             ie.initCause(e);
223             throw ie;
224         } catch (Exception e) {
225             final InflateException ie = new InflateException(
226                     "Error inflating class " + className);
227             ie.initCause(e);
228             throw ie;
229         }
230     }
231 }
232