com.android.tools.idea.rendering.LayoutlibCallbackImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.idea.rendering.LayoutlibCallbackImpl.java

Source

/*
 * Copyright (C) 2013 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.tools.idea.rendering;

import com.android.ide.common.rendering.LayoutLibrary;
import com.android.ide.common.rendering.api.*;
import com.android.ide.common.resources.ResourceResolver;
import com.android.resources.ResourceType;
import com.android.tools.idea.AndroidPsiUtils;
import com.android.tools.idea.gradle.project.model.AndroidModuleModel;
import com.android.tools.idea.gradle.util.GradleUtil;
import com.android.tools.idea.model.AndroidModuleInfo;
import com.android.tools.idea.model.MergedManifest;
import com.android.tools.idea.res.AppResourceRepository;
import com.android.tools.idea.res.LocalResourceRepository;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.utils.HtmlBuilder;
import com.android.utils.SdkUtils;
import com.android.utils.XmlUtils;
import com.google.common.base.Charsets;
import com.google.common.collect.*;
import com.google.common.io.Files;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.uipreview.RecyclerViewHelper;
import org.jetbrains.android.uipreview.ViewLoader;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.kxml2.io.KXmlParser;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.StringReader;
import java.net.MalformedURLException;
import java.util.*;

import static com.android.SdkConstants.*;
import static com.android.ide.common.rendering.RenderParamsFlags.*;
import static com.android.tools.idea.gradle.project.model.AndroidModuleModel.EXPLODED_AAR;
import static com.intellij.lang.annotation.HighlightSeverity.WARNING;

/**
 * Loader for Android Project class in order to use them in the layout editor.
 * <p/>This implements {@code com.android.ide.common.rendering.api.IProjectCallback} for the old and new API through
 * {@link LayoutlibCallback}
 */
public class LayoutlibCallbackImpl extends LayoutlibCallback {
    private static final Logger LOG = Logger.getInstance("#com.android.tools.idea.rendering.LayoutlibCallback");

    /** Maximum number of getParser calls in a render before we suspect and investigate potential include cycles */
    private static final int MAX_PARSER_INCLUDES = 50;
    /** Class names that are not a view. When instantiating them, errors should be logged by LayoutLib. */
    private static final Set<String> NOT_VIEW = Collections.unmodifiableSet(
            Sets.newHashSet(RecyclerViewHelper.CN_RV_ADAPTER, RecyclerViewHelper.CN_RV_LAYOUT_MANAGER,
                    "android.support.v7.internal.app.WindowDecorActionBar"));
    /** Directory name for the bundled layoutlib installation */
    public static final String FD_LAYOUTLIB = "layoutlib";

    @NotNull
    private final Module myModule;
    @NotNull
    private final AppResourceRepository myProjectRes;
    @NotNull
    final private LayoutLibrary myLayoutLib;
    @Nullable
    private final Object myCredential;
    private final boolean myHasAppCompat;
    @Nullable
    private String myNamespace;
    @Nullable
    private RenderLogger myLogger;
    @NotNull
    private final ViewLoader myClassLoader;
    @Nullable
    private String myLayoutName;
    @Nullable
    private ILayoutPullParser myLayoutEmbeddedParser;
    @Nullable
    private ResourceResolver myResourceResolver;
    @Nullable
    private final ActionBarHandler myActionBarHandler;
    @Nullable
    private final RenderTask myRenderTask;
    private boolean myUsed = false;
    private Set<File> myParserFiles;
    private int myParserCount;
    private ParserFactory myParserFactory;

