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