com.android.tools.idea.uibuilder.property.NlProperties.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.idea.uibuilder.property.NlProperties.java

Source

/*
 * Copyright (C) 2015 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.uibuilder.property;

import com.android.annotations.VisibleForTesting;
import com.android.tools.idea.configurations.Configuration;
import com.android.tools.idea.gradle.dependencies.GradleDependencyManager;
import com.android.tools.idea.model.MergedManifest;
import com.android.tools.idea.uibuilder.api.ViewHandler;
import com.android.tools.idea.uibuilder.handlers.ImageViewHandler;
import com.android.tools.idea.uibuilder.handlers.ViewHandlerManager;
import com.android.tools.idea.uibuilder.model.NlComponent;
import com.android.utils.Pair;
import com.google.common.base.Splitter;
import com.android.tools.idea.uibuilder.model.NlModel;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableTable;
import com.google.common.collect.Table;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.JavaPsiFacade;
import com.intellij.psi.PsiClass;
import com.intellij.psi.xml.XmlTag;
import com.intellij.xml.NamespaceAwareXmlAttributeDescriptor;
import com.intellij.xml.XmlAttributeDescriptor;
import com.intellij.xml.XmlElementDescriptor;
import org.jetbrains.android.dom.AndroidAnyAttributeDescriptor;
import org.jetbrains.android.dom.AndroidDomElementDescriptorProvider;
import org.jetbrains.android.dom.attrs.AttributeDefinition;
import org.jetbrains.android.dom.attrs.AttributeDefinitions;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.resourceManagers.ResourceManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import static com.android.SdkConstants.*;

public class NlProperties {
    public static final String STARRED_PROP = "ANDROID.STARRED_PROPERTIES";

    private static NlProperties ourInstance = null;
    private final AndroidDomElementDescriptorProvider myDescriptorProvider = new AndroidDomElementDescriptorProvider();

    public static synchronized NlProperties getInstance() {
        if (ourInstance == null) {
            ourInstance = new NlProperties();
        }
        return ourInstance;
    }

    @NotNull
    public Table<String, String, NlPropertyItem> getProperties(@NotNull List<NlComponent> components) {
        AndroidFacet facet = getFacet(components);
        if (facet == null) {
            return ImmutableTable.of();
        }
        GradleDependencyManager dependencyManager = GradleDependencyManager
                .getInstance(facet.getModule().getProject());
        return getProperties(facet, components, dependencyManager);
    }

    @VisibleForTesting
    Table<String, String, NlPropertyItem> getProperties(@NotNull AndroidFacet facet,
            @NotNull List<NlComponent> components, @NotNull GradleDependencyManager dependencyManager) {
        return ApplicationManager.getApplication().runReadAction(
                (Computable<Table<String, String, NlPropertyItem>>) () -> getPropertiesWithReadLock(facet,
                        components, dependencyManager));
    }

    @NotNull
    private Table<String, String, NlPropertyItem> getPropertiesWithReadLock(@NotNull AndroidFacet facet,
            @NotNull List<NlComponent> components, @NotNull GradleDependencyManager dependencyManager) {
        ResourceManager localResourceManager = facet.getLocalResourceManager();
        ResourceManager systemResourceManager = facet.getSystemResourceManager();
        if (systemResourceManager == null) {
            Logger.getInstance(NlProperties.class)
                    .error("No system resource manager for module: " + facet.getModule().getName());
            return ImmutableTable.of();
        }

        AttributeDefinitions localAttrDefs = localResourceManager.getAttributeDefinitions();
        AttributeDefinitions systemAttrDefs = systemResourceManager.getAttributeDefinitions();

        Table<String, String, NlPropertyItem> combinedProperties = null;

        for (NlComponent component : components) {
            XmlTag tag = component.getTag();
            if (!tag.isValid()) {
                return ImmutableTable.of();
            }

            XmlElementDescriptor elementDescriptor = myDescriptorProvider.getDescriptor(tag);
            if (elementDescriptor == null) {
                return ImmutableTable.of();
            }

            XmlAttributeDescriptor[] descriptors = elementDescriptor.getAttributesDescriptors(tag);
            Table<String, String, NlPropertyItem> properties = HashBasedTable.create(3, descriptors.length);

            for (XmlAttributeDescriptor desc : descriptors) {
                String namespace = getNamespace(desc, tag);
                AttributeDefinitions attrDefs = NS_RESOURCES.equals(namespace) ? systemAttrDefs : localAttrDefs;
                AttributeDefinition attrDef = attrDefs == null ? null : attrDefs.getAttrDefByName(desc.getName());
                NlPropertyItem property = NlPropertyItem.create(components, desc, namespace, attrDef);
                properties.put(StringUtil.notNullize(namespace), property.getName(), property);
            }

            // Exceptions:
            switch (tag.getName()) {
            case AUTO_COMPLETE_TEXT_VIEW:
                // An AutoCompleteTextView has a popup that is created at runtime.
                // Properties for this popup can be added to the AutoCompleteTextView tag.
                properties.put(ANDROID_URI, ATTR_POPUP_BACKGROUND, NlPropertyItem.create(components,
                        new AndroidAnyAttributeDescriptor(ATTR_POPUP_BACKGROUND), ANDROID_URI,
                        systemAttrDefs != null ? systemAttrDefs.getAttrDefByName(ATTR_POPUP_BACKGROUND) : null));
                break;
            }

            combinedProperties = combine(properties, combinedProperties);
        }

        // The following properties are deprecated in the support library and can be ignored by tools:
        assert combinedProperties != null;
        combinedProperties.remove(AUTO_URI, ATTR_PADDING_START);
        combinedProperties.remove(AUTO_URI, ATTR_PADDING_END);
        combinedProperties.remove(AUTO_URI, ATTR_THEME);

        setUpDesignProperties(combinedProperties);
        setUpSrcCompat(combinedProperties, facet, components, dependencyManager);

        initStarState(combinedProperties);

        //noinspection ConstantConditions
        return combinedProperties;
    }

    @Nullable
    private static AndroidFacet getFacet(@NotNull List<NlComponent> components) {
        if (components.isEmpty()) {
            return null;
        }
        NlComponent first = components.get(0);
        XmlTag firstTag = first.getTag();

        return AndroidFacet.getInstance(firstTag);
    }

    private static void initStarState(@NotNull Table<String, String, NlPropertyItem> properties) {
        for (String starredProperty : getStarredProperties()) {
            Pair<String, String> property = split(starredProperty);
            NlPropertyItem item = properties.get(property.getFirst(), property.getSecond());
            if (item != null) {
                item.setInitialStarred();
            }
        }
    }

    public static void saveStarState(@Nullable String propertyNamespace, @NotNull String propertyName,
            boolean starred) {
        String propertyNameWithPrefix = getPropertyNameWithPrefix(propertyNamespace, propertyName);
        StringBuilder builder = new StringBuilder();
        for (String starredProperty : getStarredProperties()) {
            if (!starredProperty.equals(propertyNameWithPrefix)) {
                builder.append(starredProperty);
                builder.append(";");
            }
        }
        if (starred) {
            builder.append(propertyNameWithPrefix);
            builder.append(";");
        }
        PropertiesComponent properties = PropertiesComponent.getInstance();
        properties.setValue(STARRED_PROP, builder.toString());
    }

    public static String getStarredPropertiesAsString() {
        String starredProperties = PropertiesComponent.getInstance().getValue(STARRED_PROP);
        if (starredProperties == null) {
            starredProperties = ATTR_VISIBILITY;
        }
        return starredProperties;
    }

    public static Iterable<String> getStarredProperties() {
        return Splitter.on(';').trimResults().omitEmptyStrings().split(getStarredPropertiesAsString());
    }

    @NotNull
    private static String getPropertyNameWithPrefix(@Nullable String namespace, @NotNull String propertyName) {
        if (namespace == null) {
            return propertyName;
        }
        switch (namespace) {
        case TOOLS_URI:
            return TOOLS_NS_NAME_PREFIX + propertyName;
        case ANDROID_URI:
            return propertyName;
        default:
            return PREFIX_APP + propertyName;
        }
    }

    @NotNull
    private static Pair<String, String> split(@NotNull String propertyNameWithPrefix) {
        if (propertyNameWithPrefix.startsWith(TOOLS_NS_NAME_PREFIX)) {
            return Pair.of(TOOLS_URI, propertyNameWithPrefix.substring(TOOLS_NS_NAME_PREFIX.length()));
        }
        if (propertyNameWithPrefix.startsWith(PREFIX_APP)) {
            return Pair.of(AUTO_URI, propertyNameWithPrefix.substring(PREFIX_APP.length()));
        }
        return Pair.of(ANDROID_URI, propertyNameWithPrefix);
    }

    @Nullable
    private static String getNamespace(@NotNull XmlAttributeDescriptor descriptor, @NotNull XmlTag context) {
        if (descriptor instanceof NamespaceAwareXmlAttributeDescriptor) {
            return ((NamespaceAwareXmlAttributeDescriptor) descriptor).getNamespace(context);
        } else {
            return null;
        }
    }

    private static Table<String, String, NlPropertyItem> combine(
            @NotNull Table<String, String, NlPropertyItem> properties,
            @Nullable Table<String, String, NlPropertyItem> combinedProperties) {
        if (combinedProperties == null) {
            return properties;
        }
        List<String> namespaces = new ArrayList<>(combinedProperties.rowKeySet());
        List<String> propertiesToRemove = new ArrayList<>();
        for (String namespace : namespaces) {
            propertiesToRemove.clear();
            for (Map.Entry<String, NlPropertyItem> entry : combinedProperties.row(namespace).entrySet()) {
                NlPropertyItem other = properties.get(namespace, entry.getKey());
                if (!entry.getValue().sameDefinition(other)) {
                    propertiesToRemove.add(entry.getKey());
                }
            }
            for (String propertyName : propertiesToRemove) {
                combinedProperties.remove(namespace, propertyName);
            }
        }
        // Never include the ID attribute when looking at multiple components:
        combinedProperties.remove(ANDROID_URI, ATTR_ID);
        return combinedProperties;
    }

    private static void setUpDesignProperties(@NotNull Table<String, String, NlPropertyItem> properties) {
        List<String> designProperties = new ArrayList<>(properties.row(TOOLS_URI).keySet());
        for (String propertyName : designProperties) {
            NlPropertyItem item = properties.get(AUTO_URI, propertyName);
            if (item == null) {
                item = properties.get(ANDROID_URI, propertyName);
            }
            if (item != null) {
                NlPropertyItem designItem = item.getDesignTimeProperty();
                properties.put(TOOLS_URI, propertyName, designItem);
            }
        }
    }

    // If the src property is available and AppCompat is used then fabricate another property: srcCompat.
    // This is how appCompat is supporting vector drawables in older versions of Android.
    private static void setUpSrcCompat(@NotNull Table<String, String, NlPropertyItem> properties,
            @NotNull AndroidFacet facet, @NotNull List<NlComponent> components,
            @NotNull GradleDependencyManager dependencyManager) {
        NlPropertyItem srcProperty = properties.get(ANDROID_URI, ATTR_SRC);
        if (srcProperty != null && shouldAddSrcCompat(facet, components, dependencyManager)) {
            AttributeDefinition srcDefinition = srcProperty.getDefinition();
            assert srcDefinition != null;
            AttributeDefinition srcCompatDefinition = new AttributeDefinition(ATTR_SRC_COMPAT, null,
                    srcDefinition.getFormats());
            srcCompatDefinition.getParentStyleables().addAll(srcDefinition.getParentStyleables());
            NlPropertyItem srcCompatProperty = new NlPropertyItem(components, AUTO_URI, srcCompatDefinition);
            properties.put(AUTO_URI, ATTR_SRC_COMPAT, srcCompatProperty);
        }
    }

    private static boolean shouldAddSrcCompat(@NotNull AndroidFacet facet, @NotNull List<NlComponent> components,
            @NotNull GradleDependencyManager dependencyManager) {
        return dependencyManager.dependsOn(facet.getModule(), APPCOMPAT_LIB_ARTIFACT)
                && allComponentsAreImageViews(facet, components)
                && currentActivityIfFoundIsDerivedFromAppCompatActivity(components);
    }

    private static boolean allComponentsAreImageViews(@NotNull AndroidFacet facet,
            @NotNull List<NlComponent> components) {
        ViewHandlerManager manager = ViewHandlerManager.get(facet);
        if (components.isEmpty()) {
            return false;
        }
        for (NlComponent component : components) {
            ViewHandler handler = manager.getHandler(component.getTagName());
            if (!(handler instanceof ImageViewHandler)) {
                return false;
            }
        }
        return true;
    }

    private static boolean currentActivityIfFoundIsDerivedFromAppCompatActivity(
            @NotNull List<NlComponent> components) {
        assert !components.isEmpty();
        NlModel model = components.get(0).getModel();
        Configuration configuration = model.getConfiguration();
        String activityClassName = configuration.getActivity();
        if (activityClassName == null) {
            // The activity is not specified in the XML file.
            // We cannot know if the activity is derived from AppCompatActivity.
            // Assume we are since this is how the default activities are created.
            return true;
        }
        if (activityClassName.startsWith(".")) {
            MergedManifest manifest = MergedManifest.get(model.getModule());
            String pkg = StringUtil.notNullize(manifest.getPackage());
            activityClassName = pkg + activityClassName;
        }
        JavaPsiFacade facade = JavaPsiFacade.getInstance(model.getProject());
        PsiClass activityClass = facade.findClass(activityClassName, model.getModule().getModuleScope());
        while (activityClass != null && !CLASS_APP_COMPAT_ACTIVITY.equals(activityClass.getQualifiedName())) {
            activityClass = activityClass.getSuperClass();
        }
        return activityClass != null;
    }
}