    /**
     * Creates a new {@link LayoutlibCallbackImpl} to be used with the layout lib.
     *
     * @param renderTask The associated render task
     * @param layoutLib  The layout library this callback is going to be invoked from
     * @param projectRes the {@link LocalResourceRepository} for the project.
     * @param module     the module
     * @param facet      the facet
     * @param logger     the render logger
     * @param credential the sandbox credential
     * @param actionBarHandler An {@link ActionBarHandler} instance.
     */
    public LayoutlibCallbackImpl(@Nullable RenderTask renderTask, @NotNull LayoutLibrary layoutLib,
            @NotNull AppResourceRepository projectRes, @NotNull Module module, @NotNull AndroidFacet facet,
            @NotNull RenderLogger logger, @Nullable Object credential,
            @Nullable ActionBarHandler actionBarHandler) {
        myRenderTask = renderTask;
        myLayoutLib = layoutLib;
        myProjectRes = projectRes;
        myModule = module;
        myCredential = credential;
        myClassLoader = new ViewLoader(myLayoutLib, facet, logger, credential);
        myActionBarHandler = actionBarHandler;

        AndroidModuleModel androidModel = AndroidModuleModel.get(facet);
        myHasAppCompat = androidModel != null && GradleUtil.dependsOn(androidModel, APPCOMPAT_LIB_ARTIFACT);

        String javaPackage = MergedManifest.get(myModule).getPackage();
        if (javaPackage != null && javaPackage.length() > 0) {
            myNamespace = URI_PREFIX + javaPackage;
        } else {
            myNamespace = AUTO_URI;
        }
    }

    /** Resets the callback state for another render */
    void reset() {
        myParserCount = 0;
        myParserFiles = null;
        myLayoutName = null;
        myLayoutEmbeddedParser = null;
    }

    /**
     * Sets the {@link LayoutLog} logger to use for error messages during problems
     *
     * @param logger the new logger to use, or null to clear it out
     */
    public void setLogger(@Nullable RenderLogger logger) {
        myLogger = logger;
        myClassLoader.setLogger(logger);
    }

    /**
     * Returns the {@link LayoutLog} logger used for error messages, or null
     *
     * @return the logger being used, or null if no logger is in use
     */
    @Nullable
    public LayoutLog getLogger() {
        return myLogger;
    }

    /**
     * {@inheritDoc}
     * <p/>
     * This implementation goes through the output directory of the project and loads the
     * <code>.class</code> file directly.
     */
    @Nullable
    @Override
    @SuppressWarnings("unchecked")
    public Object loadView(@NotNull String className, @NotNull Class[] constructorSignature,
            @NotNull Object[] constructorParameters) throws ClassNotFoundException {
        myUsed = true;
        if (NOT_VIEW.contains(className)) {
            return myClassLoader.loadClass(className, constructorSignature, constructorParameters);
        }
        return myClassLoader.loadView(className, constructorSignature, constructorParameters);
    }

    @Override
    public Object loadClass(@NotNull String name, @Nullable Class[] constructorSignature,
            @Nullable Object[] constructorArgs) throws ClassNotFoundException {
        myUsed = true;
        return myClassLoader.loadClass(name, constructorSignature, constructorArgs);
    }

    @Override
    public boolean supports(int ideFeature) {
        return ideFeature <= Features.LAST_FEATURE;
    }

    /**
     * Returns the namespace for the project. The namespace contains a standard part + the
     * application package.
     *
     * @return The package namespace of the project or null in case of error.
     */
    @Nullable
    @Override
    public String getNamespace() {
        return myNamespace;
    }

    @SuppressWarnings({ "UnnecessaryFullyQualifiedName", "deprecation" }) // Required by IProjectCallback
    @Nullable
    @Override
    public com.android.util.Pair<ResourceType, String> resolveResourceId(int id) {
        return myProjectRes.resolveResourceId(id);
    }

    @Nullable
    @Override
    public String resolveResourceId(int[] id) {
        return myProjectRes.resolveStyleable(id);
    }

    @Nullable
    @Override
    public Integer getResourceId(ResourceType type, String name) {
        return myProjectRes.getResourceId(type, name);
    }

