com.android.ide.eclipse.adt.AdtUtils.java Source code

Java tutorial

Introduction

Here is the source code for com.android.ide.eclipse.adt.AdtUtils.java

Source

/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
 *
 * 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.ide.eclipse.adt;

import static com.android.SdkConstants.TOOLS_PREFIX;
import static com.android.SdkConstants.TOOLS_URI;
import static org.eclipse.ui.IWorkbenchPage.MATCH_INPUT;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.sdklib.SdkVersionInfo;
import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper.IProjectFilter;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.repository.PkgProps;
import com.android.utils.XmlUtils;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closeables;

import org.eclipse.core.filebuffers.FileBuffers;
import org.eclipse.core.filebuffers.ITextFileBuffer;
import org.eclipse.core.filebuffers.ITextFileBufferManager;
import org.eclipse.core.filebuffers.LocationKind;
import org.eclipse.core.filesystem.URIUtil;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorReference;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IURIEditorInput;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.editors.text.TextFileDocumentProvider;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.eclipse.ui.texteditor.ITextEditor;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import java.io.File;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;

/** Utility methods for ADT */
@SuppressWarnings("restriction") // WST API
public class AdtUtils {
    /**
     * Creates a Java class name out of the given string, if possible. For
     * example, "My Project" becomes "MyProject", "hello" becomes "Hello",
     * "Java's" becomes "Java", and so on.
     *
     * @param string the string to be massaged into a Java class
     * @return the string as a Java class, or null if a class name could not be
     *         extracted
     */
    @Nullable
    public static String extractClassName(@NonNull String string) {
        StringBuilder sb = new StringBuilder(string.length());
        int n = string.length();

        int i = 0;
        for (; i < n; i++) {
            char c = Character.toUpperCase(string.charAt(i));
            if (Character.isJavaIdentifierStart(c)) {
                sb.append(c);
                i++;
                break;
            }
        }
        if (sb.length() > 0) {
            for (; i < n; i++) {
                char c = string.charAt(i);
                if (Character.isJavaIdentifierPart(c)) {
                    sb.append(c);
                }
            }

            return sb.toString();
        }

        return null;
    }

    /**
     * Strips off the last file extension from the given filename, e.g.
     * "foo.backup.diff" will be turned into "foo.backup".
     * <p>
     * Note that dot files (e.g. ".profile") will be left alone.
     *
     * @param filename the filename to be stripped
     * @return the filename without the last file extension.
     */
    public static String stripLastExtension(String filename) {
        int dotIndex = filename.lastIndexOf('.');
        if (dotIndex > 0) { // > 0 instead of != -1: Treat dot files (e.g. .profile) differently
            return filename.substring(0, dotIndex);
        } else {
            return filename;
        }
    }

    /**
     * Strips off all extensions from the given filename, e.g. "foo.9.png" will
     * be turned into "foo".
     * <p>
     * Note that dot files (e.g. ".profile") will be left alone.
     *
     * @param filename the filename to be stripped
     * @return the filename without any file extensions
     */
    public static String stripAllExtensions(String filename) {
        int dotIndex = filename.indexOf('.');
        if (dotIndex > 0) { // > 0 instead of != -1: Treat dot files (e.g. .profile) differently
            return filename.substring(0, dotIndex);
        } else {
            return filename;
        }
    }

    /**
     * Strips the given suffix from the given string, provided that the string ends with
     * the suffix.
     *
     * @param string the full string to strip from
     * @param suffix the suffix to strip out
     * @return the string without the suffix at the end
     */
    public static String stripSuffix(@NonNull String string, @NonNull String suffix) {
        if (string.endsWith(suffix)) {
            return string.substring(0, string.length() - suffix.length());
        }

        return string;
    }

    /**
     * Capitalizes the string, i.e. transforms the initial [a-z] into [A-Z].
     * Returns the string unmodified if the first character is not [a-z].
     *
     * @param str The string to capitalize.
     * @return The capitalized string
     */
    public static String capitalize(String str) {
        if (str == null || str.length() < 1 || Character.isUpperCase(str.charAt(0))) {
            return str;
        }

        StringBuilder sb = new StringBuilder();
        sb.append(Character.toUpperCase(str.charAt(0)));
        sb.append(str.substring(1));
        return sb.toString();
    }

    /**
     * Converts a CamelCase word into an underlined_word
     *
     * @param string the CamelCase version of the word
     * @return the underlined version of the word
     */
    public static String camelCaseToUnderlines(String string) {
        if (string.isEmpty()) {
            return string;
        }

        StringBuilder sb = new StringBuilder(2 * string.length());
        int n = string.length();
        boolean lastWasUpperCase = Character.isUpperCase(string.charAt(0));
        for (int i = 0; i < n; i++) {
            char c = string.charAt(i);
            boolean isUpperCase = Character.isUpperCase(c);
            if (isUpperCase && !lastWasUpperCase) {
                sb.append('_');
            }
            lastWasUpperCase = isUpperCase;
            c = Character.toLowerCase(c);
            sb.append(c);
        }

        return sb.toString();
    }

    /**
     * Converts an underlined_word into a CamelCase word
     *
     * @param string the underlined word to convert
     * @return the CamelCase version of the word
     */
    public static String underlinesToCamelCase(String string) {
        StringBuilder sb = new StringBuilder(string.length());
        int n = string.length();

        int i = 0;
        boolean upcaseNext = true;
        for (; i < n; i++) {
            char c = string.charAt(i);
            if (c == '_') {
                upcaseNext = true;
            } else {
                if (upcaseNext) {
                    c = Character.toUpperCase(c);
                }
                upcaseNext = false;
                sb.append(c);
            }
        }

        return sb.toString();
    }

