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