    /**
     * Returns whether the loader has received requests to load custom views. Note that
     * the custom view loading may not actually have succeeded; this flag only records
     * whether it was <b>requested</b>.
     * <p/>
     * This allows to efficiently only recreate when needed upon code change in the
     * project.
     *
     * @return true if the loader has been asked to load custom views
     */
    public boolean isUsed() {
        return myUsed;
    }

    @Nullable
    @Override
    public XmlPullParser getXmlFileParser(String fileName) {
        // No need to generate a PSI-based parser (which can read edited/unsaved contents) for files in build outputs or
        // layoutlib built-in directories
        if (fileName.contains(EXPLODED_AAR) || fileName.contains(FD_LAYOUTLIB)) {
            return null;
        }

        boolean token = RenderSecurityManager.enterSafeRegion(myCredential);
        try {
            VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByPath(fileName);
            if (virtualFile != null) {
                PsiFile psiFile = AndroidPsiUtils.getPsiFileSafely(myModule.getProject(), virtualFile);
                if (psiFile != null) {
                    try {
                        XmlPullParser parser = getParserFactory().createParser(fileName);
                        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
                        String psiText = ApplicationManager.getApplication().isReadAccessAllowed()
                                ? psiFile.getText()
                                : ApplicationManager.getApplication()
                                        .runReadAction((Computable<String>) psiFile::getText);
                        parser.setInput(new StringReader(psiText));
                        return parser;
                    } catch (XmlPullParserException e) {
                        LOG.warn("Could not create parser for " + fileName);
                    }
                }
            }
            return null;
        } finally {
            RenderSecurityManager.exitSafeRegion(token);
        }
    }

    public void setLayoutParser(@Nullable String layoutName, @Nullable ILayoutPullParser layoutParser) {
        myLayoutName = layoutName;
        myLayoutEmbeddedParser = layoutParser;
    }

    @Nullable
    public ILayoutPullParser getLayoutEmbeddedParser() {
        return myLayoutEmbeddedParser;
    }

    @SuppressWarnings("deprecation") // Required by IProjectCallback
    @Nullable
    @Override
    public ILayoutPullParser getParser(@NotNull String layoutName) {
        // Try to compute the ResourceValue for this layout since layoutlib
        // must be an older version which doesn't pass the value:
        if (myResourceResolver != null) {
            ResourceValue value = myResourceResolver.getProjectResource(ResourceType.LAYOUT, layoutName);
            if (value != null) {
                return getParser(value);
            }
        }

        return getParser(layoutName, false, null);
    }

    @Nullable
    @Override
    public ILayoutPullParser getParser(@NotNull ResourceValue layoutResource) {
        return getParser(layoutResource.getName(), layoutResource.isFramework(),
                new File(layoutResource.getValue()));
    }

