/* * 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(); ResourceManager manager = facet.getResourceManager(attribute.getNamespace().equals(SdkConstants.NS_RESOURCES) ? 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> removeUnambigiousNames(@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); } }