    /**
     * Returns the current editor (the currently visible and active editor), or null if
     * not found
     *
     * @return the current editor, or null
     */
    public static IEditorPart getActiveEditor() {
        IWorkbenchWindow window = getActiveWorkbenchWindow();
        if (window != null) {
            IWorkbenchPage page = window.getActivePage();
            if (page != null) {
                return page.getActiveEditor();
            }
        }

        return null;
    }

    /**
     * Returns the current active workbench, or null if not found
     *
     * @return the current window, or null
     */
    @Nullable
    public static IWorkbenchWindow getActiveWorkbenchWindow() {
        IWorkbench workbench = PlatformUI.getWorkbench();
        IWorkbenchWindow window = workbench.getActiveWorkbenchWindow();
        if (window == null) {
            IWorkbenchWindow[] windows = workbench.getWorkbenchWindows();
            if (windows.length > 0) {
                window = windows[0];
            }
        }

        return window;
    }

    /**
     * Returns the current active workbench page, or null if not found
     *
     * @return the current page, or null
     */
    @Nullable
    public static IWorkbenchPage getActiveWorkbenchPage() {
        IWorkbenchWindow window = getActiveWorkbenchWindow();
        if (window != null) {
            IWorkbenchPage page = window.getActivePage();
            if (page == null) {
                IWorkbenchPage[] pages = window.getPages();
                if (pages.length > 0) {
                    page = pages[0];
                }
            }

            return page;
        }

        return null;
    }

    /**
     * Returns the current active workbench part, or null if not found
     *
     * @return the current active workbench part, or null
     */
    @Nullable
    public static IWorkbenchPart getActivePart() {
        IWorkbenchWindow window = getActiveWorkbenchWindow();
        if (window != null) {
            IWorkbenchPage activePage = window.getActivePage();
            if (activePage != null) {
                return activePage.getActivePart();
            }
        }
        return null;
    }

    /**
     * Returns the current text editor (the currently visible and active editor), or null
     * if not found.
     *
     * @return the current text editor, or null
     */
    public static ITextEditor getActiveTextEditor() {
        IEditorPart editor = getActiveEditor();
        if (editor != null) {
            if (editor instanceof ITextEditor) {
                return (ITextEditor) editor;
            } else {
                return (ITextEditor) editor.getAdapter(ITextEditor.class);
            }
        }

        return null;
    }

    /**
     * Looks through the open editors and returns the editors that have the
     * given file as input.
     *
     * @param file the file to search for
     * @param restore whether editors should be restored (if they have an open
     *            tab, but the editor hasn't been restored since the most recent
     *            IDE start yet
     * @return a collection of editors
     */
    @NonNull
    public static Collection<IEditorPart> findEditorsFor(@NonNull IFile file, boolean restore) {
        FileEditorInput input = new FileEditorInput(file);
        List<IEditorPart> result = null;
        IWorkbench workbench = PlatformUI.getWorkbench();
        IWorkbenchWindow[] windows = workbench.getWorkbenchWindows();
        for (IWorkbenchWindow window : windows) {
            IWorkbenchPage[] pages = window.getPages();
            for (IWorkbenchPage page : pages) {
                IEditorReference[] editors = page.findEditors(input, null, MATCH_INPUT);
                if (editors != null) {
                    for (IEditorReference reference : editors) {
                        IEditorPart editor = reference.getEditor(restore);
                        if (editor != null) {
                            if (result == null) {
                                result = new ArrayList<IEditorPart>();
                            }
                            result.add(editor);
                        }
                    }
                }
            }
        }

        if (result == null) {
            return Collections.emptyList();
        }

        return result;
    }

    /**
     * Attempts to convert the given {@link URL} into a {@link File}.
     *
     * @param url the {@link URL} to be converted
     * @return the corresponding {@link File}, which may not exist
     */
    @NonNull
    public static File getFile(@NonNull URL url) {
        try {
            // First try URL.toURI(): this will work for URLs that contain %20 for spaces etc.
            // Unfortunately, it *doesn't* work for "broken" URLs where the URL contains
            // spaces, which is often the case.
            return new File(url.toURI());
        } catch (URISyntaxException e) {
            // ...so as a fallback, go to the old url.getPath() method, which handles space paths.
            return new File(url.getPath());
        }
    }

    /**
     * Returns the file for the current editor, if any.
     *
     * @return the file for the current editor, or null if none
     */
    public static IFile getActiveFile() {
        IEditorPart editor = getActiveEditor();
        if (editor != null) {
            IEditorInput input = editor.getEditorInput();
            if (input instanceof IFileEditorInput) {
                IFileEditorInput fileInput = (IFileEditorInput) input;
                return fileInput.getFile();
            }
        }

        return null;
    }

    /**
     * Returns an absolute path to the given resource
     *
     * @param resource the resource to look up a path for
     * @return an absolute file system path to the resource
     */
    @NonNull
    public static IPath getAbsolutePath(@NonNull IResource resource) {
        IPath location = resource.getRawLocation();
        if (location != null) {
            return location.makeAbsolute();
        } else {
            IWorkspace workspace = ResourcesPlugin.getWorkspace();
            IWorkspaceRoot root = workspace.getRoot();
            IPath workspacePath = root.getLocation();
            return workspacePath.append(resource.getFullPath());
        }
    }

