Home | History | Annotate | Download | only in testing
      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      */
     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