    @Nullable
    private ILayoutPullParser getParser(@NotNull String layoutName, boolean isFramework, @Nullable File xml) {
        if (myParserFiles != null && myParserFiles.contains(xml)) {
            if (myParserCount > MAX_PARSER_INCLUDES) {
                // Unlikely large number of includes. Look for cyclic dependencies in the available
                // files.
                if (findCycles()) {
                    throw new RuntimeException("Aborting rendering");
                }

                // Also reset counter to 0 so we don't check on every subsequent iteration.
                myParserCount = 0;
            }
        } else {
            if (myParserFiles == null) {
                myParserFiles = Sets.newHashSet();
            }
            myParserFiles.add(xml);
        }
        myParserCount++;

        if (layoutName.equals(myLayoutName) && !isFramework) {
            ILayoutPullParser parser = myLayoutEmbeddedParser;
            // The parser should only be used once!! If it is included more than once,
            // subsequent includes should just use a plain pull parser that is not tied
            // to the XML model
            myLayoutEmbeddedParser = null;
            return parser;
        }

        // See if we can find a corresponding PSI file for this included layout, and
        // if so directly reuse the PSI parser, such that we pick up the live, edited
        // contents rather than the most recently saved file contents.
        if (xml != null && xml.isFile()) {
            File parent = xml.getParentFile();
            String path = xml.getPath();
            // No need to generate a PSI-based parser (which can read edited/unsaved contents) for files in build outputs or
            // layoutlib built-in directories
            if (parent != null && !path.contains(EXPLODED_AAR) && !path.contains(FD_LAYOUTLIB)) {
                String parentName = parent.getName();
                if (parentName.startsWith(FD_RES_LAYOUT) || parentName.startsWith(FD_RES_MENU)) {
                    VirtualFile file = LocalFileSystem.getInstance().findFileByIoFile(xml);
                    if (file != null) {
                        PsiFile psiFile = AndroidPsiUtils.getPsiFileSafely(myModule.getProject(), file);
                        if (psiFile instanceof XmlFile) {
                            assert myLogger != null;
                            LayoutPsiPullParser parser = LayoutPsiPullParser.create((XmlFile) psiFile, myLogger);
                            parser.setUseSrcCompat(myHasAppCompat);
                            if (parentName.startsWith(FD_RES_LAYOUT)) {
                                // For included layouts, we don't normally see view cookies; we want the leaf to point back to the include tag
                                parser.setProvideViewCookies(
                                        myRenderTask != null && myRenderTask.getProvideCookiesForIncludedViews());
                            }
                            return parser;
                        }
                    }
                }
            }

            // For included layouts, create a LayoutFilePullParser such that we get the
            // layout editor behavior in included layouts as well - which for example
            // replaces <fragment> tags with <include>.
            try {
                return LayoutFilePullParser.create(this, xml);
            } catch (XmlPullParserException e) {
                LOG.error(e);
            } catch (FileNotFoundException e) {
                // Shouldn't happen since we check isFile() above
            } catch (IOException e) {
                LOG.error(e);
            }
        }

        return null;
    }

    /**
     * Searches for cycles in the {@code <include>} tag graph of the layout files we've
     * been asked to provide parsers for
     */
    private boolean findCycles() {
        Map<File, String> fileToLayout = Maps.newHashMap();
        Map<String, File> layoutToFile = Maps.newHashMap();
        Multimap<String, String> includeMap = ArrayListMultimap.create();
        for (File file : myParserFiles) {
            String layoutName = LintUtils.getLayoutName(file);
            layoutToFile.put(layoutName, file);
            fileToLayout.put(file, layoutName);
            try {
                String xml = Files.toString(file, Charsets.UTF_8);
                Document document = XmlUtils.parseDocumentSilently(xml, true);
                if (document != null) {
                    NodeList includeNodeList = document.getElementsByTagName(VIEW_INCLUDE);
                    for (int i = 0, n = includeNodeList.getLength(); i < n; i++) {
                        Element include = (Element) includeNodeList.item(i);
                        String included = include.getAttribute(ATTR_LAYOUT);
                        if (included.startsWith(LAYOUT_RESOURCE_PREFIX)) {
                            String resource = included.substring(LAYOUT_RESOURCE_PREFIX.length());
                            includeMap.put(layoutName, resource);
                        }
                    }

                    // Deals with tools:layout attribute from fragments
                    NodeList fragmentNodeList = document.getElementsByTagName(VIEW_FRAGMENT);
                    for (int i = 0, n = fragmentNodeList.getLength(); i < n; i++) {
                        Element fragment = (Element) fragmentNodeList.item(i);
                        String included = fragment.getAttributeNS(TOOLS_URI, ATTR_LAYOUT);
                        if (included.startsWith(LAYOUT_RESOURCE_PREFIX)) {
                            String resource = included.substring(LAYOUT_RESOURCE_PREFIX.length());
                            includeMap.put(layoutName, resource);
                        }
                    }
                }
            } catch (IOException e) {
                LOG.warn("Could not check file " + file + " for cyclic dependencies", e);
            }
        }

        // We now have a DAG over the include dependencies in the layouts
        // Do a DFS to detect cycles

        // Perform DFS on the include graph and look for a cycle; if we find one, produce
        // a chain of includes on the way back to show to the user
        if (includeMap.size() > 0) {
            for (String from : includeMap.keySet()) {
                Set<String> visiting = Sets.newHashSetWithExpectedSize(includeMap.size());
                List<String> chain = dfs(from, visiting, includeMap);
                if (chain != null) {
                    if (myLogger != null) {
                        RenderProblem.Html problem = RenderProblem.create(WARNING);
                        HtmlBuilder builder = problem.getHtmlBuilder();
                        builder.add("Found cyclical <include> chain: ");
                        boolean first = true;
                        Collections.reverse(chain);
                        for (String layout : chain) {
                            if (first) {
                                first = false;
                            } else {
                                builder.add(" includes ");
                            }
                            File file = layoutToFile.get(layout);
                            if (file != null) {
                                try {
                                    String url = SdkUtils.fileToUrlString(file);
                                    builder.addLink(layout, url);
                                } catch (MalformedURLException e) {
                                    builder.add(layout);
                                }
                            } else {
                                builder.add(layout);
                            }
                        }

                        myLogger.addMessage(problem);
                    }
                    return true;
                }
            }
        }

        return false;
    }