    /**
     * Converts a workspace-relative path to an absolute file path
     *
     * @param path the workspace-relative path to convert
     * @return the corresponding absolute file in the file system
     */
    @NonNull
    public static File workspacePathToFile(@NonNull IPath path) {
        IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
        IResource res = root.findMember(path);
        if (res != null) {
            IPath location = res.getLocation();
            if (location != null) {
                return location.toFile();
            }
            return root.getLocation().append(path).toFile();
        }

        return path.toFile();
    }

    /**
     * Converts a {@link File} to an {@link IFile}, if possible.
     *
     * @param file a file to be converted
     * @return the corresponding {@link IFile}, or null
     */
    public static IFile fileToIFile(File file) {
        if (!file.isAbsolute()) {
            file = file.getAbsoluteFile();
        }

        IWorkspaceRoot workspace = ResourcesPlugin.getWorkspace().getRoot();
        IFile[] files = workspace.findFilesForLocationURI(file.toURI());
        if (files.length > 0) {
            return files[0];
        }

        IPath filePath = new Path(file.getPath());
        return pathToIFile(filePath);
    }

    /**
     * Converts a {@link File} to an {@link IResource}, if possible.
     *
     * @param file a file to be converted
     * @return the corresponding {@link IResource}, or null
     */
    public static IResource fileToResource(File file) {
        if (!file.isAbsolute()) {
            file = file.getAbsoluteFile();
        }

        IWorkspaceRoot workspace = ResourcesPlugin.getWorkspace().getRoot();
        IFile[] files = workspace.findFilesForLocationURI(file.toURI());
        if (files.length > 0) {
            return files[0];
        }

        IPath filePath = new Path(file.getPath());
        return pathToResource(filePath);
    }

    /**
     * Converts a {@link IPath} to an {@link IFile}, if possible.
     *
     * @param path a path to be converted
     * @return the corresponding {@link IFile}, or null
     */
    public static IFile pathToIFile(IPath path) {
        IWorkspaceRoot workspace = ResourcesPlugin.getWorkspace().getRoot();

        IFile[] files = workspace.findFilesForLocationURI(URIUtil.toURI(path.makeAbsolute()));
        if (files.length > 0) {
            return files[0];
        }

        IPath workspacePath = workspace.getLocation();
        if (workspacePath.isPrefixOf(path)) {
            IPath relativePath = path.makeRelativeTo(workspacePath);
            IResource member = workspace.findMember(relativePath);
            if (member instanceof IFile) {
                return (IFile) member;
            }
        } else if (path.isAbsolute()) {
            return workspace.getFileForLocation(path);
        }

        return null;
    }

    /**
     * Converts a {@link IPath} to an {@link IResource}, if possible.
     *
     * @param path a path to be converted
     * @return the corresponding {@link IResource}, or null
     */
    public static IResource pathToResource(IPath path) {
        IWorkspaceRoot workspace = ResourcesPlugin.getWorkspace().getRoot();

        IFile[] files = workspace.findFilesForLocationURI(URIUtil.toURI(path.makeAbsolute()));
        if (files.length > 0) {
            return files[0];
        }

        IPath workspacePath = workspace.getLocation();
        if (workspacePath.isPrefixOf(path)) {
            IPath relativePath = path.makeRelativeTo(workspacePath);
            return workspace.findMember(relativePath);
        } else if (path.isAbsolute()) {
            return workspace.getFileForLocation(path);
        }

        return null;
    }

    /**
     * Returns all markers in a file/document that fit on the same line as the given offset
     *
     * @param markerType the marker type
     * @param file the file containing the markers
     * @param document the document showing the markers
     * @param offset the offset to be checked
     * @return a list (possibly empty but never null) of matching markers
     */
    @NonNull
    public static List<IMarker> findMarkersOnLine(@NonNull String markerType, @NonNull IResource file,
            @NonNull IDocument document, int offset) {
        List<IMarker> matchingMarkers = new ArrayList<IMarker>(2);
        try {
            IMarker[] markers = file.findMarkers(markerType, true, IResource.DEPTH_ZERO);

            // Look for a match on the same line as the caret.
            IRegion lineInfo = document.getLineInformationOfOffset(offset);
            int lineStart = lineInfo.getOffset();
            int lineEnd = lineStart + lineInfo.getLength();
            int offsetLine = document.getLineOfOffset(offset);

            for (IMarker marker : markers) {
                int start = marker.getAttribute(IMarker.CHAR_START, -1);
                int end = marker.getAttribute(IMarker.CHAR_END, -1);
                if (start >= lineStart && start <= lineEnd && end > start) {
                    matchingMarkers.add(marker);
                } else if (start == -1 && end == -1) {
                    // Some markers don't set character range, they only set the line
                    int line = marker.getAttribute(IMarker.LINE_NUMBER, -1);
                    if (line == offsetLine + 1) {
                        matchingMarkers.add(marker);
                    }
                }
            }
        } catch (CoreException e) {
            AdtPlugin.log(e, null);
        } catch (BadLocationException e) {
            AdtPlugin.log(e, null);
        }

        return matchingMarkers;
    }

    /**
     * Returns the available and open Android projects
     *
     * @return the available and open Android projects, never null
     */
    @NonNull
    public static IJavaProject[] getOpenAndroidProjects() {
        return BaseProjectHelper.getAndroidProjects(new IProjectFilter() {
            @Override
            public boolean accept(IProject project) {
                return project.isAccessible();
            }
        });
    }

