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        // Inner classes must be referenced as Outer$Inner, but XML tag names
116        // can't contain $, so the <drawable> tag allows developers to specify
117        // the class in an attribute. We'll still run it through inflateFromTag
118        // to stay consistent with how LayoutInflater works.
119        if (name.equals("drawable")) {
120            name = attrs.getAttributeValue(null, "class");
121            if (name == null) {
122                throw new InflateException("<drawable> tag must specify class attribute");
123            }
124        }
125
126        Drawable drawable = inflateFromTag(name);
127        if (drawable == null) {
128            drawable = inflateFromClass(name);
129        }
130        drawable.inflate(mRes, parser, attrs, theme);
131        return drawable;
132    }
133
134    @NonNull
135    @SuppressWarnings("deprecation")
136    private Drawable inflateFromTag(@NonNull String name) {
137        switch (name) {
138            case "selector":
139                return new StateListDrawable();
140            case "animated-selector":
141                return new AnimatedStateListDrawable();
142            case "level-list":
143                return new LevelListDrawable();
144            case "layer-list":
145                return new LayerDrawable();
146            case "transition":
147                return new TransitionDrawable();
148            case "ripple":
149                return new RippleDrawable();
150            case "color":
151                return new ColorDrawable();
152            case "shape":
153                return new GradientDrawable();
154            case "vector":
155                return new VectorDrawable();
156            case "animated-vector":
157                return new AnimatedVectorDrawable();
158            case "scale":
159                return new ScaleDrawable();
160            case "clip":
161                return new ClipDrawable();
162            case "rotate":
163                return new RotateDrawable();
164            case "animated-rotate":
165                return new AnimatedRotateDrawable();
166            case "animation-list":
167                return new AnimationDrawable();
168            case "inset":
169                return new InsetDrawable();
170            case "bitmap":
171                return new BitmapDrawable();
172            case "nine-patch":
173                return new NinePatchDrawable();
174            default:
175                return null;
176        }
177    }
178
179    @NonNull
180    private Drawable inflateFromClass(@NonNull String className) {
181        try {
182            Constructor<? extends Drawable> constructor;
183            synchronized (CONSTRUCTOR_MAP) {
184                constructor = CONSTRUCTOR_MAP.get(className);
185                if (constructor == null) {
186                    final Class<? extends Drawable> clazz =
187                            mClassLoader.loadClass(className).asSubclass(Drawable.class);
188                    constructor = clazz.getConstructor();
189                    CONSTRUCTOR_MAP.put(className, constructor);
190                }
191            }
192            return constructor.newInstance();
193        } catch (NoSuchMethodException e) {
194            final InflateException ie = new InflateException(
195                    "Error inflating class " + className);
196            ie.initCause(e);
197            throw ie;
198        } catch (ClassCastException e) {
199            // If loaded class is not a Drawable subclass.
200            final InflateException ie = new InflateException(
201                    "Class is not a Drawable " + className);
202            ie.initCause(e);
203            throw ie;
204        } catch (ClassNotFoundException e) {
205            // If loadClass fails, we should propagate the exception.
206            final InflateException ie = new InflateException(
207                    "Class not found " + className);
208            ie.initCause(e);
209            throw ie;
210        } catch (Exception e) {
211            final InflateException ie = new InflateException(
212                    "Error inflating class " + className);
213            ie.initCause(e);
214            throw ie;
215        }
216    }
217}
218