    @Nullable
    private static List<String> dfs(String from, Set<String> visiting, Multimap<String, String> includeMap) {
        visiting.add(from);
        Collection<String> includes = includeMap.get(from);
        if (includes != null && includes.size() > 0) {
            for (String include : includes) {
                if (visiting.contains(include)) {
                    List<String> list = Lists.newLinkedList();
                    list.add(include);
                    list.add(from);
                    return list;
                }
                List<String> chain = dfs(include, visiting, includeMap);
                if (chain != null) {
                    chain.add(from);
                    return chain;
                }
            }
        }
        visiting.remove(from);
        return null;
    }

    @Nullable
    @Override
    public Object getAdapterItemValue(ResourceReference adapterView, Object adapterCookie,
            ResourceReference itemRef, int fullPosition, int typePosition, int fullChildPosition,
            int typeChildPosition, ResourceReference viewRef, ViewAttribute viewAttribute, Object defaultValue) {

        // Special case for the palette preview
        if (viewAttribute == ViewAttribute.TEXT && adapterView.getName().startsWith("android_widget_")) { //$NON-NLS-1$
            String name = adapterView.getName();
            if (viewRef.getName().equals("text2")) { //$NON-NLS-1$
                return "Sub Item";
            }
            if (fullPosition == 0) {
                String viewName = name.substring("android_widget_".length());
                if (viewName.equals(EXPANDABLE_LIST_VIEW)) {
                    return "ExpandableList"; // ExpandableListView is too wide, character-wraps
                }
                return viewName;
            } else {
                return "Next Item";
            }
        }

        if (itemRef.isFramework()) {
            // Special case for list_view_item_2 and friends
            if (viewRef.getName().equals("text2")) { //$NON-NLS-1$
                return "Sub Item " + (fullPosition + 1);
            }
        }

        if (viewAttribute == ViewAttribute.TEXT && ((String) defaultValue).length() == 0) {
            return "Item " + (fullPosition + 1);
        }

        return null;
    }

    /**
     * For the given class, finds and returns the nearest super class which is a ListView
     * or an ExpandableListView or a GridView (which uses a list adapter), or returns null.
     *
     * @param clz the class of the view object
     * @return the fully qualified class name of the list ancestor, or null if there
     *         is no list view ancestor
     */
    @Nullable
    public static String getListAdapterViewFqcn(@NotNull Class<?> clz) {
        String fqcn = clz.getName();
        if (fqcn.endsWith(LIST_VIEW) // including EXPANDABLE_LIST_VIEW
                || fqcn.equals(FQCN_GRID_VIEW) || fqcn.equals(FQCN_SPINNER)) {
            return fqcn;
        } else if (fqcn.startsWith(ANDROID_PKG_PREFIX)) {
            return null;
        }
        Class<?> superClass = clz.getSuperclass();
        if (superClass != null) {
            return getListAdapterViewFqcn(superClass);
        } else {
            // Should not happen; we would have encountered android.view.View first,
            // and it should have been covered by the ANDROID_PKG_PREFIX case above.
            return null;
        }
    }

