/* * 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.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.Nullable; 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); if (prefix == null) { // Add in new prefix... prefix = XmlUtils.lookupNamespacePrefix(element, TOOLS_URI, TOOLS_PREFIX); 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 the applicable build code (for * {@code android.os.Build.VERSION_CODES}) for the corresponding API level, * or null if it's unknown. * * @param api the API level to look up a version code for * @return the corresponding build code field name, or null */ @Nullable public static String getBuildCodes(int api) { // See http://developer.android.com/reference/android/os/Build.VERSION_CODES.html switch (api) { case 1: return "BASE"; //$NON-NLS-1$ case 2: return "BASE_1_1"; //$NON-NLS-1$ case 3: return "CUPCAKE"; //$NON-NLS-1$ case 4: return "DONUT"; //$NON-NLS-1$ case 5: return "ECLAIR"; //$NON-NLS-1$ case 6: return "ECLAIR_0_1"; //$NON-NLS-1$ case 7: return "ECLAIR_MR1"; //$NON-NLS-1$ case 8: return "FROYO"; //$NON-NLS-1$ case 9: return "GINGERBREAD"; //$NON-NLS-1$ case 10: return "GINGERBREAD_MR1"; //$NON-NLS-1$ case 11: return "HONEYCOMB"; //$NON-NLS-1$ case 12: return "HONEYCOMB_MR1"; //$NON-NLS-1$ case 13: return "HONEYCOMB_MR2"; //$NON-NLS-1$ case 14: return "ICE_CREAM_SANDWICH"; //$NON-NLS-1$ case 15: return "ICE_CREAM_SANDWICH_MR1"; //$NON-NLS-1$ case 16: return "JELLY_BEAN"; //$NON-NLS-1$ case 17: return "JELLY_BEAN_MR1"; //$NON-NLS-1$ // If you add more versions here, also update #getAndroidName and // SdkConstants#HIGHEST_KNOWN_API } return null; } /** * 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); if (prefix == null) { // Add in new prefix... prefix = XmlUtils.lookupNamespacePrefix(element, TOOLS_URI, TOOLS_PREFIX); } 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) { // See http://source.android.com/source/build-numbers.html switch (api) { case 1: return "API 1: Android 1.0"; case 2: return "API 2: Android 1.1"; case 3: return "API 3: Android 1.5 (Cupcake)"; case 4: return "API 4: Android 1.6 (Donut)"; case 5: return "API 5: Android 2.0 (Eclair)"; case 6: return "API 6: Android 2.0.1 (Eclair)"; case 7: return "API 7: Android 2.1 (Eclair)"; case 8: return "API 8: Android 2.2 (Froyo)"; case 9: return "API 9: Android 2.3 (Gingerbread)"; case 10: return "API 10: Android 2.3.3 (Gingerbread)"; case 11: return "API 11: Android 3.0 (Honeycomb)"; case 12: return "API 12: Android 3.1 (Honeycomb)"; case 13: return "API 13: Android 3.2 (Honeycomb)"; case 14: return "API 14: Android 4.0 (IceCreamSandwich)"; case 15: return "API 15: Android 4.0.3 (IceCreamSandwich)"; case 16: return "API 16: Android 4.1 (Jelly Bean)"; case 17: return "API 17: Android 4.2 (Jelly Bean)"; // If you add more versions here, also update #getBuildCodes and // SdkConstants#HIGHEST_KNOWN_API default: { // 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 SdkConstants.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; } } }