    /**
     * Returns a unique project name, based on the given {@code base} file name
     * possibly with a {@code conjunction} and a new number behind it to ensure
     * that the project name is unique. For example,
     * {@code getUniqueProjectName("project", "_")} will return
     * {@code "project"} if that name does not already exist, and if it does, it
     * will return {@code "project_2"}.
     *
     * @param base the base name to use, such as "foo"
     * @param conjunction a string to insert between the base name and the
     *            number.
     * @return a unique project name based on the given base and conjunction
     */
    public static String getUniqueProjectName(String base, String conjunction) {
        // We're using all workspace projects here rather than just open Android project
        // via getOpenAndroidProjects because the name cannot conflict with non-Android
        // or closed projects either
        IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
        IProject[] projects = workspaceRoot.getProjects();

        for (int i = 1; i < 1000; i++) {
            String name = i == 1 ? base : base + conjunction + Integer.toString(i);
            boolean found = false;
            for (IProject project : projects) {
                // Need to make case insensitive comparison, since otherwise we can hit
                // org.eclipse.core.internal.resources.ResourceException:
                // A resource exists with a different case: '/test'.
                if (project.getName().equalsIgnoreCase(name)) {
                    found = true;
                    break;
                }
            }
            if (!found) {
                return name;
            }
        }

        return base;
    }

    /**
     * Returns the name of the parent folder for the given editor input
     *
     * @param editorInput the editor input to check
     * @return the parent folder, which is never null but may be ""
     */
    @NonNull
    public static String getParentFolderName(@Nullable IEditorInput editorInput) {
        if (editorInput instanceof IFileEditorInput) {
            IFile file = ((IFileEditorInput) editorInput).getFile();
            return file.getParent().getName();
        }

        if (editorInput instanceof IURIEditorInput) {
            IURIEditorInput urlEditorInput = (IURIEditorInput) editorInput;
            String path = urlEditorInput.getURI().toString();
            int lastIndex = path.lastIndexOf('/');
            if (lastIndex != -1) {
                int lastLastIndex = path.lastIndexOf('/', lastIndex - 1);
                if (lastLastIndex != -1) {
                    return path.substring(lastLastIndex + 1, lastIndex);
                }
            }
        }

        return "";
    }

    /**
     * Returns the XML editor for the given editor part
     *
     * @param part the editor part to look up the editor for
     * @return the editor or null if this part is not an XML editor
     */
    @Nullable
    public static AndroidXmlEditor getXmlEditor(@NonNull IEditorPart part) {
        if (part instanceof AndroidXmlEditor) {
            return (AndroidXmlEditor) part;
        } else if (part instanceof GraphicalEditorPart) {
            ((GraphicalEditorPart) part).getEditorDelegate().getEditor();
        }

        return null;
    }

    /**
     * Sets the given tools: attribute in the given XML editor document, adding
     * the tools name space declaration if necessary, formatting the affected
     * document region, and optionally comma-appending to an existing value and
     * optionally opening and revealing the attribute.
     *
     * @param editor the associated editor
     * @param element the associated element
     * @param description the description of the attribute (shown in the undo
     *            event)
     * @param name the name of the attribute
     * @param value the attribute value
     * @param reveal if true, open the editor and select the given attribute
     *            node
     * @param appendValue if true, add this value as a comma separated value to
     *            the existing attribute value, if any
     */
    public static void setToolsAttribute(@NonNull final AndroidXmlEditor editor, @NonNull final Element element,
            @NonNull final String description, @NonNull final String name, @Nullable final String value,
            final boolean reveal, final boolean appendValue) {
        editor.wrapUndoEditXmlModel(description, new Runnable() {
            @Override
            public void run() {
                String prefix = XmlUtils.lookupNamespacePrefix(element, TOOLS_URI, null, true);
                if (prefix == null) {
                    // Add in new prefix...
                    prefix = XmlUtils.lookupNamespacePrefix(element, TOOLS_URI, TOOLS_PREFIX, true /*create*/);
                    if (value != null) {
                        // ...and ensure that the header is formatted such that
                        // the XML namespace declaration is placed in the right
                        // position and wrapping is applied etc.
                        editor.scheduleNodeReformat(editor.getUiRootNode(), true /*attributesOnly*/);
                    }
                }

                String v = value;
                if (appendValue && v != null) {
                    String prev = element.getAttributeNS(TOOLS_URI, name);
                    if (prev.length() > 0) {
                        v = prev + ',' + value;
                    }
                }

                // Use the non-namespace form of set attribute since we can't
                // reference the namespace until the model has been reloaded
                if (v != null) {
                    element.setAttribute(prefix + ':' + name, v);
                } else {
                    element.removeAttribute(prefix + ':' + name);
                }

                UiElementNode rootUiNode = editor.getUiRootNode();
                if (rootUiNode != null && v != null) {
                    final UiElementNode uiNode = rootUiNode.findXmlNode(element);
                    if (uiNode != null) {
                        editor.scheduleNodeReformat(uiNode, true /*attributesOnly*/);

                        if (reveal) {
                            // Update editor selection after format
                            Display display = AdtPlugin.getDisplay();
                            if (display != null) {
                                display.asyncExec(new Runnable() {
                                    @Override
                                    public void run() {
                                        Node xmlNode = uiNode.getXmlNode();
                                        Attr attribute = ((Element) xmlNode).getAttributeNodeNS(TOOLS_URI, name);
                                        if (attribute instanceof IndexedRegion) {
                                            IndexedRegion region = (IndexedRegion) attribute;
                                            editor.getStructuredTextEditor()
                                                    .selectAndReveal(region.getStartOffset(), region.getLength());
                                        }
                                    }
                                });
                            }
                        }
                    }
                }
            }
        });
    }

