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
17package android.graphics.drawable;
18
19import org.xmlpull.v1.XmlPullParser;
20import org.xmlpull.v1.XmlPullParserException;
21
22import android.annotation.DrawableRes;
23import android.annotation.NonNull;
24import android.annotation.Nullable;
25import android.content.Context;
26import android.content.res.Resources;
27import android.content.res.Resources.Theme;
28import android.util.AttributeSet;
29import android.view.InflateException;
30
31import java.io.IOException;
32import java.lang.reflect.Constructor;
33import 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 */
47public 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
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
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     */
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
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
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")
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
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