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