    /**
     * Returns a string label for the given target, of the form
     * "API 16: Android 4.1 (Jelly Bean)".
     *
     * @param target the target to generate a string from
     * @return a suitable display string
     */
    @NonNull
    public static String getTargetLabel(@NonNull IAndroidTarget target) {
        if (target.isPlatform()) {
            AndroidVersion version = target.getVersion();
            String codename = target.getProperty(PkgProps.PLATFORM_CODENAME);
            String release = target.getProperty("ro.build.version.release"); //$NON-NLS-1$
            if (codename != null) {
                return String.format("API %1$d: Android %2$s (%3$s)", version.getApiLevel(), release, codename);
            }
            return String.format("API %1$d: Android %2$s", version.getApiLevel(), release);
        }

        return String.format("%1$s (API %2$s)", target.getFullName(), target.getVersion().getApiString());
    }

    /**
     * Sets the given tools: attribute in the given XML editor document, adding
     * the tools name space declaration if necessary, and optionally
     * comma-appending to an existing value.
     *
     * @param file the file associated with the element
     * @param element the associated element
     * @param description the description of the attribute (shown in the undo
     *            event)
     * @param name the name of the attribute
     * @param value the attribute value
     * @param appendValue if true, add this value as a comma separated value to
     *            the existing attribute value, if any
     */
    public static void setToolsAttribute(@NonNull final IFile file, @NonNull final Element element,
            @NonNull final String description, @NonNull final String name, @Nullable final String value,
            final boolean appendValue) {
        IModelManager modelManager = StructuredModelManager.getModelManager();
        if (modelManager == null) {
            return;
        }

        try {
            IStructuredModel model = null;
            if (model == null) {
                model = modelManager.getModelForEdit(file);
            }
            if (model != null) {
                try {
                    model.aboutToChangeModel();
                    if (model instanceof IDOMModel) {
                        IDOMModel domModel = (IDOMModel) model;
                        Document doc = domModel.getDocument();
                        if (doc != null && element.getOwnerDocument() == doc) {
                            String prefix = XmlUtils.lookupNamespacePrefix(element, TOOLS_URI, null, true);
                            if (prefix == null) {
                                // Add in new prefix...
                                prefix = XmlUtils.lookupNamespacePrefix(element, TOOLS_URI, TOOLS_PREFIX, true);
                            }

                            String v = value;
                            if (appendValue && v != null) {
                                String prev = element.getAttributeNS(TOOLS_URI, name);
                                if (prev.length() > 0) {
                                    v = prev + ',' + value;
                                }
                            }

                            // Use the non-namespace form of set attribute since we can't
                            // reference the namespace until the model has been reloaded
                            if (v != null) {
                                element.setAttribute(prefix + ':' + name, v);
                            } else {
                                element.removeAttribute(prefix + ':' + name);
                            }
                        }
                    }
                } finally {
                    model.changedModel();
                    String updated = model.getStructuredDocument().get();
                    model.releaseFromEdit();
                    model.save(file);

                    // Must also force a save on disk since the above model.save(file) often
                    // (always?) has no effect.
                    ITextFileBufferManager manager = FileBuffers.getTextFileBufferManager();
                    NullProgressMonitor monitor = new NullProgressMonitor();
                    IPath path = file.getFullPath();
                    manager.connect(path, LocationKind.IFILE, monitor);
                    try {
                        ITextFileBuffer buffer = manager.getTextFileBuffer(path, LocationKind.IFILE);
                        IDocument currentDocument = buffer.getDocument();
                        currentDocument.set(updated);
                        buffer.commit(monitor, true);
                    } finally {
                        manager.disconnect(path, LocationKind.IFILE, monitor);
                    }
                }
            }
        } catch (Exception e) {
            AdtPlugin.log(e, null);
        }
    }

    /**
     * Returns the Android version and code name of the given API level
     *
     * @param api the api level
     * @return a suitable version display name
     */
    public static String getAndroidName(int api) {
        if (api <= SdkVersionInfo.HIGHEST_KNOWN_API) {
            return SdkVersionInfo.getAndroidName(api);
        }

        // Consult SDK manager to see if we know any more (later) names,
        // installed by user
        Sdk sdk = Sdk.getCurrent();
        if (sdk != null) {
            for (IAndroidTarget target : sdk.getTargets()) {
                if (target.isPlatform()) {
                    AndroidVersion version = target.getVersion();
                    if (version.getApiLevel() == api) {
                        return getTargetLabel(target);
                    }
                }
            }
        }

        return "API " + api;
    }

    /**
     * Returns the highest known API level to this version of ADT. The
     * {@link #getAndroidName(int)} method will return real names up to and
     * including this number.
     *
     * @return the highest known API number
     */
    public static int getHighestKnownApiLevel() {
        return SdkVersionInfo.HIGHEST_KNOWN_API;
    }

    /**
     * Returns a list of known API names
     *
     * @return a list of string API names, starting from 1 and up through the
     *         maximum known versions (with no gaps)
     */
    public static String[] getKnownVersions() {
        int max = getHighestKnownApiLevel();
        Sdk sdk = Sdk.getCurrent();
        if (sdk != null) {
            for (IAndroidTarget target : sdk.getTargets()) {
                if (target.isPlatform()) {
                    AndroidVersion version = target.getVersion();
                    if (!version.isPreview()) {
                        max = Math.max(max, version.getApiLevel());
                    }
                }
            }
        }

        String[] versions = new String[max];
        for (int api = 1; api <= max; api++) {
            versions[api - 1] = getAndroidName(api);
        }

        return versions;
    }

