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 
15 package android.testing;
16 
17 import android.annotation.NonNull;
18 import android.content.Context;
19 import android.util.ArrayMap;
20 import android.util.ArraySet;
21 import android.util.AttributeSet;
22 import android.util.Log;
23 import android.view.LayoutInflater;
24 import android.view.View;
25 import java.util.Map;
26 import 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  */
36 public 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      */
LayoutInflaterBuilder(@onNull Context fromContext)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      */
target(@onNull Context targetContext)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      */
replace(@onNull Class from, @NonNull Class to)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      */
replace(@onNull String tag, @NonNull Class to)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      */
disallow(@onNull Class disallowedClass)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      */
build()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 
assertIfAlreadyBuilt()131     private void assertIfAlreadyBuilt() {
132         if (mBuiltInflater != null) {
133             throw new AssertionError("Cannot use this Builder after build() has been called.");
134         }
135     }
136 
setFactoryIfNeeded(LayoutInflater inflater)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 
setFilterIfNeeded(LayoutInflater inflater)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