blob: 1ca9d2370cd6b1ddcefe16bc8f83ef30990b0399 [file] [log] [blame]
/*
* Copyright 2000-2010 JetBrains s.r.o.
*
* 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 org.jetbrains.android.dom;
import com.android.SdkConstants;
import com.android.resources.ResourceType;
import com.google.common.collect.Maps;
import com.intellij.openapi.project.Project;
import com.intellij.psi.JavaPsiFacade;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiReference;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlAttributeValue;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.HashMap;
import com.intellij.util.xml.*;
import org.jetbrains.android.dom.attrs.AttributeDefinition;
import org.jetbrains.android.dom.attrs.AttributeDefinitions;
import org.jetbrains.android.dom.attrs.AttributeFormat;
import org.jetbrains.android.dom.converters.*;
import org.jetbrains.android.dom.layout.LayoutElement;
import org.jetbrains.android.dom.layout.LayoutViewElement;
import org.jetbrains.android.dom.manifest.*;
import org.jetbrains.android.dom.menu.Group;
import org.jetbrains.android.dom.menu.Menu;
import org.jetbrains.android.dom.menu.MenuItem;
import org.jetbrains.android.dom.resources.*;
import org.jetbrains.android.dom.xml.PreferenceElement;
import org.jetbrains.android.dom.xml.XmlResourceElement;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.resourceManagers.ResourceManager;
import org.jetbrains.android.util.AndroidResourceUtil;
import org.jetbrains.android.util.AndroidUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import static com.android.SdkConstants.*;
import static org.jetbrains.android.util.AndroidUtils.SYSTEM_RESOURCE_PACKAGE;
/**
* @author Eugene.Kudelevsky
*/
@SuppressWarnings({"EnumSwitchStatementWhichMissesCases"})
public class AndroidDomUtil {
public static final StaticEnumConverter BOOLEAN_CONVERTER = new StaticEnumConverter(VALUE_TRUE, VALUE_FALSE);
public static final Map<String, String> SPECIAL_RESOURCE_TYPES = Maps.newHashMapWithExpectedSize(20);
private static final PackageClassConverter ACTIVITY_CONVERTER = new PackageClassConverter(AndroidUtils.ACTIVITY_BASE_CLASS_NAME);
private static final FragmentClassConverter FRAGMENT_CLASS_CONVERTER = new FragmentClassConverter();
static {
// This section adds additional resource type registrations where the attrs metadata is lacking. For
// example, attrs_manifest.xml tells us that the android:icon attribute can be a reference, but not
// that it's a reference to a drawable.
addSpecialResourceType(ResourceType.STRING.getName(), ATTR_LABEL, "description", ATTR_TITLE);
addSpecialResourceType(ResourceType.DRAWABLE.getName(), ATTR_ICON);
addSpecialResourceType(ResourceType.STYLE.getName(), ATTR_THEME);
addSpecialResourceType(ResourceType.ANIM.getName(), "animation");
addSpecialResourceType(ResourceType.ID.getName(), ATTR_ID, ATTR_LAYOUT_TO_RIGHT_OF, ATTR_LAYOUT_TO_LEFT_OF, ATTR_LAYOUT_ABOVE,
ATTR_LAYOUT_BELOW, ATTR_LAYOUT_ALIGN_BASELINE, ATTR_LAYOUT_ALIGN_LEFT, ATTR_LAYOUT_ALIGN_TOP,
ATTR_LAYOUT_ALIGN_RIGHT, ATTR_LAYOUT_ALIGN_BOTTOM, ATTR_LAYOUT_ALIGN_START, ATTR_LAYOUT_ALIGN_END,
ATTR_LAYOUT_TO_START_OF, ATTR_LAYOUT_TO_END_OF);
}
private AndroidDomUtil() {
}
@Nullable
public static String getResourceType(@NotNull AttributeFormat format) {
switch (format) {
case Color:
return ResourceType.COLOR.getName();
case Dimension:
return ResourceType.DIMEN.getName();
case String:
return ResourceType.STRING.getName();
case Float:
case Integer:
return ResourceType.INTEGER.getName();
case Fraction:
return ResourceType.FRACTION.getName();
case Boolean:
return ResourceType.BOOL.getName();
default:
return null;
}
}
@Nullable
public static ResolvingConverter<String> getStringConverter(@NotNull AttributeFormat format, @NotNull String[] values) {
switch (format) {
case Enum:
return new StaticEnumConverter(values);
case Boolean:
return BOOLEAN_CONVERTER;
case Integer:
return IntegerConverter.INSTANCE;
case Dimension:
return DimensionConverter.INSTANCE;
default:
return null;
}
}
@Nullable
public static ResourceReferenceConverter getResourceReferenceConverter(@NotNull AttributeDefinition attr) {
boolean containsReference = false;
boolean containsNotReference = false;
Set<String> resourceTypes = new HashSet<String>();
Set<AttributeFormat> formats = attr.getFormats();
for (AttributeFormat format : formats) {
if (format == AttributeFormat.Reference) {
containsReference = true;
}
else {
containsNotReference = true;
}
String type = getResourceType(format);
if (type != null) {
resourceTypes.add(type);
}
}
String specialResourceType = getSpecialResourceType(attr.getName());
if (specialResourceType != null) {
resourceTypes.add(specialResourceType);
}
if (containsReference) {
if (resourceTypes.contains(ResourceType.COLOR.getName())) {
resourceTypes.add(ResourceType.DRAWABLE.getName());
}
if (resourceTypes.size() == 0) {
resourceTypes.addAll(AndroidResourceUtil.getNames(AndroidResourceUtil.REFERRABLE_RESOURCE_TYPES));
}
}
if (resourceTypes.size() > 0) {
final ResourceReferenceConverter converter = new ResourceReferenceConverter(resourceTypes);
converter.setAllowLiterals(containsNotReference);
return converter;
}
return null;
}
@Nullable
public static ResolvingConverter<String> simplify(CompositeConverter composite) {
switch (composite.size()) {
case 0:
return null;
case 1:
return composite.getConverters().get(0);
default:
return composite;
}
}
@Nullable
public static Converter getSpecificConverter(@NotNull XmlName attrName, DomElement context) {
if (context == null) {
return null;
}
if (!SdkConstants.NS_RESOURCES.equals(attrName.getNamespaceKey())) {
return null;
}
final XmlTag xmlTag = context.getXmlTag();
if (xmlTag == null) {
return null;
}
final String localName = attrName.getLocalName();
final String tagName = xmlTag.getName();
if (context instanceof XmlResourceElement) {
if ("configure".equals(localName) && "appwidget-provider".equals(tagName)) {
return ACTIVITY_CONVERTER;
}
else if (VIEW_FRAGMENT.equals(localName)) {
return FRAGMENT_CLASS_CONVERTER;
}
}
else if (context instanceof LayoutViewElement || context instanceof MenuItem) {
if (ATTR_ON_CLICK.equals(localName)) {
return context instanceof LayoutViewElement
? OnClickConverter.CONVERTER_FOR_LAYOUT
: OnClickConverter.CONVERTER_FOR_MENU;
}
}
return null;
}
@Nullable
public static ResolvingConverter getConverter(@NotNull AttributeDefinition attr) {
Set<AttributeFormat> formats = attr.getFormats();
CompositeConverter composite = new CompositeConverter();
String[] values = attr.getValues();
boolean containsUnsupportedFormats = false;
for (AttributeFormat format : formats) {
ResolvingConverter<String> converter = getStringConverter(format, values);
if (converter != null) {
composite.addConverter(converter);
}
else {
containsUnsupportedFormats = true;
}
}
ResourceReferenceConverter resConverter = getResourceReferenceConverter(attr);
if (formats.contains(AttributeFormat.Flag)) {
if (resConverter != null) {
composite.addConverter(new LightFlagConverter(values));
}
return new FlagConverter(simplify(composite), values);
}
if (resConverter == null && formats.contains(AttributeFormat.Enum)) {
resConverter = new ResourceReferenceConverter(Arrays.asList(ResourceType.INTEGER.getName()));
resConverter.setQuiet(true);
}
ResolvingConverter<String> stringConverter = simplify(composite);
if (resConverter != null) {
resConverter.setAdditionalConverter(simplify(composite), containsUnsupportedFormats);
return resConverter;
}
return stringConverter;
}
/** A "special" resource type is just additional information we've manually added about an attribute
* name that augments what attrs.xml and attrs_manifest.xml tell us about the attributes */
@Nullable
public static String getSpecialResourceType(String attrName) {
String type = SPECIAL_RESOURCE_TYPES.get(attrName);
if (type != null) return type;
if (attrName.endsWith("Animation")) return "anim";
return null;
}
// for special cases
static void addSpecialResourceType(String type, String... attrs) {
for (String attr : attrs) {
SPECIAL_RESOURCE_TYPES.put(attr, type);
}
}
public static boolean containsAction(@NotNull IntentFilter filter, @NotNull String name) {
for (Action action : filter.getActions()) {
if (name.equals(action.getName().getValue())) {
return true;
}
}
return false;
}
public static boolean containsCategory(@NotNull IntentFilter filter, @NotNull String name) {
for (Category category : filter.getCategories()) {
if (name.equals(category.getName().getValue())) {
return true;
}
}
return false;
}
@Nullable
public static Activity getActivityDomElementByClass(@NotNull List<Activity> activities, PsiClass c) {
for (Activity activity : activities) {
PsiClass activityClass = activity.getActivityClass().getValue();
if (c.getManager().areElementsEquivalent(c, activityClass)) {
return activity;
}
}
return null;
}
public static String[] getStaticallyDefinedSubtags(@NotNull AndroidDomElement element) {
if (element instanceof ManifestElement) {
return AndroidManifestUtils.getStaticallyDefinedSubtags((ManifestElement)element);
}
if (element instanceof LayoutViewElement) {
return new String[] {VIEW_INCLUDE, REQUEST_FOCUS, TAG};
}
if (element instanceof LayoutElement) {
return new String[]{REQUEST_FOCUS};
}
if (element instanceof Group || element instanceof StringArray || element instanceof IntegerArray || element instanceof Style) {
return new String[]{TAG_ITEM};
}
if (element instanceof MenuItem) {
return new String[]{TAG_MENU};
}
if (element instanceof Menu) {
return new String[]{TAG_ITEM, TAG_GROUP};
}
if (element instanceof Attr) {
return new String[]{TAG_ENUM, TAG_FLAG};
}
if (element instanceof DeclareStyleable) {
return new String[]{TAG_ATTR};
}
if (element instanceof Resources) {
return new String[]{"string", "drawable", "dimen", "color", "style", "string-array", "integer-array", "array", "plurals",
"declare-styleable", "integer", "bool", "attr", "item", "eat-comment"};
}
if (element instanceof StyledText) {
// TODO: The documentation suggests that the allowed tags are <u>, <b> and <i>:
// developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling
// However, the full set of tags accepted by Html.fromHtml is much larger. Therefore,
// instead consider *any* element nested inside a <string> definition to be a markup
// element. See frameworks/base/core/java/android/text/Html.java and look for
// HtmlToSpannedConverter#handleStartTag.
return new String[]{"b", "i", "u"};
}
if (element instanceof PreferenceElement) {
return new String[]{"intent", "extra"};
}
return ArrayUtil.EMPTY_STRING_ARRAY;
}
@Nullable
public static AttributeDefinition getAttributeDefinition(@NotNull AndroidFacet facet, @NotNull XmlAttribute attribute) {
String localName = attribute.getLocalName();
String namespace = attribute.getNamespace();
boolean isFramework = namespace.equals(ANDROID_URI);
if (!isFramework && TOOLS_URI.equals(namespace)) {
// Treat tools namespace attributes as aliases for Android namespaces: see http://tools.android.com/tips/layout-designtime-attributes
isFramework = true;
// However, there are some attributes with other meanings: http://tools.android.com/tech-docs/tools-attributes
// Filter some of these out such that they are not treated as the (unrelated but identically named) platform attributes
if (ATTR_CONTEXT.equals(localName)
|| ATTR_IGNORE.equals(localName)
|| ATTR_LOCALE.equals(localName)
|| ATTR_TARGET_API.equals(localName)) {
return null;
}
}
ResourceManager manager = facet.getResourceManager(isFramework ? SYSTEM_RESOURCE_PACKAGE : null);
if (manager != null) {
AttributeDefinitions attrDefs = manager.getAttributeDefinitions();
if (attrDefs != null) {
return attrDefs.getAttrDefByName(localName);
}
}
return null;
}
@NotNull
public static Collection<String> removeUnambiguousNames(@NotNull Map<String, PsiClass> viewClassMap) {
final Map<String, String> class2Name = new HashMap<String, String>();
for (String tagName : viewClassMap.keySet()) {
final PsiClass viewClass = viewClassMap.get(tagName);
if (!AndroidUtils.isAbstract(viewClass)) {
final String qName = viewClass.getQualifiedName();
final String prevTagName = class2Name.get(qName);
if (prevTagName == null || tagName.indexOf('.') == -1) {
class2Name.put(qName, tagName);
}
}
}
return class2Name.values();
}
@Nullable
public static AndroidResourceReferenceBase getAndroidResourceReference(@Nullable GenericAttributeValue<ResourceValue> attribute,
boolean localOnly) {
if (attribute == null) {
return null;
}
final ResourceValue resValue = attribute.getValue();
if (resValue == null || (localOnly && resValue.getPackage() != null)) {
return null;
}
final XmlAttributeValue value = attribute.getXmlAttributeValue();
if (value == null) {
return null;
}
for (PsiReference reference : value.getReferences()) {
if (reference instanceof AndroidResourceReferenceBase) {
return (AndroidResourceReferenceBase)reference;
}
}
return null;
}
@Nullable
public static AndroidAttributeValue<PsiClass> findComponentDeclarationInManifest(@NotNull PsiClass aClass) {
final AndroidFacet facet = AndroidFacet.getInstance(aClass);
if (facet == null) {
return null;
}
final boolean isActivity = isInheritor(aClass, AndroidUtils.ACTIVITY_BASE_CLASS_NAME);
final boolean isService = isInheritor(aClass, AndroidUtils.SERVICE_CLASS_NAME);
final boolean isReceiver = isInheritor(aClass, AndroidUtils.RECEIVER_CLASS_NAME);
final boolean isProvider = isInheritor(aClass, AndroidUtils.PROVIDER_CLASS_NAME);
if (!isActivity && !isService && !isReceiver && !isProvider) {
return null;
}
final Manifest manifest = facet.getManifest();
if (manifest == null) {
return null;
}
final Application application = manifest.getApplication();
if (application == null) {
return null;
}
if (isActivity) {
for (Activity activity : application.getActivities()) {
final AndroidAttributeValue<PsiClass> activityClass = activity.getActivityClass();
if (activityClass.getValue() == aClass) {
return activityClass;
}
}
}
else if (isService) {
for (Service service : application.getServices()) {
final AndroidAttributeValue<PsiClass> serviceClass = service.getServiceClass();
if (serviceClass.getValue() == aClass) {
return serviceClass;
}
}
}
else if (isReceiver) {
for (Receiver receiver : application.getReceivers()) {
final AndroidAttributeValue<PsiClass> receiverClass = receiver.getReceiverClass();
if (receiverClass.getValue() == aClass) {
return receiverClass;
}
}
}
else {
for (Provider provider : application.getProviders()) {
final AndroidAttributeValue<PsiClass> providerClass = provider.getProviderClass();
if (providerClass.getValue() == aClass) {
return providerClass;
}
}
}
return null;
}
public static boolean isInheritor(@NotNull PsiClass aClass, @NotNull String baseClassQName) {
final Project project = aClass.getProject();
final PsiClass baseClass = JavaPsiFacade.getInstance(project).findClass(baseClassQName, aClass.getResolveScope());
return baseClass != null && aClass.isInheritor(baseClass, true);
}
}