    /**
     * Returns the Android project(s) that are selected or active, if any. This
     * considers the selection, the active editor, etc.
     *
     * @param selection the current selection
     * @return a list of projects, possibly empty (but never null)
     */
    @NonNull
    public static List<IProject> getSelectedProjects(@Nullable ISelection selection) {
        List<IProject> projects = new ArrayList<IProject>();

        if (selection instanceof IStructuredSelection) {
            IStructuredSelection structuredSelection = (IStructuredSelection) selection;
            // get the unique selected item.
            Iterator<?> iterator = structuredSelection.iterator();
            while (iterator.hasNext()) {
                Object element = iterator.next();

                // First look up the resource (since some adaptables
                // provide an IResource but not an IProject, and we can
                // always go from IResource to IProject)
                IResource resource = null;
                if (element instanceof IResource) { // may include IProject
                    resource = (IResource) element;
                } else if (element instanceof IAdaptable) {
                    IAdaptable adaptable = (IAdaptable) element;
                    Object adapter = adaptable.getAdapter(IResource.class);
                    resource = (IResource) adapter;
                }

                // get the project object from it.
                IProject project = null;
                if (resource != null) {
                    project = resource.getProject();
                } else if (element instanceof IAdaptable) {
                    project = (IProject) ((IAdaptable) element).getAdapter(IProject.class);
                }

                if (project != null && !projects.contains(project)) {
                    projects.add(project);
                }
            }
        }

        if (projects.isEmpty()) {
            // Try to look at the active editor instead
            IFile file = AdtUtils.getActiveFile();
            if (file != null) {
                projects.add(file.getProject());
            }
        }

        if (projects.isEmpty()) {
            // If we didn't find a default project based on the selection, check how many
            // open Android projects we can find in the current workspace. If there's only
            // one, we'll just select it by default.
            IJavaProject[] open = AdtUtils.getOpenAndroidProjects();
            for (IJavaProject project : open) {
                projects.add(project.getProject());
            }
            return projects;
        } else {
            // Make sure all the projects are Android projects
            List<IProject> androidProjects = new ArrayList<IProject>(projects.size());
            for (IProject project : projects) {
                if (BaseProjectHelper.isAndroidProject(project)) {
                    androidProjects.add(project);
                }
            }
            return androidProjects;
        }
    }

    private static Boolean sEclipse4;

    /**
     * Returns true if the running Eclipse is version 4.x or later
     *
     * @return true if the current Eclipse version is 4.x or later, false
     *         otherwise
     */
    public static boolean isEclipse4() {
        if (sEclipse4 == null) {
            sEclipse4 = Platform.getBundle("org.eclipse.e4.ui.model.workbench") != null; //$NON-NLS-1$
        }

        return sEclipse4;
    }

    /**
     * Reads the contents of an {@link IFile} and return it as a byte array
     *
     * @param file the file to be read
     * @return the String read from the file, or null if there was an error
     */
    @SuppressWarnings("resource") // Eclipse doesn't understand Closeables.closeQuietly yet
    @Nullable
    public static byte[] readData(@NonNull IFile file) {
        InputStream contents = null;
        try {
            contents = file.getContents();
            return ByteStreams.toByteArray(contents);
        } catch (Exception e) {
            // Pass -- just return null
        } finally {
            Closeables.closeQuietly(contents);
        }

        return null;
    }

    /**
     * Ensure that a given folder (and all its parents) are created. This implements
     * the equivalent of {@link File#mkdirs()} for {@link IContainer} folders.
     *
     * @param container the container to ensure exists
     * @throws CoreException if an error occurs
     */
    public static void ensureExists(@Nullable IContainer container) throws CoreException {
        if (container == null || container.exists()) {
            return;
        }
        IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
        IFolder folder = root.getFolder(container.getFullPath());
        ensureExists(folder);
    }

    private static void ensureExists(IFolder folder) throws CoreException {
        if (folder != null && !folder.exists()) {
            IContainer parent = folder.getParent();
            if (parent instanceof IFolder) {
                ensureExists((IFolder) parent);
            }
            folder.create(false, false, null);
        }
    }

    /**
     * Format the given floating value into an XML string, omitting decimals if
     * 0
     *
     * @param value the value to be formatted
     * @return the corresponding XML string for the value
     */
    public static String formatFloatAttribute(float value) {
        if (value != (int) value) {
            // Run String.format without a locale, because we don't want locale-specific
            // conversions here like separating the decimal part with a comma instead of a dot!
            return String.format((Locale) null, "%.2f", value); //$NON-NLS-1$
        } else {
            return Integer.toString((int) value);
        }
    }

    /**
     * Creates all the directories required for the given path.
     *
     * @param wsPath the path to create all the parent directories for
     * @return true if all the parent directories were created
     */
    public static boolean createWsParentDirectory(IContainer wsPath) {
        if (wsPath.getType() == IResource.FOLDER) {
            if (wsPath.exists()) {
                return true;
            }

            IFolder folder = (IFolder) wsPath;
            try {
                if (createWsParentDirectory(wsPath.getParent())) {
                    folder.create(true /* force */, true /* local */, null /* monitor */);
                    return true;
                }
            } catch (CoreException e) {
                e.printStackTrace();
            }
        }

        return false;
    }

    /**
     * Lists the files of the given directory and returns them as an array which
     * is never null. This simplifies processing file listings from for each
     * loops since {@link File#listFiles} can return null. This method simply
     * wraps it and makes sure it returns an empty array instead if necessary.
     *
     * @param dir the directory to list
     * @return the children, or empty if it has no children, is not a directory,
     *         etc.
     */
    @NonNull
    public static File[] listFiles(File dir) {
        File[] files = dir.listFiles();
        if (files != null) {
            return files;
        } else {
            return new File[0];
        }
    }

