Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 847eb5ab authored by Geoffrey Pitsch's avatar Geoffrey Pitsch
Browse files

LayoutInflaterBuilder helper in systemui util

Useful for tests that want to inflate production layouts but with
some classes replaced with Testable/Mock instances.  Located in
SystemUI until more clients can be found.

Test: See ag/1630107
Change-Id: I4addb4cb4adfdf8c3e8f065dd3926574dc352e84
parent 014505ba
Loading
Loading
Loading
Loading
+162 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.util;

import android.annotation.NonNull;
import android.content.Context;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import java.util.Map;
import java.util.Set;

/**
 * Builder class to create a {@link LayoutInflater} with various properties.
 *
 * Call any desired configuration methods on the Builder and then use
 * {@link Builder#build} to create the LayoutInflater. This is an alternative to directly using
 * {@link LayoutInflater#setFilter} and {@link LayoutInflater#setFactory}.
 * @hide for use by framework
 */
public class LayoutInflaterBuilder {
    private static final String TAG = "LayoutInflaterBuilder";

    private Context mFromContext;
    private Context mTargetContext;
    private Map<String, String> mReplaceMap;
    private Set<Class> mDisallowedClasses;
    private LayoutInflater mBuiltInflater;

    /**
     * Creates a new Builder which will construct a LayoutInflater.
     *
     * @param fromContext This context's LayoutInflater will be cloned by the Builder using
     * {@link LayoutInflater#cloneInContext}. By default, the new LayoutInflater will point at
     * this same Context.
     */
    public LayoutInflaterBuilder(@NonNull Context fromContext) {
        mFromContext = fromContext;
        mTargetContext = fromContext;
        mReplaceMap = null;
        mDisallowedClasses = null;
        mBuiltInflater = null;
    }

    /**
     * Instructs the Builder to point the LayoutInflater at a different Context.
     *
     * @param targetContext Context to be provided to
     * {@link LayoutInflater#cloneInContext(Context)}.
     * @return Builder object post-modification.
     */
    public LayoutInflaterBuilder target(@NonNull Context targetContext) {
        assertIfAlreadyBuilt();
        mTargetContext = targetContext;
        return this;
    }

    /**
     * Instructs the Builder to configure the LayoutInflater such that all instances
     * of one {@link View} will be replaced with instances of another during inflation.
     *
     * @param from Instances of this class will be replaced during inflation.
     * @param to Instances of this class will be inflated as replacements.
     * @return Builder object post-modification.
     */
    public LayoutInflaterBuilder replace(@NonNull Class from, @NonNull Class to) {
        assertIfAlreadyBuilt();
        if (mReplaceMap == null) {
            mReplaceMap = new ArrayMap<String, String>();
        }
        mReplaceMap.put(from.getName(), to.getName());
        return this;
    }

    /**
     * Instructs the Builder to configure the LayoutInflater such that any attempt to inflate
     * a {@link View} of a given type will throw a {@link InflateException}.
     *
     * @param disallowedClass The Class type that will be disallowed.
     * @return Builder object post-modification.
     */
    public LayoutInflaterBuilder disallow(@NonNull Class disallowedClass) {
        assertIfAlreadyBuilt();
        if (mDisallowedClasses == null) {
            mDisallowedClasses = new ArraySet<Class>();
        }
        mDisallowedClasses.add(disallowedClass);
        return this;
    }

    /**
     * Builds and returns the LayoutInflater.  Afterwards, this Builder can no longer can be
     * used, all future calls on the Builder will throw {@link AssertionError}.
     */
    public LayoutInflater build() {
        assertIfAlreadyBuilt();
        mBuiltInflater =
                LayoutInflater.from(mFromContext).cloneInContext(mTargetContext);
        setFactoryIfNeeded(mBuiltInflater);
        setFilterIfNeeded(mBuiltInflater);
        return mBuiltInflater;
    }

    private void assertIfAlreadyBuilt() {
        if (mBuiltInflater != null) {
            throw new AssertionError("Cannot use this Builder after build() has been called.");
        }
    }

    private void setFactoryIfNeeded(LayoutInflater inflater) {
        if (mReplaceMap == null) {
            return;
        }
        inflater.setFactory(
                new LayoutInflater.Factory() {
                    @Override
                    public View onCreateView(String name, Context context, AttributeSet attrs) {
                        String replacingClassName = mReplaceMap.get(name);
                        if (replacingClassName != null) {
                            try {
                                return inflater.createView(replacingClassName, null, attrs);
                            } catch (ClassNotFoundException e) {
                                Log.e(TAG, "Could not replace " + name
                                        + " with " + replacingClassName
                                        + ", Exception: ", e);
                            }
                        }
                        return null;
                    }
                });
    }

    private void setFilterIfNeeded(LayoutInflater inflater) {
        if (mDisallowedClasses == null) {
            return;
        }
        inflater.setFilter(
                new LayoutInflater.Filter() {
                    @Override
                    public boolean onLoadClass(Class clazz) {
                        return !mDisallowedClasses.contains(clazz);
                    }
                });
    }
}