LayoutIncludeDetector.java

/*
 * Copyright 2021 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 androidx.appcompat.app;

import android.util.AttributeSet;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.lang.ref.WeakReference;
import java.util.ArrayDeque;
import java.util.Deque;

/**
 * Used on KitKat and below to determine if the currently inflated view is the start of
 * an included layout file. If so, any themed context (android:theme) needs to be manually
 * carried over to preserve it as expected.
 */
class LayoutIncludeDetector {

    @NonNull
    private final Deque<WeakReference<XmlPullParser>> mXmlParserStack = new ArrayDeque<>();

    /**
     * Returns true if this is the start of an included layout file, otherwise false.
     */
    boolean detect(@NonNull AttributeSet attrs) {
        if (attrs instanceof XmlPullParser) {
            XmlPullParser xmlAttrs = (XmlPullParser) attrs;
            if (xmlAttrs.getDepth() == 1) {
                // This is either beginning of an inflate or an include.
                // Start by popping XmlPullParsers which are no longer valid since we may
                // have returned from any number of sub-includes
                XmlPullParser ancestorXmlAttrs = popOutdatedAttrHolders(mXmlParserStack);
                // Then store current attrs for possible future use
                mXmlParserStack.push(new WeakReference<>(xmlAttrs));
                // Finally check if we need to inherit the parent context based on the
                // current and ancestor attribute set
                if (shouldInheritContext(xmlAttrs, ancestorXmlAttrs)) {
                    return true;
                }
            }
        }
        return false;
    }

    private static boolean shouldInheritContext(@NonNull XmlPullParser parser,
            @Nullable XmlPullParser previousParser) {
        if (previousParser != null && parser != previousParser) {

            // First check event type since that should avoid accessing native side with
            // possibly nulled native ptr. We do this since the previous parser could be
            // either the parent parser for an <include> (in which case it is still active,
            // with event type == START_TAG) or a parser from a separate inflate() call (in
            // which case the parser has been closed and would typically have event type
            // END_TAG or possibly END_DOCUMENT)
            try {
                if (previousParser.getEventType() == XmlPullParser.START_TAG) {
                    // Check if the parent parser is actually on an <include> tag,
                    // if so we need to inherit the parent context
                    return "include".equals(previousParser.getName());
                }
            } catch (XmlPullParserException e) {
            }
        }
        return false;
    }


    /**
     * Pops any outdated {@link XmlPullParser}s from the given stack.
     * @param xmlParserStack stack to purge
     * @return most recent {@link XmlPullParser} that is not outdated
     */
    @Nullable
    private static XmlPullParser popOutdatedAttrHolders(@NonNull
            Deque<WeakReference<XmlPullParser>> xmlParserStack) {
        while (!xmlParserStack.isEmpty()) {
            XmlPullParser parser = xmlParserStack.peek().get();
            if (isParserOutdated(parser)) {
                xmlParserStack.pop();
            } else {
                return parser;
            }
        }
        return null;
    }

    private static boolean isParserOutdated(@Nullable XmlPullParser parser) {
        try {
            return parser == null || (parser.getEventType() == XmlPullParser.END_TAG
                    || parser.getEventType() == XmlPullParser.END_DOCUMENT);
        } catch (XmlPullParserException e) {
            return true;
        }
    }
}