    /**
     * Closes all open editors that are showing a file for the given project. This method
     * should be called when a project is closed or deleted.
     * <p>
     * This method can be called from any thread, but if it is not called on the GUI thread
     * the editor will be closed asynchronously.
     *
     * @param project the project to close all editors for
     * @param save whether unsaved editors should be saved first
     */
    public static void closeEditors(@NonNull final IProject project, final boolean save) {
        final Display display = AdtPlugin.getDisplay();
        if (display == null || display.isDisposed()) {
            return;
        }
        if (display.getThread() != Thread.currentThread()) {
            display.asyncExec(new Runnable() {
                @Override
                public void run() {
                    closeEditors(project, save);
                }
            });
            return;
        }

        // Close editors for removed files
        IWorkbench workbench = PlatformUI.getWorkbench();
        for (IWorkbenchWindow window : workbench.getWorkbenchWindows()) {
            for (IWorkbenchPage page : window.getPages()) {
                List<IEditorReference> matching = null;
                for (IEditorReference ref : page.getEditorReferences()) {
                    boolean close = false;
                    try {
                        IEditorInput input = ref.getEditorInput();
                        if (input instanceof IFileEditorInput) {
                            IFileEditorInput fileInput = (IFileEditorInput) input;
                            if (project.equals(fileInput.getFile().getProject())) {
                                close = true;
                            }
                        }
                    } catch (PartInitException ex) {
                        close = true;
                    }
                    if (close) {
                        if (matching == null) {
                            matching = new ArrayList<IEditorReference>(2);
                        }
                        matching.add(ref);
                    }
                }
                if (matching != null) {
                    IEditorReference[] refs = new IEditorReference[matching.size()];
                    page.closeEditors(matching.toArray(refs), save);
                }
            }
        }
    }

    /**
     * Closes all open editors for the given file. Note that a file can be open in
     * more than one editor, for example by using Open With on the file to choose different
     * editors.
     * <p>
     * This method can be called from any thread, but if it is not called on the GUI thread
     * the editor will be closed asynchronously.
     *
     * @param file the file whose editors should be closed.
     * @param save whether unsaved editors should be saved first
     */
    public static void closeEditors(@NonNull final IFile file, final boolean save) {
        final Display display = AdtPlugin.getDisplay();
        if (display == null || display.isDisposed()) {
            return;
        }
        if (display.getThread() != Thread.currentThread()) {
            display.asyncExec(new Runnable() {
                @Override
                public void run() {
                    closeEditors(file, save);
                }
            });
            return;
        }

        // Close editors for removed files
        IWorkbench workbench = PlatformUI.getWorkbench();
        for (IWorkbenchWindow window : workbench.getWorkbenchWindows()) {
            for (IWorkbenchPage page : window.getPages()) {
                List<IEditorReference> matching = null;
                for (IEditorReference ref : page.getEditorReferences()) {
                    boolean close = false;
                    try {
                        IEditorInput input = ref.getEditorInput();
                        if (input instanceof IFileEditorInput) {
                            IFileEditorInput fileInput = (IFileEditorInput) input;
                            if (file.equals(fileInput.getFile())) {
                                close = true;
                            }
                        }
                    } catch (PartInitException ex) {
                        close = true;
                    }
                    if (close) {
                        // Found
                        if (matching == null) {
                            matching = new ArrayList<IEditorReference>(2);
                        }
                        matching.add(ref);
                        // We don't break here in case the file is
                        // opened multiple times with different editors.
                    }
                }
                if (matching != null) {
                    IEditorReference[] refs = new IEditorReference[matching.size()];
                    page.closeEditors(matching.toArray(refs), save);
                }
            }
        }
    }

    /**
     * Returns the offset region of the given 0-based line number in the given
     * file
     *
     * @param file the file to look up the line number in
     * @param line the line number (0-based, meaning that the first line is line
     *            0)
     * @return the corresponding offset range, or null
     */
    @Nullable
    public static IRegion getRegionOfLine(@NonNull IFile file, int line) {
        IDocumentProvider provider = new TextFileDocumentProvider();
        try {
            provider.connect(file);
            IDocument document = provider.getDocument(file);
            if (document != null) {
                return document.getLineInformation(line);
            }
        } catch (Exception e) {
            AdtPlugin.log(e, "Can't find range information for %1$s", file.getName());
        } finally {
            provider.disconnect(file);
        }

        return null;
    }

    /**
     * Returns all resource variations for the given file
     *
     * @param file resource file, which should be an XML file in one of the
     *            various resource folders, e.g. res/layout, res/values-xlarge, etc.
     * @param includeSelf if true, include the file itself in the list,
     *            otherwise exclude it
     * @return a list of all the resource variations
     */
    public static List<IFile> getResourceVariations(@Nullable IFile file, boolean includeSelf) {
        if (file == null) {
            return Collections.emptyList();
        }

        // Compute the set of layout files defining this layout resource
        List<IFile> variations = new ArrayList<IFile>();
        String name = file.getName();
        IContainer parent = file.getParent();
        if (parent != null) {
            IContainer resFolder = parent.getParent();
            if (resFolder != null) {
                String parentName = parent.getName();
                String prefix = parentName;
                int qualifiers = prefix.indexOf('-');

                if (qualifiers != -1) {
                    parentName = prefix.substring(0, qualifiers);
                    prefix = prefix.substring(0, qualifiers + 1);
                } else {
                    prefix = prefix + '-';
                }
                try {
                    for (IResource resource : resFolder.members()) {
                        String n = resource.getName();
                        if ((n.startsWith(prefix) || n.equals(parentName)) && resource instanceof IContainer) {
                            IContainer layoutFolder = (IContainer) resource;
                            IResource r = layoutFolder.findMember(name);
                            if (r instanceof IFile) {
                                IFile variation = (IFile) r;
                                if (!includeSelf && file.equals(variation)) {
                                    continue;
                                }
                                variations.add(variation);
                            }
                        }
                    }
                } catch (CoreException e) {
                    AdtPlugin.log(e, null);
                }
            }
        }

        return variations;
    }

