LayoutInflaterBuilder.java revision 340b0e5216b4fcc435e0459b1ca46155a572100d
1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5 * except in compliance with the License. You may obtain a copy of the License at
6 *
7 *      http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the
10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11 * KIND, either express or implied. See the License for the specific language governing
12 * permissions and limitations under the License.
13 */
14
15package android.testing;
16
17import android.annotation.NonNull;
18import android.content.Context;
19import android.util.ArrayMap;
20import android.util.ArraySet;
21import android.util.AttributeSet;
22import android.util.Log;
23import android.view.LayoutInflater;
24import android.view.View;
25import java.util.Map;
26import java.util.Set;
27
28/**
29 * Builder class to create a {@link LayoutInflater} with various properties.
30 *
31 * Call any desired configuration methods on the Builder and then use
32 * {@link Builder#build} to create the LayoutInflater. This is an alternative to directly using
33 * {@link LayoutInflater#setFilter} and {@link LayoutInflater#setFactory}.
34 * @hide for use by framework
35 */
36public class LayoutInflaterBuilder {
37    private static final String TAG = "LayoutInflaterBuilder";
38
39    private Context mFromContext;
40    private Context mTargetContext;
41    private Map<String, String> mReplaceMap;
42    private Set<Class> mDisallowedClasses;
43    private LayoutInflater mBuiltInflater;
44
45    /**
46     * Creates a new Builder which will construct a LayoutInflater.
47     *
48     * @param fromContext This context's LayoutInflater will be cloned by the Builder using
49     * {@link LayoutInflater#cloneInContext}. By default, the new LayoutInflater will point at
50     * this same Context.
51     */
52    public LayoutInflaterBuilder(@NonNull Context fromContext) {
53        mFromContext = fromContext;
54        mTargetContext = fromContext;
55        mReplaceMap = null;
56        mDisallowedClasses = null;
57        mBuiltInflater = null;
58    }
59
60    /**
61     * Instructs the Builder to point the LayoutInflater at a different Context.
62     *
63     * @param targetContext Context to be provided to
64     * {@link LayoutInflater#cloneInContext(Context)}.
65     * @return Builder object post-modification.
66     */
67    public LayoutInflaterBuilder target(@NonNull Context targetContext) {
68        assertIfAlreadyBuilt();
69        mTargetContext = targetContext;
70        return this;
71    }
72
73    /**
74     * Instructs the Builder to configure the LayoutInflater such that all instances
75     * of one {@link View} will be replaced with instances of another during inflation.
76     *
77     * @param from Instances of this class will be replaced during inflation.
78     * @param to Instances of this class will be inflated as replacements.
79     * @return Builder object post-modification.
80     */
81    public LayoutInflaterBuilder replace(@NonNull Class from, @NonNull Class to) {
82        return replace(from.getName(), to);
83    }
84
85    /**
86     * Instructs the Builder to configure the LayoutInflater such that all instances
87     * of one {@link View} will be replaced with instances of another during inflation.
88     *
89     * @param tag Instances of this tag will be replaced during inflation.
90     * @param to Instances of this class will be inflated as replacements.
91     * @return Builder object post-modification.
92     */
93    public LayoutInflaterBuilder replace(@NonNull String tag, @NonNull Class to) {
94        assertIfAlreadyBuilt();
95        if (mReplaceMap == null) {
96            mReplaceMap = new ArrayMap<String, String>();
97        }
98        mReplaceMap.put(tag, to.getName());
99        return this;
100    }
101
102    /**
103     * Instructs the Builder to configure the LayoutInflater such that any attempt to inflate
104     * a {@link View} of a given type will throw a {@link InflateException}.
105     *
106     * @param disallowedClass The Class type that will be disallowed.
107     * @return Builder object post-modification.
108     */
109    public LayoutInflaterBuilder disallow(@NonNull Class disallowedClass) {
110        assertIfAlreadyBuilt();
111        if (mDisallowedClasses == null) {
112            mDisallowedClasses = new ArraySet<Class>();
113        }
114        mDisallowedClasses.add(disallowedClass);
115        return this;
116    }
117
118    /**
119     * Builds and returns the LayoutInflater.  Afterwards, this Builder can no longer can be
120     * used, all future calls on the Builder will throw {@link AssertionError}.
121     */
122    public LayoutInflater build() {
123        assertIfAlreadyBuilt();
124        mBuiltInflater =
125                LayoutInflater.from(mFromContext).cloneInContext(mTargetContext);
126        setFactoryIfNeeded(mBuiltInflater);
127        setFilterIfNeeded(mBuiltInflater);
128        return mBuiltInflater;
129    }
130
131    private void assertIfAlreadyBuilt() {
132        if (mBuiltInflater != null) {
133            throw new AssertionError("Cannot use this Builder after build() has been called.");
134        }
135    }
136
137    private void setFactoryIfNeeded(LayoutInflater inflater) {
138        if (mReplaceMap == null) {
139            return;
140        }
141        inflater.setFactory(
142                new LayoutInflater.Factory() {
143                    @Override
144                    public View onCreateView(String name, Context context, AttributeSet attrs) {
145                        String replacingClassName = mReplaceMap.get(name);
146                        if (replacingClassName != null) {
147                            try {
148                                return inflater.createView(replacingClassName, null, attrs);
149                            } catch (ClassNotFoundException e) {
150                                Log.e(TAG, "Could not replace " + name
151                                        + " with " + replacingClassName
152                                        + ", Exception: ", e);
153                            }
154                        }
155                        return null;
156                    }
157                });
158    }
159
160    private void setFilterIfNeeded(LayoutInflater inflater) {
161        if (mDisallowedClasses == null) {
162            return;
163        }
164        inflater.setFilter(
165                new LayoutInflater.Filter() {
166                    @Override
167                    public boolean onLoadClass(Class clazz) {
168                        return !mDisallowedClasses.contains(clazz);
169                    }
170                });
171    }
172}
173