    /**
     * Looks at the parent-chain of the view and if it finds a custom view, or a
     * CalendarView, within the given distance then it returns true. A ListView within a
     * CalendarView should not be assigned a custom list view type because it sets its own
     * and then attempts to cast the layout to its own type which would fail if the normal
     * default list item binding is used.
     */
    private boolean isWithinIllegalParent(@NotNull Object viewObject, int depth) {
        String fqcn = viewObject.getClass().getName();
        if (fqcn.endsWith(CALENDAR_VIEW) || !(fqcn.startsWith(ANDROID_PKG_PREFIX)
                // ActionBar at the root level
                || fqcn.startsWith("com.android.internal.widget."))) {
            return true;
        }

        if (depth > 0) {
            Result result = myLayoutLib.getViewParent(viewObject);
            if (result.isSuccess()) {
                Object parent = result.getData();
                if (parent != null) {
                    return isWithinIllegalParent(parent, depth - 1);
                }
            }
        }

        return false;
    }

    @Nullable
    @Override
    public AdapterBinding getAdapterBinding(final ResourceReference adapterView, final Object adapterCookie,
            final Object viewObject) {
        // Look for user-recorded preference for layout to be used for previews
        if (adapterCookie instanceof TagSnapshot) {
            AdapterBinding binding = LayoutMetadata.getNodeBinding(viewObject, (TagSnapshot) adapterCookie);
            if (binding != null) {
                return binding;
            }
        } else if (adapterCookie instanceof XmlTag) {
            AdapterBinding binding = LayoutMetadata.getNodeBinding(viewObject,
                    TagSnapshot.createTagSnapshotWithoutChildren((XmlTag) adapterCookie));
            if (binding != null) {
                return binding;
            }
        } else if (adapterCookie instanceof Map<?, ?>) {
            @SuppressWarnings("unchecked")
            Map<String, String> map = (Map<String, String>) adapterCookie;
            AdapterBinding binding = LayoutMetadata.getNodeBinding(viewObject, map);
            if (binding != null) {
                return binding;
            }
        }

        if (viewObject == null) {
            return null;
        }

        // Is this a ListView or ExpandableListView? If so, return its fully qualified
        // class name, otherwise return null. This is used to filter out other types
        // of AdapterViews (such as Spinners) where we don't want to use the list item
        // binding.
        String listFqcn = getListAdapterViewFqcn(viewObject.getClass());
        if (listFqcn == null) {
            return null;
        }

        // Is this ListView nested within an "illegal" container, such as a CalendarView?
        // If so, don't change the bindings below. Some views, such as CalendarView, and
        // potentially some custom views, might be doing specific things with the ListView
        // that could break if we add our own list binding, so for these leave the list
        // alone.
        if (isWithinIllegalParent(viewObject, 2)) {
            return null;
        }

        int count = listFqcn.endsWith(GRID_VIEW) ? 24 : 12;
        AdapterBinding binding = new AdapterBinding(count);
        if (listFqcn.endsWith(EXPANDABLE_LIST_VIEW)) {
            binding.addItem(
                    new DataBindingItem(LayoutMetadata.DEFAULT_EXPANDABLE_LIST_ITEM, true /* isFramework */, 1));
        } else if (listFqcn.equals(FQCN_SPINNER)) {
            binding.addItem(new DataBindingItem(LayoutMetadata.DEFAULT_SPINNER_ITEM, true /* isFramework */, 1));
        } else {
            binding.addItem(new DataBindingItem(LayoutMetadata.DEFAULT_LIST_ITEM, true /* isFramework */, 1));
        }

        return binding;
    }