    /**
     * Returns whether the current thread is the UI thread
     *
     * @return true if the current thread is the UI thread
     */
    public static boolean isUiThread() {
        return AdtPlugin.getDisplay() != null && AdtPlugin.getDisplay().getThread() == Thread.currentThread();
    }

    /**
     * Replaces any {@code \\uNNNN} references in the given string with the corresponding
     * unicode characters.
     *
     * @param s the string to perform replacements in
     * @return the string with unicode escapes replaced with actual characters
     */
    @NonNull
    public static String replaceUnicodeEscapes(@NonNull String s) {
        // Handle unicode escapes
        if (s.indexOf("\\u") != -1) { //$NON-NLS-1$
            StringBuilder sb = new StringBuilder(s.length());
            for (int i = 0, n = s.length(); i < n; i++) {
                char c = s.charAt(i);
                if (c == '\\' && i < n - 1) {
                    char next = s.charAt(i + 1);
                    if (next == 'u' && i < n - 5) { // case sensitive
                        String hex = s.substring(i + 2, i + 6);
                        try {
                            int unicodeValue = Integer.parseInt(hex, 16);
                            sb.append((char) unicodeValue);
                            i += 5;
                            continue;
                        } catch (NumberFormatException nufe) {
                            // Invalid escape: Just proceed to literally transcribe it
                            sb.append(c);
                        }
                    } else {
                        sb.append(c);
                        sb.append(next);
                        i++;
                        continue;
                    }
                } else {
                    sb.append(c);
                }
            }
            s = sb.toString();
        }

        return s;
    }

    /**
     * Looks up the {@link ResourceFolderType} corresponding to a given
     * {@link ResourceType}: the folder where those resources can be found.
     * <p>
     * Note that {@link ResourceType#ID} is a special case: it can not just
     * be defined in {@link ResourceFolderType#VALUES}, but it can also be
     * defined inline via {@code @+id} in {@link ResourceFolderType#LAYOUT} and
     * {@link ResourceFolderType#MENU} folders.
     *
     * @param type the resource type
     * @return the corresponding resource folder type
     */
    @NonNull
    public static ResourceFolderType getFolderTypeFor(@NonNull ResourceType type) {
        switch (type) {
        case ANIM:
            return ResourceFolderType.ANIM;
        case ANIMATOR:
            return ResourceFolderType.ANIMATOR;
        case ARRAY:
            return ResourceFolderType.VALUES;
        case COLOR:
            return ResourceFolderType.COLOR;
        case DRAWABLE:
            return ResourceFolderType.DRAWABLE;
        case INTERPOLATOR:
            return ResourceFolderType.INTERPOLATOR;
        case LAYOUT:
            return ResourceFolderType.LAYOUT;
        case MENU:
            return ResourceFolderType.MENU;
        case MIPMAP:
            return ResourceFolderType.MIPMAP;
        case RAW:
            return ResourceFolderType.RAW;
        case XML:
            return ResourceFolderType.XML;
        case ATTR:
        case BOOL:
        case DECLARE_STYLEABLE:
        case DIMEN:
        case FRACTION:
        case ID:
        case INTEGER:
        case PLURALS:
        case PUBLIC:
        case STRING:
        case STYLE:
        case STYLEABLE:
            return ResourceFolderType.VALUES;
        default:
            assert false : type;
            return ResourceFolderType.VALUES;

        }
    }

    /**
     * Looks up the {@link ResourceType} defined in a given {@link ResourceFolderType}.
     * <p>
     * Note that for {@link ResourceFolderType#VALUES} there are many, many
     * different types of resources that can be defined, so this method returns
     * {@code null} for that scenario.
     * <p>
     * Note also that {@link ResourceType#ID} is a special case: it can not just
     * be defined in {@link ResourceFolderType#VALUES}, but it can also be
     * defined inline via {@code @+id} in {@link ResourceFolderType#LAYOUT} and
     * {@link ResourceFolderType#MENU} folders.
     *
     * @param folderType the resource folder type
     * @return the corresponding resource type, or null if {@code folderType} is
     *         {@link ResourceFolderType#VALUES}
     */
    @Nullable
    public static ResourceType getResourceTypeFor(@NonNull ResourceFolderType folderType) {
        switch (folderType) {
        case ANIM:
            return ResourceType.ANIM;
        case ANIMATOR:
            return ResourceType.ANIMATOR;
        case COLOR:
            return ResourceType.COLOR;
        case DRAWABLE:
            return ResourceType.DRAWABLE;
        case INTERPOLATOR:
            return ResourceType.INTERPOLATOR;
        case LAYOUT:
            return ResourceType.LAYOUT;
        case MENU:
            return ResourceType.MENU;
        case MIPMAP:
            return ResourceType.MIPMAP;
        case RAW:
            return ResourceType.RAW;
        case XML:
            return ResourceType.XML;
        case VALUES:
            return null;
        default:
            assert false : folderType;
            return null;
        }
    }
}