ReflectionInflater.java revision 9ac3d54040b4dae6addc224fd8a69bf62ee84a83
1/*
2 * Copyright (C) 2017 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 com.android.setupwizardlib.items;
18
19import android.content.Context;
20import android.support.annotation.NonNull;
21import android.support.annotation.Nullable;
22import android.util.AttributeSet;
23import android.view.InflateException;
24
25import java.lang.reflect.Constructor;
26import java.util.HashMap;
27
28/**
29 * An XML inflater that creates items by reading the tag as a class name, and constructs said class
30 * by invoking the 2-argument constructor {@code Constructor(Context, AttributeSet)} via reflection.
31 *
32 * <p>Optionally a "default package" can be specified so that for unqualified tag names (i.e. names
33 * that do not contain "."), the default package will be prefixed onto the tag.
34 *
35 * @param <T> The class where all instances (including child elements) belong to. If parent and
36 *     child elements belong to different class hierarchies, it's OK to set this to {@link Object}.
37 */
38public abstract class ReflectionInflater<T> extends SimpleInflater<T> {
39
40    /* static section */
41
42    private static final Class<?>[] CONSTRUCTOR_SIGNATURE =
43            new Class<?>[] {Context.class, AttributeSet.class};
44
45    private static final HashMap<String, Constructor<?>> sConstructorMap = new HashMap<>();
46
47    /* non-static section */
48
49    // Array used to contain the constructor arguments (Context, AttributeSet), to avoid allocating
50    // a new array for creation of every item.
51    private final Object[] mTempConstructorArgs = new Object[2];
52
53    @Nullable
54    private String mDefaultPackage;
55
56    @NonNull
57    private final Context mContext;
58
59    /**
60     * Create a new inflater instance associated with a particular Context.
61     *
62     * @param context The context used to resolve resource IDs. This context is also passed to the
63     *     constructor of the items created as the first argument.
64     */
65    protected ReflectionInflater(@NonNull Context context) {
66        super(context.getResources());
67        mContext = context;
68    }
69
70    @NonNull
71    public Context getContext() {
72        return mContext;
73    }
74
75    /**
76     * Instantiate the class by name. This attempts to instantiate class of the given {@code name}
77     * found in this inflater's ClassLoader.
78     *
79     * @param tagName The full name of the class to be instantiated.
80     * @param attrs The XML attributes supplied for this instance.
81     *
82     * @return The newly instantiated item.
83     */
84    @NonNull
85    public final T createItem(String tagName, String prefix, AttributeSet attrs) {
86        String qualifiedName = tagName;
87        if (prefix != null && qualifiedName.indexOf('.') == -1) {
88            qualifiedName = prefix.concat(qualifiedName);
89        }
90        @SuppressWarnings("unchecked") // qualifiedName should correspond to a subclass of T
91        Constructor<? extends T> constructor =
92                (Constructor<? extends T>) sConstructorMap.get(qualifiedName);
93
94        try {
95            if (constructor == null) {
96                // Class not found in the cache, see if it's real, and try to add it
97                @SuppressWarnings("unchecked") // qualifiedName should correspond to a subclass of T
98                Class<? extends T> clazz =
99                        (Class<? extends T>) mContext.getClassLoader().loadClass(qualifiedName);
100                constructor = clazz.getConstructor(CONSTRUCTOR_SIGNATURE);
101                constructor.setAccessible(true);
102                sConstructorMap.put(tagName, constructor);
103            }
104
105            mTempConstructorArgs[0] = mContext;
106            mTempConstructorArgs[1] = attrs;
107            final T item = constructor.newInstance(mTempConstructorArgs);
108            mTempConstructorArgs[0] = null;
109            mTempConstructorArgs[1] = null;
110            return item;
111        } catch (Exception e) {
112            throw new InflateException(attrs.getPositionDescription()
113                    + ": Error inflating class " + qualifiedName, e);
114        }
115    }
116
117    @Override
118    protected T onCreateItem(String tagName, AttributeSet attrs) {
119        return createItem(tagName, mDefaultPackage, attrs);
120    }
121
122    /**
123     * Sets the default package that will be searched for classes to construct for tag names that
124     * have no explicit package.
125     *
126     * @param defaultPackage The default package. This will be prepended to the tag name, so it
127     *     should end with a period.
128     */
129    public void setDefaultPackage(@Nullable String defaultPackage) {
130        mDefaultPackage = defaultPackage;
131    }
132
133    /**
134     * Returns the default package, or null if it is not set.
135     *
136     * @see #setDefaultPackage(String)
137     * @return The default package.
138     */
139    @Nullable
140    public String getDefaultPackage() {
141        return mDefaultPackage;
142    }
143}
144