/* * 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.refactoring.rtl; import com.android.SdkConstants; import com.android.resources.ResourceFolderType; import com.android.xml.AndroidManifest; import com.google.common.collect.Maps; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlFile; import com.intellij.psi.xml.XmlTag; import com.intellij.refactoring.BaseRefactoringProcessor; import com.intellij.usageView.UsageInfo; import com.intellij.usageView.UsageViewDescriptor; import com.intellij.util.xml.DomElement; import com.intellij.util.xml.DomManager; import org.jetbrains.android.dom.layout.LayoutDomFileDescription; import org.jetbrains.android.dom.layout.LayoutViewElement; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.android.facet.IdeaSourceProvider; import org.jetbrains.android.facet.ResourceFolderManager; import org.jetbrains.android.util.AndroidBundle; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import static com.android.SdkConstants.*; import static com.android.tools.idea.refactoring.rtl.RtlRefactoringUsageInfo.RtlRefactoringType.*; import static com.android.xml.AndroidManifest.*; public class RtlSupportProcessor extends BaseRefactoringProcessor { private static final Logger LOG = Logger.getInstance("#com.android.tools.idea.refactoring.AddRTLSupportProcessor"); private static final String REFACTORING_NAME = AndroidBundle.message("android.refactoring.rtl.addsupport.title"); public static final String RES_V_QUALIFIER = "-v"; public static final String RES_V17_QUALIFIER = "-v17"; private final RtlSupportProperties myProperties; private final Project myProject; // This is the API level corresponding to the first public release for RTL support public static final int RTL_TARGET_SDK_START = 17; private static Map<String, String> ourMapMirroredAttributeName = Maps.newHashMapWithExpectedSize(12); static { initMapMirroredAttributes(); } private static void initMapMirroredAttributes() { ourMapMirroredAttributeName.put(ATTR_PADDING_LEFT, ATTR_PADDING_START); ourMapMirroredAttributeName.put(ATTR_PADDING_RIGHT, ATTR_PADDING_END); ourMapMirroredAttributeName.put(ATTR_LAYOUT_MARGIN_LEFT, ATTR_LAYOUT_MARGIN_START); ourMapMirroredAttributeName.put(ATTR_LAYOUT_MARGIN_RIGHT, ATTR_LAYOUT_MARGIN_END); ourMapMirroredAttributeName.put(ATTR_DRAWABLE_LEFT, ATTR_DRAWABLE_START); ourMapMirroredAttributeName.put(ATTR_DRAWABLE_RIGHT, ATTR_DRAWABLE_END); ourMapMirroredAttributeName.put(ATTR_LAYOUT_TO_LEFT_OF, ATTR_LAYOUT_TO_START_OF); ourMapMirroredAttributeName.put(ATTR_LAYOUT_TO_RIGHT_OF, ATTR_LAYOUT_TO_END_OF); ourMapMirroredAttributeName.put(ATTR_LAYOUT_ALIGN_LEFT, ATTR_LAYOUT_ALIGN_START); ourMapMirroredAttributeName.put(ATTR_LAYOUT_ALIGN_RIGHT, ATTR_LAYOUT_ALIGN_END); ourMapMirroredAttributeName.put(ATTR_LAYOUT_ALIGN_PARENT_LEFT, ATTR_LAYOUT_ALIGN_PARENT_START); ourMapMirroredAttributeName.put(ATTR_LAYOUT_ALIGN_PARENT_RIGHT, ATTR_LAYOUT_ALIGN_PARENT_END); // Gravity is a special case that we will handled separately as we will mirror its value instead of its name } protected RtlSupportProcessor(Project project, @NotNull RtlSupportProperties properties) { super(project); myProject = project; myProperties = properties; setPreviewUsages(true); } @NotNull @Override protected UsageViewDescriptor createUsageViewDescriptor(UsageInfo[] usages) { return new RtlSupportUsageViewDescriptor(); } @NotNull @Override protected UsageInfo[] findUsages() { if (!myProperties.hasSomethingToDo()) { return UsageInfo.EMPTY_ARRAY; } final List<UsageInfo> list = new ArrayList<UsageInfo>(); if (myProperties.updateAndroidManifest) { addManifestRefactoring(list); // TODO: Update build.gradle as well } if (myProperties.updateLayouts) { addLayoutRefactoring(list); } final int size = list.size(); return list.toArray(new UsageInfo[size]); } @Override protected void performRefactoring(UsageInfo[] usages) { for (UsageInfo usageInfo : usages) { RtlRefactoringUsageInfo refactoring = (RtlRefactoringUsageInfo)usageInfo; switch (refactoring.getType()) { case MANIFEST_SUPPORTS_RTL: performRefactoringForAndroidManifestApplicationTag(refactoring); break; case MANIFEST_TARGET_SDK: performRefactoringForAndroidManifestTargetSdk(refactoring); break; case LAYOUT_FILE_ATTRIBUTE: performRefactoringForLayoutFile(refactoring); break; case UNDEFINED: break; default: assert false : refactoring.getType(); } } } @Override protected void performPsiSpoilingRefactoring() { PsiDocumentManager.getInstance(myProject).commitAllDocuments(); } private void addManifestRefactoring(List<UsageInfo> list) { // For all non library modules in our project for (Module module : ModuleManager.getInstance(myProject).getModules()) { AndroidFacet facet = AndroidFacet.getInstance(module); if (facet == null || facet.isLibraryProject()) { continue; } for (VirtualFile manifestFile : IdeaSourceProvider.getManifestFiles(facet)) { XmlFile manifestPsiFile = (XmlFile)PsiManager.getInstance(myProject).findFile(manifestFile); try { if (manifestPsiFile == null) { continue; } XmlTag root = manifestPsiFile.getRootTag(); if (root == null) { continue; } // First, deal with "supportsRtl" into the <application> tag XmlTag[] applicationNodes = root.findSubTags(NODE_APPLICATION); if (applicationNodes.length > 0) { assert applicationNodes.length == 1; XmlTag applicationTag = applicationNodes[0]; XmlAttribute supportsRtlAttribute = applicationTag.getAttribute(AndroidManifest.ATTRIBUTE_SUPPORTS_RTL, ANDROID_URI); if (supportsRtlAttribute == null || VALUE_FALSE.equals(supportsRtlAttribute.getValue())) { final int startOffset; final int endOffset; if (supportsRtlAttribute == null) { XmlAttribute[] applicationTagAttributes = applicationTag.getAttributes(); XmlAttribute lastAttribute = applicationTagAttributes[applicationTagAttributes.length - 1]; PsiElement nextSibling = lastAttribute.getNextSibling(); assert nextSibling != null; // Will position the caret just before the ">" for the application tag startOffset = nextSibling.getStartOffsetInParent() + nextSibling.getTextLength(); endOffset = startOffset; } else { // Will position the caret at the beginning of the "supportsRtl" attribute startOffset = supportsRtlAttribute.getStartOffsetInParent(); endOffset = startOffset + supportsRtlAttribute.getTextLength(); } RtlRefactoringUsageInfo usageInfo = new RtlRefactoringUsageInfo(applicationTag, startOffset, endOffset); usageInfo.setType(MANIFEST_SUPPORTS_RTL); list.add(usageInfo); } } // Second, deal with targetSdkVersion / minSdkVersion XmlTag[] usesSdkNodes = root.findSubTags(NODE_USES_SDK); if (usesSdkNodes.length > 0) { assert usesSdkNodes.length == 1; XmlTag usesSdkTag = usesSdkNodes[0]; XmlAttribute targetSdkAttribute = usesSdkTag.getAttribute(ATTRIBUTE_TARGET_SDK_VERSION, ANDROID_URI); int targetSdk = (targetSdkAttribute != null) ? Integer.parseInt(targetSdkAttribute.getValue()) : 0; // Will need to set existing targetSdkVersion to 17 if (targetSdk == 0 || targetSdk < RTL_TARGET_SDK_START) { // Will position the caret just at the start of final int startOffset = (targetSdkAttribute != null) ? targetSdkAttribute.getStartOffsetInParent() : usesSdkTag.getStartOffsetInParent(); final int endOffset = startOffset + ((targetSdkAttribute != null) ? targetSdkAttribute.getTextLength() : usesSdkTag.getTextLength()); RtlRefactoringUsageInfo usageInfo = new RtlRefactoringUsageInfo(usesSdkTag, startOffset, endOffset); usageInfo.setType(MANIFEST_TARGET_SDK); list.add(usageInfo); } } } catch (Exception e) { LOG.error("Could not read Manifest data", e); } } } } private static String quote(String str) { return quoteWith(str, "'"); } private static String quoteWith(String str, String quote) { return quote + str + quote; } @Nullable private VirtualFile getLayoutV17(final VirtualFile oneLayoutRes, boolean bCreateIfNeeded) { final String resName = oneLayoutRes.getName(); if (resName.contains(RES_V_QUALIFIER)) { return null; } final String resNameWithV17 = resName + RES_V17_QUALIFIER; final VirtualFile parent = oneLayoutRes.getParent(); assert parent != null; VirtualFile layoutV17Dir = parent.findChild(resNameWithV17); if ((layoutV17Dir == null || !layoutV17Dir.exists()) && bCreateIfNeeded) { try { layoutV17Dir = parent.createChildDirectory(this, resNameWithV17); } catch (IOException e) { LOG.error("Cannot create " + quote(resNameWithV17) + " directory in resource directory: " + parent.getName()); } } if (layoutV17Dir != null) { assert layoutV17Dir.isDirectory() : layoutV17Dir; } return layoutV17Dir; } private List<UsageInfo> getLayoutRefactoringForOneDir(@NotNull VirtualFile layoutDir, boolean createV17, int minSdk) { List<UsageInfo> result = new ArrayList<UsageInfo>(); final VirtualFile[] layoutChildren = layoutDir.getChildren(); for (final VirtualFile oneLayoutFile : layoutChildren) { result.addAll(getLayoutRefactoringForOneFile(oneLayoutFile, createV17, minSdk)); } return result; } private List<UsageInfo> getLayoutRefactoringForOneFile(@NotNull VirtualFile layoutFile, boolean createV17, int minSdk) { final PsiFile psiFile = PsiManager.getInstance(myProject).findFile(layoutFile); assert psiFile != null; return getLayoutRefactoringForFile(psiFile, createV17, minSdk); } private void addLayoutRefactoring(List<UsageInfo> list) { // For all non library modules in our project for (Module module : ModuleManager.getInstance(myProject).getModules()) { AndroidFacet facet = AndroidFacet.getInstance(module); if (facet != null && !facet.isLibraryProject()) { int minSdk = facet.getAndroidModuleInfo().getMinSdkVersion().getApiLevel(); if (myProperties.generateV17resourcesOption) { // First get all the "res" directories final List<VirtualFile> allRes = facet.getAllResourceDirectories(); // Then, need to get all the "layout-XXX" sub directories final List<VirtualFile> allLayoutDir = new ArrayList<VirtualFile>(); for (VirtualFile oneRes : allRes) { if (ResourceFolderManager.isLibraryResourceRoot(oneRes)) { continue; } final VirtualFile[] children = oneRes.getChildren(); // Check every children if they are a layout dir but not a "-v17" one for (VirtualFile oneChild : children) { final String childName = oneChild.getName(); if (childName.startsWith(FD_RES_LAYOUT) && !childName.contains(RES_V_QUALIFIER)) { allLayoutDir.add(oneChild); } } } // For all "layout-XXX" entries, process all the contained files for (final VirtualFile layoutDir : allLayoutDir) { final VirtualFile layoutV17Dir = getLayoutV17(layoutDir, false /* no creation */); // The corresponding "v17" directory already exists if (layoutV17Dir != null) { // ... so add refactoring for all files in the "v17" directory if needed if (layoutV17Dir.getChildren().length != 0) { list.addAll(getLayoutRefactoringForOneDir(layoutV17Dir, false /* do not create v17 version */, minSdk)); } else { list.addAll(getLayoutRefactoringForOneDir(layoutDir, true /* create v17 version */, minSdk)); } } else { // otherwise all refactoring for all the non "v17" file and will create the "v17" file later on (we *cannot* // create them here even with a ApplicationManager.getApplication().runWriteAction(...) list.addAll(getLayoutRefactoringForOneDir(layoutDir, true /* create the v17 version */, minSdk)); } } } else { final List<PsiFile> files = facet.getLocalResourceManager().findResourceFiles(ResourceFolderType.LAYOUT.getName()); for (PsiFile psiFile : files) { if (ResourceFolderManager.isLibraryResourceFile(psiFile.getVirtualFile())) { continue; } list.addAll(getLayoutRefactoringForFile(psiFile, false /* do not create the v17 version */, minSdk)); } } } } } private List<UsageInfo> getLayoutRefactoringForFile(@NotNull final PsiFile layoutFile, final boolean createV17, final int minSdk) { final List<UsageInfo> result = new ArrayList<UsageInfo>(); if (layoutFile instanceof XmlFile && DomManager.getDomManager(myProject).getDomFileDescription((XmlFile)layoutFile) instanceof LayoutDomFileDescription) { layoutFile.accept(new XmlRecursiveElementVisitor() { @Override public void visitXmlTag(XmlTag tag) { super.visitXmlTag(tag); List<UsageInfo> usageInfos = getLayoutRefactoringForTag(tag, createV17, minSdk); if (usageInfos.isEmpty()) { return; } result.addAll(usageInfos); } }); } return result; } private List<UsageInfo> getLayoutRefactoringForTag(@NotNull XmlTag tag, boolean createV17, int minSdk) { final DomElement domElement = DomManager.getDomManager(myProject).getDomElement(tag); if (!(domElement instanceof LayoutViewElement)) { return Collections.emptyList(); } final List<UsageInfo> result = new ArrayList<UsageInfo>(); final XmlAttribute[] attributes = tag.getAttributes(); for (XmlAttribute attributeToMirror : attributes) { final String localName = attributeToMirror.getLocalName(); final String namespacePrefix = attributeToMirror.getNamespacePrefix(); final String mirroredLocalName = ourMapMirroredAttributeName.get(localName); // Check if this is a RTL attribute to mirror or if it is a Gravity attribute if (mirroredLocalName != null) { // Mirror only attributes that has not been mirrored before final XmlAttribute attributeMirrored = tag.getAttribute(namespacePrefix + ":" + mirroredLocalName); if (attributeMirrored == null) { final int startOffset = 0; final int endOffset = attributeToMirror.getTextLength(); RtlRefactoringUsageInfo usageInfoForAttribute = new RtlRefactoringUsageInfo(attributeToMirror, startOffset, endOffset); usageInfoForAttribute.setType(LAYOUT_FILE_ATTRIBUTE); usageInfoForAttribute.setCreateV17(createV17); usageInfoForAttribute.setAndroidManifestMinSdkVersion(minSdk); result.add(usageInfoForAttribute); } } else if (localName.equals(ATTR_GRAVITY) || localName.equals(ATTR_LAYOUT_GRAVITY)) { final String value = attributeToMirror.getValue(); if (value != null && (value.contains(GRAVITY_VALUE_LEFT) || value.contains(GRAVITY_VALUE_RIGHT))) { final int startOffset = 0; final int endOffset = attributeToMirror.getTextLength(); RtlRefactoringUsageInfo usageInfoForAttribute = new RtlRefactoringUsageInfo(attributeToMirror, startOffset, endOffset); usageInfoForAttribute.setType(LAYOUT_FILE_ATTRIBUTE); usageInfoForAttribute.setCreateV17(createV17); result.add(usageInfoForAttribute); } } } return result; } private static void performRefactoringForAndroidManifestApplicationTag(@NotNull UsageInfo usageInfo) { PsiElement element = usageInfo.getElement(); assert element != null; XmlTag applicationTag = (XmlTag)element; XmlAttribute supportsRtlAttribute = applicationTag.getAttribute(ATTRIBUTE_SUPPORTS_RTL, ANDROID_URI); if (supportsRtlAttribute != null) { supportsRtlAttribute.setValue(SdkConstants.VALUE_TRUE); } else { applicationTag.setAttribute(ATTRIBUTE_SUPPORTS_RTL, ANDROID_URI, SdkConstants.VALUE_TRUE); } } private static void performRefactoringForAndroidManifestTargetSdk(@NotNull UsageInfo usageInfo) { PsiElement element = usageInfo.getElement(); assert element != null; XmlTag usesSdkTag = (XmlTag)element; XmlAttribute targetSdkAttribute = usesSdkTag.getAttribute(ATTRIBUTE_TARGET_SDK_VERSION, ANDROID_URI); if (targetSdkAttribute != null) { targetSdkAttribute.setValue(Integer.toString(RTL_TARGET_SDK_START)); } else { usesSdkTag.setAttribute(ATTRIBUTE_TARGET_SDK_VERSION, ANDROID_URI, Integer.toString(RTL_TARGET_SDK_START)); } } private void performRefactoringForLayoutFile(@NotNull final RtlRefactoringUsageInfo usageInfo) { final PsiElement element = usageInfo.getElement(); assert element != null; final XmlAttribute attribute = (XmlAttribute)element; final int minSdk = usageInfo.getAndroidManifestMinSdkVersion(); if (!usageInfo.isCreateV17()) { updateAttributeForElement(attribute, minSdk); } else { // We need first to create the v17 layout file, so first get our initial layout file final PsiFile psiFile = element.getContainingFile(); final VirtualFile layoutFile = psiFile.getVirtualFile(); assert layoutFile != null; final VirtualFile layoutDir = layoutFile.getParent(); assert layoutDir != null; final VirtualFile layoutV17Dir = getLayoutV17(layoutDir, true /* create if needed */); assert layoutV17Dir != null; final String layoutFileName = layoutFile.getName(); // Create the v17 file if needed (should be done only once) if (layoutV17Dir.findChild(layoutFileName) == null) { ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { try { layoutFile.copy(this, layoutV17Dir, layoutFileName); } catch (IOException e) { LOG.error("Cannot copy layout file " + quote(layoutFileName) + " from " + quote(layoutDir.getName()) + " directory to " + quote(layoutV17Dir.getName()) + " directory"); } } }); } final VirtualFile layoutV17File = layoutV17Dir.findChild(layoutFileName); assert layoutV17File != null; final XmlFile xmlV17File = (XmlFile)PsiManager.getInstance(myProject).findFile(layoutV17File); assert xmlV17File != null; LOG.info("Processing refactoring for attribute: " + attribute.getName() + " into file: " + layoutV17File.getPath()); if (DomManager.getDomManager(myProject).getDomFileDescription((XmlFile)xmlV17File) instanceof LayoutDomFileDescription) { xmlV17File.accept(new XmlRecursiveElementVisitor() { @Override public void visitXmlTag(XmlTag tag) { super.visitXmlTag(tag); final XmlAttribute attribute = tag.getAttribute(((XmlAttribute)element).getName()); if (attribute == null) { return; } updateAttributeForElement(attribute, minSdk); } }); } layoutV17File.refresh(true /* asynchronous */, false /* not recursive */); } } private void updateAttributeForElement(@NotNull XmlAttribute attribute, int minSdk) { final String attributeLocalName = attribute.getLocalName(); LOG.info("Updating attribute name: " + attributeLocalName + " value: " + attribute.getValue()); if (attributeLocalName.equals(ATTR_GRAVITY) || attributeLocalName.equals(ATTR_LAYOUT_GRAVITY)) { // Special case for android:gravity and android:layout_gravity final String value = StringUtil.notNullize(attribute.getValue()); final String newValue = value.replace(GRAVITY_VALUE_LEFT, GRAVITY_VALUE_START).replace(GRAVITY_VALUE_RIGHT, GRAVITY_VALUE_END); attribute.setValue(newValue); LOG.info("Changing gravity from: " + value + " to: " + newValue); } else { // General case for RTL attributes final String mirroredAttributeLocalName = ourMapMirroredAttributeName.get(attributeLocalName); if (mirroredAttributeLocalName == null) { LOG.warn("Cannot mirror attribute: " + attribute.toString()); return; } final String mirroredAttributeName = attribute.getNamespacePrefix() + ":" + mirroredAttributeLocalName; XmlAttribute attributeForUpdatingValue; if (myProperties.replaceLeftRightPropertiesOption) { attribute.setName(mirroredAttributeName); LOG.info("Replacing attribute name from: " + attributeLocalName + " to: " + mirroredAttributeLocalName); attributeForUpdatingValue = attribute; } else { XmlTag parent = attribute.getParent(); attributeForUpdatingValue = parent.setAttribute(mirroredAttributeName, StringUtil.notNullize(attribute.getValue())); LOG.info("Adding attribute name: " + mirroredAttributeName + " value: " + attribute.getValue()); } // Special case for updating attribute value updateAttributeValueIfNeeded(attributeForUpdatingValue, minSdk); } } private static void updateAttributeValueIfNeeded(@NotNull XmlAttribute attribute, int minSdk) { final String attributeLocalName = attribute.getLocalName(); final String value = StringUtil.notNullize(attribute.getValue()); if (attributeLocalName.equals(ATTR_PADDING_LEFT) || attributeLocalName.equals(ATTR_PADDING_RIGHT) || attributeLocalName.equals(ATTR_PADDING_START) || attributeLocalName.equals(ATTR_PADDING_END)) { if (minSdk >= RTL_TARGET_SDK_START && (value.contains(ATTR_LIST_PREFERRED_ITEM_PADDING_LEFT) || value.contains(ATTR_LIST_PREFERRED_ITEM_PADDING_RIGHT))) { final String newValue = value.replace(ATTR_LIST_PREFERRED_ITEM_PADDING_LEFT, ATTR_LIST_PREFERRED_ITEM_PADDING_START). replace(ATTR_LIST_PREFERRED_ITEM_PADDING_RIGHT, ATTR_LIST_PREFERRED_ITEM_PADDING_END); attribute.setValue(newValue); LOG.info("Changing attribute value from: " + value + " to: " + newValue); } } } @Override protected String getCommandName() { return REFACTORING_NAME; } }