    /**
     * Sets the {@link ResourceResolver} to be used when looking up resources
     *
     * @param resolver the resolver to use
     */
    public void setResourceResolver(@Nullable ResourceResolver resolver) {
        myResourceResolver = resolver;
    }

    /**
     * Load and parse the R class such that resource references in the layout rendering can refer
     * to local resources properly
     */
    public void loadAndParseRClass() {
        myClassLoader.loadAndParseRClassSilently();
    }

    @Override
    public ActionBarCallback getActionBarCallback() {
        return myActionBarHandler;
    }

    @Nullable
    public ActionBarHandler getActionBarHandler() {
        return myActionBarHandler;
    }

    @Nullable
    @Override
    @SuppressWarnings("unchecked")
    public <T> T getFlag(@NotNull SessionParams.Key<T> key) {
        if (key.equals(FLAG_KEY_APPLICATION_PACKAGE)) {
            return (T) getPackage();
        }
        if (key.equals(FLAG_KEY_RECYCLER_VIEW_SUPPORT)) {
            return (T) Boolean.TRUE;
        }
        if (key.equals(FLAG_KEY_XML_FILE_PARSER_SUPPORT)) {
            return (T) Boolean.TRUE;
        }
        return null;
    }

    @Nullable
    private String getPackage() {
        AndroidModuleInfo info = AndroidModuleInfo.get(myModule);
        return info == null ? null : info.getPackage();
    }

    @NotNull
    @Override
    public ParserFactory getParserFactory() {
        if (myParserFactory == null) {
            myParserFactory = new ParserFactoryImpl();
        }
        return myParserFactory;
    }

    @NotNull
    @Override
    public Class<?> findClass(@NotNull String name) throws ClassNotFoundException {
        try {
            Class<?> aClass = myClassLoader.loadClass(name, false);
            if (aClass != null) {
                return aClass;
            }
            throw new ClassNotFoundException(name + " not found.");
        } catch (InconvertibleClassError e) {
            throw new ClassNotFoundException(name + " not found.", e);
        }

    }

    private static class ParserFactoryImpl extends ParserFactory {
        @NotNull
        @Override
        public XmlPullParser createParser(@Nullable String debugName) throws XmlPullParserException {
            return new NamedParser(debugName);
        }
    }

    private static class NamedParser extends KXmlParser {
        @Nullable
        private final String myName;
        /**
         * Attribute that caches whether the tools prefix has been defined or not. This allow us to save unnecessary checks in the most common
         * case ("tools" is not defined).
         */
        private boolean hasToolsNamespace;

        public NamedParser(@Nullable String name) {
            myName = name;
        }

        @Override
        public int next() throws XmlPullParserException, IOException {
            int tagType = super.next();

            // We check if the tools namespace is still defined in two cases:
            // - If it's a start tag and it was defined by a previous tag
            // - If it WAS defined by a previous tag, and we are closing a tag (going out of scope)
            if ((!hasToolsNamespace && tagType == XmlPullParser.START_TAG)
                    || (hasToolsNamespace && tagType == XmlPullParser.END_TAG)) {
                hasToolsNamespace = getNamespace("tools") != null;
            }

            return tagType;
        }

        @Override
        public String getAttributeValue(String namespace, String name) {
            if (hasToolsNamespace && ANDROID_URI.equals(namespace)) {
                // Only for "android:" attribute, we will check if there is a "tools:" version overriding the value
                String toolsValue = super.getAttributeValue(TOOLS_URI, name);
                if (toolsValue != null) {
                    return toolsValue;
                }
            }

            return super.getAttributeValue(namespace, name);
        }

        @Override
        public String toString() {
            return myName != null ? myName : super.toString();
        }
    }
}