1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.eclipse.org/org/documents/epl-v10.php
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.ide.eclipse.adt;
18 
19 import static com.android.SdkConstants.TOOLS_PREFIX;
20 import static com.android.SdkConstants.TOOLS_URI;
21 import static org.eclipse.ui.IWorkbenchPage.MATCH_INPUT;
22 
23 import com.android.annotations.NonNull;
24 import com.android.annotations.Nullable;
25 import com.android.sdklib.SdkVersionInfo;
26 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
27 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
28 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
29 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
30 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper.IProjectFilter;
31 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
32 import com.android.resources.ResourceFolderType;
33 import com.android.resources.ResourceType;
34 import com.android.sdklib.AndroidVersion;
35 import com.android.sdklib.IAndroidTarget;
36 import com.android.sdklib.repository.PkgProps;
37 import com.android.utils.XmlUtils;
38 import com.google.common.io.ByteStreams;
39 import com.google.common.io.Closeables;
40 
41 import org.eclipse.core.filebuffers.FileBuffers;
42 import org.eclipse.core.filebuffers.ITextFileBuffer;
43 import org.eclipse.core.filebuffers.ITextFileBufferManager;
44 import org.eclipse.core.filebuffers.LocationKind;
45 import org.eclipse.core.filesystem.URIUtil;
46 import org.eclipse.core.resources.IContainer;
47 import org.eclipse.core.resources.IFile;
48 import org.eclipse.core.resources.IFolder;
49 import org.eclipse.core.resources.IMarker;
50 import org.eclipse.core.resources.IProject;
51 import org.eclipse.core.resources.IResource;
52 import org.eclipse.core.resources.IWorkspace;
53 import org.eclipse.core.resources.IWorkspaceRoot;
54 import org.eclipse.core.resources.ResourcesPlugin;
55 import org.eclipse.core.runtime.CoreException;
56 import org.eclipse.core.runtime.IAdaptable;
57 import org.eclipse.core.runtime.IPath;
58 import org.eclipse.core.runtime.NullProgressMonitor;
59 import org.eclipse.core.runtime.Path;
60 import org.eclipse.core.runtime.Platform;
61 import org.eclipse.jdt.core.IJavaProject;
62 import org.eclipse.jface.text.BadLocationException;
63 import org.eclipse.jface.text.IDocument;
64 import org.eclipse.jface.text.IRegion;
65 import org.eclipse.jface.viewers.ISelection;
66 import org.eclipse.jface.viewers.IStructuredSelection;
67 import org.eclipse.swt.widgets.Display;
68 import org.eclipse.ui.IEditorInput;
69 import org.eclipse.ui.IEditorPart;
70 import org.eclipse.ui.IEditorReference;
71 import org.eclipse.ui.IFileEditorInput;
72 import org.eclipse.ui.IURIEditorInput;
73 import org.eclipse.ui.IWorkbench;
74 import org.eclipse.ui.IWorkbenchPage;
75 import org.eclipse.ui.IWorkbenchPart;
76 import org.eclipse.ui.IWorkbenchWindow;
77 import org.eclipse.ui.PartInitException;
78 import org.eclipse.ui.PlatformUI;
79 import org.eclipse.ui.editors.text.TextFileDocumentProvider;
80 import org.eclipse.ui.part.FileEditorInput;
81 import org.eclipse.ui.texteditor.IDocumentProvider;
82 import org.eclipse.ui.texteditor.ITextEditor;
83 import org.eclipse.wst.sse.core.StructuredModelManager;
84 import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
85 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
86 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
87 import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
88 import org.w3c.dom.Attr;
89 import org.w3c.dom.Document;
90 import org.w3c.dom.Element;
91 import org.w3c.dom.Node;
92 
93 import java.io.File;
94 import java.io.InputStream;
95 import java.net.URISyntaxException;
96 import java.net.URL;
97 import java.util.ArrayList;
98 import java.util.Collection;
99 import java.util.Collections;
100 import java.util.Iterator;
101 import java.util.List;
102 import java.util.Locale;
103 
104 
105 /** Utility methods for ADT */
106 @SuppressWarnings("restriction") // WST API
107 public class AdtUtils {
108     /**
109      * Creates a Java class name out of the given string, if possible. For
110      * example, "My Project" becomes "MyProject", "hello" becomes "Hello",
111      * "Java's" becomes "Java", and so on.
112      *
113      * @param string the string to be massaged into a Java class
114      * @return the string as a Java class, or null if a class name could not be
115      *         extracted
116      */
117     @Nullable
extractClassName(@onNull String string)118     public static String extractClassName(@NonNull String string) {
119         StringBuilder sb = new StringBuilder(string.length());
120         int n = string.length();
121 
122         int i = 0;
123         for (; i < n; i++) {
124             char c = Character.toUpperCase(string.charAt(i));
125             if (Character.isJavaIdentifierStart(c)) {
126                 sb.append(c);
127                 i++;
128                 break;
129             }
130         }
131         if (sb.length() > 0) {
132             for (; i < n; i++) {
133                 char c = string.charAt(i);
134                 if (Character.isJavaIdentifierPart(c)) {
135                     sb.append(c);
136                 }
137             }
138 
139             return sb.toString();
140         }
141 
142         return null;
143     }
144 
145     /**
146      * Strips off the last file extension from the given filename, e.g.
147      * "foo.backup.diff" will be turned into "foo.backup".
148      * <p>
149      * Note that dot files (e.g. ".profile") will be left alone.
150      *
151      * @param filename the filename to be stripped
152      * @return the filename without the last file extension.
153      */
stripLastExtension(String filename)154     public static String stripLastExtension(String filename) {
155         int dotIndex = filename.lastIndexOf('.');
156         if (dotIndex > 0) { // > 0 instead of != -1: Treat dot files (e.g. .profile) differently
157             return filename.substring(0, dotIndex);
158         } else {
159             return filename;
160         }
161     }
162 
163     /**
164      * Strips off all extensions from the given filename, e.g. "foo.9.png" will
165      * be turned into "foo".
166      * <p>
167      * Note that dot files (e.g. ".profile") will be left alone.
168      *
169      * @param filename the filename to be stripped
170      * @return the filename without any file extensions
171      */
stripAllExtensions(String filename)172     public static String stripAllExtensions(String filename) {
173         int dotIndex = filename.indexOf('.');
174         if (dotIndex > 0) { // > 0 instead of != -1: Treat dot files (e.g. .profile) differently
175             return filename.substring(0, dotIndex);
176         } else {
177             return filename;
178         }
179     }
180 
181     /**
182      * Strips the given suffix from the given string, provided that the string ends with
183      * the suffix.
184      *
185      * @param string the full string to strip from
186      * @param suffix the suffix to strip out
187      * @return the string without the suffix at the end
188      */
stripSuffix(@onNull String string, @NonNull String suffix)189     public static String stripSuffix(@NonNull String string, @NonNull String suffix) {
190         if (string.endsWith(suffix)) {
191             return string.substring(0, string.length() - suffix.length());
192         }
193 
194         return string;
195     }
196 
197     /**
198      * Capitalizes the string, i.e. transforms the initial [a-z] into [A-Z].
199      * Returns the string unmodified if the first character is not [a-z].
200      *
201      * @param str The string to capitalize.
202      * @return The capitalized string
203      */
capitalize(String str)204     public static String capitalize(String str) {
205         if (str == null || str.length() < 1 || Character.isUpperCase(str.charAt(0))) {
206             return str;
207         }
208 
209         StringBuilder sb = new StringBuilder();
210         sb.append(Character.toUpperCase(str.charAt(0)));
211         sb.append(str.substring(1));
212         return sb.toString();
213     }
214 
215     /**
216      * Converts a CamelCase word into an underlined_word
217      *
218      * @param string the CamelCase version of the word
219      * @return the underlined version of the word
220      */
camelCaseToUnderlines(String string)221     public static String camelCaseToUnderlines(String string) {
222         if (string.isEmpty()) {
223             return string;
224         }
225 
226         StringBuilder sb = new StringBuilder(2 * string.length());
227         int n = string.length();
228         boolean lastWasUpperCase = Character.isUpperCase(string.charAt(0));
229         for (int i = 0; i < n; i++) {
230             char c = string.charAt(i);
231             boolean isUpperCase = Character.isUpperCase(c);
232             if (isUpperCase && !lastWasUpperCase) {
233                 sb.append('_');
234             }
235             lastWasUpperCase = isUpperCase;
236             c = Character.toLowerCase(c);
237             sb.append(c);
238         }
239 
240         return sb.toString();
241     }
242 
243     /**
244      * Converts an underlined_word into a CamelCase word
245      *
246      * @param string the underlined word to convert
247      * @return the CamelCase version of the word
248      */
underlinesToCamelCase(String string)249     public static String underlinesToCamelCase(String string) {
250         StringBuilder sb = new StringBuilder(string.length());
251         int n = string.length();
252 
253         int i = 0;
254         boolean upcaseNext = true;
255         for (; i < n; i++) {
256             char c = string.charAt(i);
257             if (c == '_') {
258                 upcaseNext = true;
259             } else {
260                 if (upcaseNext) {
261                     c = Character.toUpperCase(c);
262                 }
263                 upcaseNext = false;
264                 sb.append(c);
265             }
266         }
267 
268         return sb.toString();
269     }
270 
271     /**
272      * Returns the current editor (the currently visible and active editor), or null if
273      * not found
274      *
275      * @return the current editor, or null
276      */
getActiveEditor()277     public static IEditorPart getActiveEditor() {
278         IWorkbenchWindow window = getActiveWorkbenchWindow();
279         if (window != null) {
280             IWorkbenchPage page = window.getActivePage();
281             if (page != null) {
282                 return page.getActiveEditor();
283             }
284         }
285 
286         return null;
287     }
288 
289     /**
290      * Returns the current active workbench, or null if not found
291      *
292      * @return the current window, or null
293      */
294     @Nullable
getActiveWorkbenchWindow()295     public static IWorkbenchWindow getActiveWorkbenchWindow() {
296         IWorkbench workbench = PlatformUI.getWorkbench();
297         IWorkbenchWindow window = workbench.getActiveWorkbenchWindow();
298         if (window == null) {
299             IWorkbenchWindow[] windows = workbench.getWorkbenchWindows();
300             if (windows.length > 0) {
301                 window = windows[0];
302             }
303         }
304 
305         return window;
306     }
307 
308     /**
309      * Returns the current active workbench page, or null if not found
310      *
311      * @return the current page, or null
312      */
313     @Nullable
getActiveWorkbenchPage()314     public static IWorkbenchPage getActiveWorkbenchPage() {
315         IWorkbenchWindow window = getActiveWorkbenchWindow();
316         if (window != null) {
317             IWorkbenchPage page = window.getActivePage();
318             if (page == null) {
319                 IWorkbenchPage[] pages = window.getPages();
320                 if (pages.length > 0) {
321                     page = pages[0];
322                 }
323             }
324 
325             return page;
326         }
327 
328         return null;
329     }
330 
331     /**
332      * Returns the current active workbench part, or null if not found
333      *
334      * @return the current active workbench part, or null
335      */
336     @Nullable
getActivePart()337     public static IWorkbenchPart getActivePart() {
338         IWorkbenchWindow window = getActiveWorkbenchWindow();
339         if (window != null) {
340             IWorkbenchPage activePage = window.getActivePage();
341             if (activePage != null) {
342                 return activePage.getActivePart();
343             }
344         }
345         return null;
346     }
347 
348     /**
349      * Returns the current text editor (the currently visible and active editor), or null
350      * if not found.
351      *
352      * @return the current text editor, or null
353      */
getActiveTextEditor()354     public static ITextEditor getActiveTextEditor() {
355         IEditorPart editor = getActiveEditor();
356         if (editor != null) {
357             if (editor instanceof ITextEditor) {
358                 return (ITextEditor) editor;
359             } else {
360                 return (ITextEditor) editor.getAdapter(ITextEditor.class);
361             }
362         }
363 
364         return null;
365     }
366 
367     /**
368      * Looks through the open editors and returns the editors that have the
369      * given file as input.
370      *
371      * @param file the file to search for
372      * @param restore whether editors should be restored (if they have an open
373      *            tab, but the editor hasn't been restored since the most recent
374      *            IDE start yet
375      * @return a collection of editors
376      */
377     @NonNull
findEditorsFor(@onNull IFile file, boolean restore)378     public static Collection<IEditorPart> findEditorsFor(@NonNull IFile file, boolean restore) {
379         FileEditorInput input = new FileEditorInput(file);
380         List<IEditorPart> result = null;
381         IWorkbench workbench = PlatformUI.getWorkbench();
382         IWorkbenchWindow[] windows = workbench.getWorkbenchWindows();
383         for (IWorkbenchWindow window : windows) {
384             IWorkbenchPage[] pages = window.getPages();
385             for (IWorkbenchPage page : pages) {
386                 IEditorReference[] editors = page.findEditors(input, null,  MATCH_INPUT);
387                 if (editors != null) {
388                     for (IEditorReference reference : editors) {
389                         IEditorPart editor = reference.getEditor(restore);
390                         if (editor != null) {
391                             if (result == null) {
392                                 result = new ArrayList<IEditorPart>();
393                             }
394                             result.add(editor);
395                         }
396                     }
397                 }
398             }
399         }
400 
401         if (result == null) {
402             return Collections.emptyList();
403         }
404 
405         return result;
406     }
407 
408     /**
409      * Attempts to convert the given {@link URL} into a {@link File}.
410      *
411      * @param url the {@link URL} to be converted
412      * @return the corresponding {@link File}, which may not exist
413      */
414     @NonNull
getFile(@onNull URL url)415     public static File getFile(@NonNull URL url) {
416         try {
417             // First try URL.toURI(): this will work for URLs that contain %20 for spaces etc.
418             // Unfortunately, it *doesn't* work for "broken" URLs where the URL contains
419             // spaces, which is often the case.
420             return new File(url.toURI());
421         } catch (URISyntaxException e) {
422             // ...so as a fallback, go to the old url.getPath() method, which handles space paths.
423             return new File(url.getPath());
424         }
425     }
426 
427     /**
428      * Returns the file for the current editor, if any.
429      *
430      * @return the file for the current editor, or null if none
431      */
getActiveFile()432     public static IFile getActiveFile() {
433         IEditorPart editor = getActiveEditor();
434         if (editor != null) {
435             IEditorInput input = editor.getEditorInput();
436             if (input instanceof IFileEditorInput) {
437                 IFileEditorInput fileInput = (IFileEditorInput) input;
438                 return fileInput.getFile();
439             }
440         }
441 
442         return null;
443     }
444 
445     /**
446      * Returns an absolute path to the given resource
447      *
448      * @param resource the resource to look up a path for
449      * @return an absolute file system path to the resource
450      */
451     @NonNull
getAbsolutePath(@onNull IResource resource)452     public static IPath getAbsolutePath(@NonNull IResource resource) {
453         IPath location = resource.getRawLocation();
454         if (location != null) {
455             return location.makeAbsolute();
456         } else {
457             IWorkspace workspace = ResourcesPlugin.getWorkspace();
458             IWorkspaceRoot root = workspace.getRoot();
459             IPath workspacePath = root.getLocation();
460             return workspacePath.append(resource.getFullPath());
461         }
462     }
463 
464     /**
465      * Converts a workspace-relative path to an absolute file path
466      *
467      * @param path the workspace-relative path to convert
468      * @return the corresponding absolute file in the file system
469      */
470     @NonNull
workspacePathToFile(@onNull IPath path)471     public static File workspacePathToFile(@NonNull IPath path) {
472         IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
473         IResource res = root.findMember(path);
474         if (res != null) {
475             IPath location = res.getLocation();
476             if (location != null) {
477                 return location.toFile();
478             }
479             return root.getLocation().append(path).toFile();
480         }
481 
482         return path.toFile();
483     }
484 
485     /**
486      * Converts a {@link File} to an {@link IFile}, if possible.
487      *
488      * @param file a file to be converted
489      * @return the corresponding {@link IFile}, or null
490      */
fileToIFile(File file)491     public static IFile fileToIFile(File file) {
492         if (!file.isAbsolute()) {
493             file = file.getAbsoluteFile();
494         }
495 
496         IWorkspaceRoot workspace = ResourcesPlugin.getWorkspace().getRoot();
497         IFile[] files = workspace.findFilesForLocationURI(file.toURI());
498         if (files.length > 0) {
499             return files[0];
500         }
501 
502         IPath filePath = new Path(file.getPath());
503         return pathToIFile(filePath);
504     }
505 
506     /**
507      * Converts a {@link File} to an {@link IResource}, if possible.
508      *
509      * @param file a file to be converted
510      * @return the corresponding {@link IResource}, or null
511      */
fileToResource(File file)512     public static IResource fileToResource(File file) {
513         if (!file.isAbsolute()) {
514             file = file.getAbsoluteFile();
515         }
516 
517         IWorkspaceRoot workspace = ResourcesPlugin.getWorkspace().getRoot();
518         IFile[] files = workspace.findFilesForLocationURI(file.toURI());
519         if (files.length > 0) {
520             return files[0];
521         }
522 
523         IPath filePath = new Path(file.getPath());
524         return pathToResource(filePath);
525     }
526 
527     /**
528      * Converts a {@link IPath} to an {@link IFile}, if possible.
529      *
530      * @param path a path to be converted
531      * @return the corresponding {@link IFile}, or null
532      */
pathToIFile(IPath path)533     public static IFile pathToIFile(IPath path) {
534         IWorkspaceRoot workspace = ResourcesPlugin.getWorkspace().getRoot();
535 
536         IFile[] files = workspace.findFilesForLocationURI(URIUtil.toURI(path.makeAbsolute()));
537         if (files.length > 0) {
538             return files[0];
539         }
540 
541         IPath workspacePath = workspace.getLocation();
542         if (workspacePath.isPrefixOf(path)) {
543             IPath relativePath = path.makeRelativeTo(workspacePath);
544             IResource member = workspace.findMember(relativePath);
545             if (member instanceof IFile) {
546                 return (IFile) member;
547             }
548         } else if (path.isAbsolute()) {
549             return workspace.getFileForLocation(path);
550         }
551 
552         return null;
553     }
554 
555     /**
556      * Converts a {@link IPath} to an {@link IResource}, if possible.
557      *
558      * @param path a path to be converted
559      * @return the corresponding {@link IResource}, or null
560      */
pathToResource(IPath path)561     public static IResource pathToResource(IPath path) {
562         IWorkspaceRoot workspace = ResourcesPlugin.getWorkspace().getRoot();
563 
564         IFile[] files = workspace.findFilesForLocationURI(URIUtil.toURI(path.makeAbsolute()));
565         if (files.length > 0) {
566             return files[0];
567         }
568 
569         IPath workspacePath = workspace.getLocation();
570         if (workspacePath.isPrefixOf(path)) {
571             IPath relativePath = path.makeRelativeTo(workspacePath);
572             return workspace.findMember(relativePath);
573         } else if (path.isAbsolute()) {
574             return workspace.getFileForLocation(path);
575         }
576 
577         return null;
578     }
579 
580     /**
581      * Returns all markers in a file/document that fit on the same line as the given offset
582      *
583      * @param markerType the marker type
584      * @param file the file containing the markers
585      * @param document the document showing the markers
586      * @param offset the offset to be checked
587      * @return a list (possibly empty but never null) of matching markers
588      */
589     @NonNull
findMarkersOnLine( @onNull String markerType, @NonNull IResource file, @NonNull IDocument document, int offset)590     public static List<IMarker> findMarkersOnLine(
591             @NonNull String markerType,
592             @NonNull IResource file,
593             @NonNull IDocument document,
594             int offset) {
595         List<IMarker> matchingMarkers = new ArrayList<IMarker>(2);
596         try {
597             IMarker[] markers = file.findMarkers(markerType, true, IResource.DEPTH_ZERO);
598 
599             // Look for a match on the same line as the caret.
600             IRegion lineInfo = document.getLineInformationOfOffset(offset);
601             int lineStart = lineInfo.getOffset();
602             int lineEnd = lineStart + lineInfo.getLength();
603             int offsetLine = document.getLineOfOffset(offset);
604 
605 
606             for (IMarker marker : markers) {
607                 int start = marker.getAttribute(IMarker.CHAR_START, -1);
608                 int end = marker.getAttribute(IMarker.CHAR_END, -1);
609                 if (start >= lineStart && start <= lineEnd && end > start) {
610                     matchingMarkers.add(marker);
611                 } else if (start == -1 && end == -1) {
612                     // Some markers don't set character range, they only set the line
613                     int line = marker.getAttribute(IMarker.LINE_NUMBER, -1);
614                     if (line == offsetLine + 1) {
615                         matchingMarkers.add(marker);
616                     }
617                 }
618             }
619         } catch (CoreException e) {
620             AdtPlugin.log(e, null);
621         } catch (BadLocationException e) {
622             AdtPlugin.log(e, null);
623         }
624 
625         return matchingMarkers;
626     }
627 
628     /**
629      * Returns the available and open Android projects
630      *
631      * @return the available and open Android projects, never null
632      */
633     @NonNull
getOpenAndroidProjects()634     public static IJavaProject[] getOpenAndroidProjects() {
635         return BaseProjectHelper.getAndroidProjects(new IProjectFilter() {
636             @Override
637             public boolean accept(IProject project) {
638                 return project.isAccessible();
639             }
640         });
641     }
642 
643     /**
644      * Returns a unique project name, based on the given {@code base} file name
645      * possibly with a {@code conjunction} and a new number behind it to ensure
646      * that the project name is unique. For example,
647      * {@code getUniqueProjectName("project", "_")} will return
648      * {@code "project"} if that name does not already exist, and if it does, it
649      * will return {@code "project_2"}.
650      *
651      * @param base the base name to use, such as "foo"
652      * @param conjunction a string to insert between the base name and the
653      *            number.
654      * @return a unique project name based on the given base and conjunction
655      */
656     public static String getUniqueProjectName(String base, String conjunction) {
657         // We're using all workspace projects here rather than just open Android project
658         // via getOpenAndroidProjects because the name cannot conflict with non-Android
659         // or closed projects either
660         IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
661         IProject[] projects = workspaceRoot.getProjects();
662 
663         for (int i = 1; i < 1000; i++) {
664             String name = i == 1 ? base : base + conjunction + Integer.toString(i);
665             boolean found = false;
666             for (IProject project : projects) {
667                 // Need to make case insensitive comparison, since otherwise we can hit
668                 // org.eclipse.core.internal.resources.ResourceException:
669                 // A resource exists with a different case: '/test'.
670                 if (project.getName().equalsIgnoreCase(name)) {
671                     found = true;
672                     break;
673                 }
674             }
675             if (!found) {
676                 return name;
677             }
678         }
679 
680         return base;
681     }
682 
683     /**
684      * Returns the name of the parent folder for the given editor input
685      *
686      * @param editorInput the editor input to check
687      * @return the parent folder, which is never null but may be ""
688      */
689     @NonNull
690     public static String getParentFolderName(@Nullable IEditorInput editorInput) {
691         if (editorInput instanceof IFileEditorInput) {
692              IFile file = ((IFileEditorInput) editorInput).getFile();
693              return file.getParent().getName();
694         }
695 
696         if (editorInput instanceof IURIEditorInput) {
697             IURIEditorInput urlEditorInput = (IURIEditorInput) editorInput;
698             String path = urlEditorInput.getURI().toString();
699             int lastIndex = path.lastIndexOf('/');
700             if (lastIndex != -1) {
701                 int lastLastIndex = path.lastIndexOf('/', lastIndex - 1);
702                 if (lastLastIndex != -1) {
703                     return path.substring(lastLastIndex + 1, lastIndex);
704                 }
705             }
706         }
707 
708         return "";
709     }
710 
711     /**
712      * Returns the XML editor for the given editor part
713      *
714      * @param part the editor part to look up the editor for
715      * @return the editor or null if this part is not an XML editor
716      */
717     @Nullable
718     public static AndroidXmlEditor getXmlEditor(@NonNull IEditorPart part) {
719         if (part instanceof AndroidXmlEditor) {
720             return (AndroidXmlEditor) part;
721         } else if (part instanceof GraphicalEditorPart) {
722             ((GraphicalEditorPart) part).getEditorDelegate().getEditor();
723         }
724 
725         return null;
726     }
727 
728     /**
729      * Sets the given tools: attribute in the given XML editor document, adding
730      * the tools name space declaration if necessary, formatting the affected
731      * document region, and optionally comma-appending to an existing value and
732      * optionally opening and revealing the attribute.
733      *
734      * @param editor the associated editor
735      * @param element the associated element
736      * @param description the description of the attribute (shown in the undo
737      *            event)
738      * @param name the name of the attribute
739      * @param value the attribute value
740      * @param reveal if true, open the editor and select the given attribute
741      *            node
742      * @param appendValue if true, add this value as a comma separated value to
743      *            the existing attribute value, if any
744      */
745     public static void setToolsAttribute(
746             @NonNull final AndroidXmlEditor editor,
747             @NonNull final Element element,
748             @NonNull final String description,
749             @NonNull final String name,
750             @Nullable final String value,
751             final boolean reveal,
752             final boolean appendValue) {
753         editor.wrapUndoEditXmlModel(description, new Runnable() {
754             @Override
755             public void run() {
756                 String prefix = XmlUtils.lookupNamespacePrefix(element, TOOLS_URI, null, true);
757                 if (prefix == null) {
758                     // Add in new prefix...
759                     prefix = XmlUtils.lookupNamespacePrefix(element,
760                             TOOLS_URI, TOOLS_PREFIX, true /*create*/);
761                     if (value != null) {
762                         // ...and ensure that the header is formatted such that
763                         // the XML namespace declaration is placed in the right
764                         // position and wrapping is applied etc.
765                         editor.scheduleNodeReformat(editor.getUiRootNode(),
766                                 true /*attributesOnly*/);
767                     }
768                 }
769 
770                 String v = value;
771                 if (appendValue && v != null) {
772                     String prev = element.getAttributeNS(TOOLS_URI, name);
773                     if (prev.length() > 0) {
774                         v = prev + ',' + value;
775                     }
776                 }
777 
778                 // Use the non-namespace form of set attribute since we can't
779                 // reference the namespace until the model has been reloaded
780                 if (v != null) {
781                     element.setAttribute(prefix + ':' + name, v);
782                 } else {
783                     element.removeAttribute(prefix + ':' + name);
784                 }
785 
786                 UiElementNode rootUiNode = editor.getUiRootNode();
787                 if (rootUiNode != null && v != null) {
788                     final UiElementNode uiNode = rootUiNode.findXmlNode(element);
789                     if (uiNode != null) {
790                         editor.scheduleNodeReformat(uiNode, true /*attributesOnly*/);
791 
792                         if (reveal) {
793                             // Update editor selection after format
794                             Display display = AdtPlugin.getDisplay();
795                             if (display != null) {
796                                 display.asyncExec(new Runnable() {
797                                     @Override
798                                     public void run() {
799                                         Node xmlNode = uiNode.getXmlNode();
800                                         Attr attribute = ((Element) xmlNode).getAttributeNodeNS(
801                                                 TOOLS_URI, name);
802                                         if (attribute instanceof IndexedRegion) {
803                                             IndexedRegion region = (IndexedRegion) attribute;
804                                             editor.getStructuredTextEditor().selectAndReveal(
805                                                     region.getStartOffset(), region.getLength());
806                                         }
807                                     }
808                                 });
809                             }
810                         }
811                     }
812                 }
813             }
814         });
815     }
816 
817     /**
818      * Returns a string label for the given target, of the form
819      * "API 16: Android 4.1 (Jelly Bean)".
820      *
821      * @param target the target to generate a string from
822      * @return a suitable display string
823      */
824     @NonNull
825     public static String getTargetLabel(@NonNull IAndroidTarget target) {
826         if (target.isPlatform()) {
827             AndroidVersion version = target.getVersion();
828             String codename = target.getProperty(PkgProps.PLATFORM_CODENAME);
829             String release = target.getProperty("ro.build.version.release"); //$NON-NLS-1$
830             if (codename != null) {
831                 return String.format("API %1$d: Android %2$s (%3$s)",
832                         version.getApiLevel(),
833                         release,
834                         codename);
835             }
836             return String.format("API %1$d: Android %2$s", version.getApiLevel(),
837                     release);
838         }
839 
840         return String.format("%1$s (API %2$s)", target.getFullName(),
841                 target.getVersion().getApiString());
842     }
843 
844     /**
845      * Sets the given tools: attribute in the given XML editor document, adding
846      * the tools name space declaration if necessary, and optionally
847      * comma-appending to an existing value.
848      *
849      * @param file the file associated with the element
850      * @param element the associated element
851      * @param description the description of the attribute (shown in the undo
852      *            event)
853      * @param name the name of the attribute
854      * @param value the attribute value
855      * @param appendValue if true, add this value as a comma separated value to
856      *            the existing attribute value, if any
857      */
858     public static void setToolsAttribute(
859             @NonNull final IFile file,
860             @NonNull final Element element,
861             @NonNull final String description,
862             @NonNull final String name,
863             @Nullable final String value,
864             final boolean appendValue) {
865         IModelManager modelManager = StructuredModelManager.getModelManager();
866         if (modelManager == null) {
867             return;
868         }
869 
870         try {
871             IStructuredModel model = null;
872             if (model == null) {
873                 model = modelManager.getModelForEdit(file);
874             }
875             if (model != null) {
876                 try {
877                     model.aboutToChangeModel();
878                     if (model instanceof IDOMModel) {
879                         IDOMModel domModel = (IDOMModel) model;
880                         Document doc = domModel.getDocument();
881                         if (doc != null && element.getOwnerDocument() == doc) {
882                             String prefix = XmlUtils.lookupNamespacePrefix(element, TOOLS_URI,
883                                     null, true);
884                             if (prefix == null) {
885                                 // Add in new prefix...
886                                 prefix = XmlUtils.lookupNamespacePrefix(element,
887                                         TOOLS_URI, TOOLS_PREFIX, true);
888                             }
889 
890                             String v = value;
891                             if (appendValue && v != null) {
892                                 String prev = element.getAttributeNS(TOOLS_URI, name);
893                                 if (prev.length() > 0) {
894                                     v = prev + ',' + value;
895                                 }
896                             }
897 
898                             // Use the non-namespace form of set attribute since we can't
899                             // reference the namespace until the model has been reloaded
900                             if (v != null) {
901                                 element.setAttribute(prefix + ':' + name, v);
902                             } else {
903                                 element.removeAttribute(prefix + ':' + name);
904                             }
905                         }
906                     }
907                 } finally {
908                     model.changedModel();
909                     String updated = model.getStructuredDocument().get();
910                     model.releaseFromEdit();
911                     model.save(file);
912 
913                     // Must also force a save on disk since the above model.save(file) often
914                     // (always?) has no effect.
915                     ITextFileBufferManager manager = FileBuffers.getTextFileBufferManager();
916                     NullProgressMonitor monitor = new NullProgressMonitor();
917                     IPath path = file.getFullPath();
918                     manager.connect(path, LocationKind.IFILE, monitor);
919                     try {
920                         ITextFileBuffer buffer = manager.getTextFileBuffer(path,
921                                 LocationKind.IFILE);
922                         IDocument currentDocument = buffer.getDocument();
923                         currentDocument.set(updated);
924                         buffer.commit(monitor, true);
925                     } finally {
926                         manager.disconnect(path, LocationKind.IFILE,  monitor);
927                     }
928                 }
929             }
930         } catch (Exception e) {
931             AdtPlugin.log(e, null);
932         }
933     }
934 
935     /**
936      * Returns the Android version and code name of the given API level
937      *
938      * @param api the api level
939      * @return a suitable version display name
940      */
941     public static String getAndroidName(int api) {
942         if (api <= SdkVersionInfo.HIGHEST_KNOWN_API) {
943             return SdkVersionInfo.getAndroidName(api);
944         }
945 
946         // Consult SDK manager to see if we know any more (later) names,
947         // installed by user
948         Sdk sdk = Sdk.getCurrent();
949         if (sdk != null) {
950             for (IAndroidTarget target : sdk.getTargets()) {
951                 if (target.isPlatform()) {
952                     AndroidVersion version = target.getVersion();
953                     if (version.getApiLevel() == api) {
954                         return getTargetLabel(target);
955                     }
956                 }
957             }
958         }
959 
960         return "API " + api;
961     }
962 
963     /**
964      * Returns the highest known API level to this version of ADT. The
965      * {@link #getAndroidName(int)} method will return real names up to and
966      * including this number.
967      *
968      * @return the highest known API number
969      */
970     public static int getHighestKnownApiLevel() {
971         return SdkVersionInfo.HIGHEST_KNOWN_API;
972     }
973 
974     /**
975      * Returns a list of known API names
976      *
977      * @return a list of string API names, starting from 1 and up through the
978      *         maximum known versions (with no gaps)
979      */
980     public static String[] getKnownVersions() {
981         int max = getHighestKnownApiLevel();
982         Sdk sdk = Sdk.getCurrent();
983         if (sdk != null) {
984             for (IAndroidTarget target : sdk.getTargets()) {
985                 if (target.isPlatform()) {
986                     AndroidVersion version = target.getVersion();
987                     if (!version.isPreview()) {
988                         max = Math.max(max, version.getApiLevel());
989                     }
990                 }
991             }
992         }
993 
994         String[] versions = new String[max];
995         for (int api = 1; api <= max; api++) {
996             versions[api-1] = getAndroidName(api);
997         }
998 
999         return versions;
1000     }
1001 
1002     /**
1003      * Returns the Android project(s) that are selected or active, if any. This
1004      * considers the selection, the active editor, etc.
1005      *
1006      * @param selection the current selection
1007      * @return a list of projects, possibly empty (but never null)
1008      */
1009     @NonNull
1010     public static List<IProject> getSelectedProjects(@Nullable ISelection selection) {
1011         List<IProject> projects = new ArrayList<IProject>();
1012 
1013         if (selection instanceof IStructuredSelection) {
1014             IStructuredSelection structuredSelection = (IStructuredSelection) selection;
1015             // get the unique selected item.
1016             Iterator<?> iterator = structuredSelection.iterator();
1017             while (iterator.hasNext()) {
1018                 Object element = iterator.next();
1019 
1020                 // First look up the resource (since some adaptables
1021                 // provide an IResource but not an IProject, and we can
1022                 // always go from IResource to IProject)
1023                 IResource resource = null;
1024                 if (element instanceof IResource) { // may include IProject
1025                    resource = (IResource) element;
1026                 } else if (element instanceof IAdaptable) {
1027                     IAdaptable adaptable = (IAdaptable)element;
1028                     Object adapter = adaptable.getAdapter(IResource.class);
1029                     resource = (IResource) adapter;
1030                 }
1031 
1032                 // get the project object from it.
1033                 IProject project = null;
1034                 if (resource != null) {
1035                     project = resource.getProject();
1036                 } else if (element instanceof IAdaptable) {
1037                     project = (IProject) ((IAdaptable) element).getAdapter(IProject.class);
1038                 }
1039 
1040                 if (project != null && !projects.contains(project)) {
1041                     projects.add(project);
1042                 }
1043             }
1044         }
1045 
1046         if (projects.isEmpty()) {
1047             // Try to look at the active editor instead
1048             IFile file = AdtUtils.getActiveFile();
1049             if (file != null) {
1050                 projects.add(file.getProject());
1051             }
1052         }
1053 
1054         if (projects.isEmpty()) {
1055             // If we didn't find a default project based on the selection, check how many
1056             // open Android projects we can find in the current workspace. If there's only
1057             // one, we'll just select it by default.
1058             IJavaProject[] open = AdtUtils.getOpenAndroidProjects();
1059             for (IJavaProject project : open) {
1060                 projects.add(project.getProject());
1061             }
1062             return projects;
1063         } else {
1064             // Make sure all the projects are Android projects
1065             List<IProject> androidProjects = new ArrayList<IProject>(projects.size());
1066             for (IProject project : projects) {
1067                 if (BaseProjectHelper.isAndroidProject(project)) {
1068                     androidProjects.add(project);
1069                 }
1070             }
1071             return androidProjects;
1072         }
1073     }
1074 
1075     private static Boolean sEclipse4;
1076 
1077     /**
1078      * Returns true if the running Eclipse is version 4.x or later
1079      *
1080      * @return true if the current Eclipse version is 4.x or later, false
1081      *         otherwise
1082      */
1083     public static boolean isEclipse4() {
1084         if (sEclipse4 == null) {
1085             sEclipse4 = Platform.getBundle("org.eclipse.e4.ui.model.workbench") != null; //$NON-NLS-1$
1086         }
1087 
1088         return sEclipse4;
1089     }
1090 
1091     /**
1092      * Reads the contents of an {@link IFile} and return it as a byte array
1093      *
1094      * @param file the file to be read
1095      * @return the String read from the file, or null if there was an error
1096      */
1097     @SuppressWarnings("resource") // Eclipse doesn't understand Closeables.closeQuietly yet
1098     @Nullable
1099     public static byte[] readData(@NonNull IFile file) {
1100         InputStream contents = null;
1101         try {
1102             contents = file.getContents();
1103             return ByteStreams.toByteArray(contents);
1104         } catch (Exception e) {
1105             // Pass -- just return null
1106         } finally {
1107             Closeables.closeQuietly(contents);
1108         }
1109 
1110         return null;
1111     }
1112 
1113     /**
1114      * Ensure that a given folder (and all its parents) are created. This implements
1115      * the equivalent of {@link File#mkdirs()} for {@link IContainer} folders.
1116      *
1117      * @param container the container to ensure exists
1118      * @throws CoreException if an error occurs
1119      */
1120     public static void ensureExists(@Nullable IContainer container) throws CoreException {
1121         if (container == null || container.exists()) {
1122             return;
1123         }
1124         IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
1125         IFolder folder = root.getFolder(container.getFullPath());
1126         ensureExists(folder);
1127     }
1128 
1129     private static void ensureExists(IFolder folder) throws CoreException {
1130         if (folder != null && !folder.exists()) {
1131             IContainer parent = folder.getParent();
1132             if (parent instanceof IFolder) {
1133                 ensureExists((IFolder) parent);
1134             }
1135             folder.create(false, false, null);
1136         }
1137     }
1138 
1139     /**
1140      * Format the given floating value into an XML string, omitting decimals if
1141      * 0
1142      *
1143      * @param value the value to be formatted
1144      * @return the corresponding XML string for the value
1145      */
1146     public static String formatFloatAttribute(float value) {
1147         if (value != (int) value) {
1148             // Run String.format without a locale, because we don't want locale-specific
1149             // conversions here like separating the decimal part with a comma instead of a dot!
1150             return String.format((Locale) null, "%.2f", value); //$NON-NLS-1$
1151         } else {
1152             return Integer.toString((int) value);
1153         }
1154     }
1155 
1156     /**
1157      * Creates all the directories required for the given path.
1158      *
1159      * @param wsPath the path to create all the parent directories for
1160      * @return true if all the parent directories were created
1161      */
1162     public static boolean createWsParentDirectory(IContainer wsPath) {
1163         if (wsPath.getType() == IResource.FOLDER) {
1164             if (wsPath.exists()) {
1165                 return true;
1166             }
1167 
1168             IFolder folder = (IFolder) wsPath;
1169             try {
1170                 if (createWsParentDirectory(wsPath.getParent())) {
1171                     folder.create(true /* force */, true /* local */, null /* monitor */);
1172                     return true;
1173                 }
1174             } catch (CoreException e) {
1175                 e.printStackTrace();
1176             }
1177         }
1178 
1179         return false;
1180     }
1181 
1182     /**
1183      * Lists the files of the given directory and returns them as an array which
1184      * is never null. This simplifies processing file listings from for each
1185      * loops since {@link File#listFiles} can return null. This method simply
1186      * wraps it and makes sure it returns an empty array instead if necessary.
1187      *
1188      * @param dir the directory to list
1189      * @return the children, or empty if it has no children, is not a directory,
1190      *         etc.
1191      */
1192     @NonNull
1193     public static File[] listFiles(File dir) {
1194         File[] files = dir.listFiles();
1195         if (files != null) {
1196             return files;
1197         } else {
1198             return new File[0];
1199         }
1200     }
1201 
1202     /**
1203      * Closes all open editors that are showing a file for the given project. This method
1204      * should be called when a project is closed or deleted.
1205      * <p>
1206      * This method can be called from any thread, but if it is not called on the GUI thread
1207      * the editor will be closed asynchronously.
1208      *
1209      * @param project the project to close all editors for
1210      * @param save whether unsaved editors should be saved first
1211      */
1212     public static void closeEditors(@NonNull final IProject project, final boolean save) {
1213         final Display display = AdtPlugin.getDisplay();
1214         if (display == null || display.isDisposed()) {
1215             return;
1216         }
1217         if (display.getThread() != Thread.currentThread()) {
1218             display.asyncExec(new Runnable() {
1219                 @Override
1220                 public void run() {
1221                     closeEditors(project, save);
1222                 }
1223             });
1224             return;
1225         }
1226 
1227         // Close editors for removed files
1228         IWorkbench workbench = PlatformUI.getWorkbench();
1229         for (IWorkbenchWindow window : workbench.getWorkbenchWindows()) {
1230             for (IWorkbenchPage page : window.getPages()) {
1231                 List<IEditorReference> matching = null;
1232                 for (IEditorReference ref : page.getEditorReferences()) {
1233                     boolean close = false;
1234                     try {
1235                         IEditorInput input = ref.getEditorInput();
1236                         if (input instanceof IFileEditorInput) {
1237                             IFileEditorInput fileInput = (IFileEditorInput) input;
1238                             if (project.equals(fileInput.getFile().getProject())) {
1239                                 close = true;
1240                             }
1241                         }
1242                     } catch (PartInitException ex) {
1243                         close = true;
1244                     }
1245                     if (close) {
1246                         if (matching == null) {
1247                             matching = new ArrayList<IEditorReference>(2);
1248                         }
1249                         matching.add(ref);
1250                     }
1251                 }
1252                 if (matching != null) {
1253                     IEditorReference[] refs = new IEditorReference[matching.size()];
1254                     page.closeEditors(matching.toArray(refs), save);
1255                 }
1256             }
1257         }
1258     }
1259 
1260     /**
1261      * Closes all open editors for the given file. Note that a file can be open in
1262      * more than one editor, for example by using Open With on the file to choose different
1263      * editors.
1264      * <p>
1265      * This method can be called from any thread, but if it is not called on the GUI thread
1266      * the editor will be closed asynchronously.
1267      *
1268      * @param file the file whose editors should be closed.
1269      * @param save whether unsaved editors should be saved first
1270      */
1271     public static void closeEditors(@NonNull final IFile file, final boolean save) {
1272         final Display display = AdtPlugin.getDisplay();
1273         if (display == null || display.isDisposed()) {
1274             return;
1275         }
1276         if (display.getThread() != Thread.currentThread()) {
1277             display.asyncExec(new Runnable() {
1278                 @Override
1279                 public void run() {
1280                     closeEditors(file, save);
1281                 }
1282             });
1283             return;
1284         }
1285 
1286         // Close editors for removed files
1287         IWorkbench workbench = PlatformUI.getWorkbench();
1288         for (IWorkbenchWindow window : workbench.getWorkbenchWindows()) {
1289             for (IWorkbenchPage page : window.getPages()) {
1290                 List<IEditorReference> matching = null;
1291                 for (IEditorReference ref : page.getEditorReferences()) {
1292                     boolean close = false;
1293                     try {
1294                         IEditorInput input = ref.getEditorInput();
1295                         if (input instanceof IFileEditorInput) {
1296                             IFileEditorInput fileInput = (IFileEditorInput) input;
1297                             if (file.equals(fileInput.getFile())) {
1298                                 close = true;
1299                             }
1300                         }
1301                     } catch (PartInitException ex) {
1302                         close = true;
1303                     }
1304                     if (close) {
1305                         // Found
1306                         if (matching == null) {
1307                             matching = new ArrayList<IEditorReference>(2);
1308                         }
1309                         matching.add(ref);
1310                         // We don't break here in case the file is
1311                         // opened multiple times with different editors.
1312                     }
1313                 }
1314                 if (matching != null) {
1315                     IEditorReference[] refs = new IEditorReference[matching.size()];
1316                     page.closeEditors(matching.toArray(refs), save);
1317                 }
1318             }
1319         }
1320     }
1321 
1322     /**
1323      * Returns the offset region of the given 0-based line number in the given
1324      * file
1325      *
1326      * @param file the file to look up the line number in
1327      * @param line the line number (0-based, meaning that the first line is line
1328      *            0)
1329      * @return the corresponding offset range, or null
1330      */
1331     @Nullable
1332     public static IRegion getRegionOfLine(@NonNull IFile file, int line) {
1333         IDocumentProvider provider = new TextFileDocumentProvider();
1334         try {
1335             provider.connect(file);
1336             IDocument document = provider.getDocument(file);
1337             if (document != null) {
1338                 return document.getLineInformation(line);
1339             }
1340         } catch (Exception e) {
1341             AdtPlugin.log(e, "Can't find range information for %1$s", file.getName());
1342         } finally {
1343             provider.disconnect(file);
1344         }
1345 
1346         return null;
1347     }
1348 
1349     /**
1350      * Returns all resource variations for the given file
1351      *
1352      * @param file resource file, which should be an XML file in one of the
1353      *            various resource folders, e.g. res/layout, res/values-xlarge, etc.
1354      * @param includeSelf if true, include the file itself in the list,
1355      *            otherwise exclude it
1356      * @return a list of all the resource variations
1357      */
1358     public static List<IFile> getResourceVariations(@Nullable IFile file, boolean includeSelf) {
1359         if (file == null) {
1360             return Collections.emptyList();
1361         }
1362 
1363         // Compute the set of layout files defining this layout resource
1364         List<IFile> variations = new ArrayList<IFile>();
1365         String name = file.getName();
1366         IContainer parent = file.getParent();
1367         if (parent != null) {
1368             IContainer resFolder = parent.getParent();
1369             if (resFolder != null) {
1370                 String parentName = parent.getName();
1371                 String prefix = parentName;
1372                 int qualifiers = prefix.indexOf('-');
1373 
1374                 if (qualifiers != -1) {
1375                     parentName = prefix.substring(0, qualifiers);
1376                     prefix = prefix.substring(0, qualifiers + 1);
1377                 } else {
1378                     prefix = prefix + '-';
1379                 }
1380                 try {
1381                     for (IResource resource : resFolder.members()) {
1382                         String n = resource.getName();
1383                         if ((n.startsWith(prefix) || n.equals(parentName))
1384                                 && resource instanceof IContainer) {
1385                             IContainer layoutFolder = (IContainer) resource;
1386                             IResource r = layoutFolder.findMember(name);
1387                             if (r instanceof IFile) {
1388                                 IFile variation = (IFile) r;
1389                                 if (!includeSelf && file.equals(variation)) {
1390                                     continue;
1391                                 }
1392                                 variations.add(variation);
1393                             }
1394                         }
1395                     }
1396                 } catch (CoreException e) {
1397                     AdtPlugin.log(e, null);
1398                 }
1399             }
1400         }
1401 
1402         return variations;
1403     }
1404 
1405     /**
1406      * Returns whether the current thread is the UI thread
1407      *
1408      * @return true if the current thread is the UI thread
1409      */
1410     public static boolean isUiThread() {
1411         return AdtPlugin.getDisplay() != null
1412                 && AdtPlugin.getDisplay().getThread() == Thread.currentThread();
1413     }
1414 
1415     /**
1416      * Replaces any {@code \\uNNNN} references in the given string with the corresponding
1417      * unicode characters.
1418      *
1419      * @param s the string to perform replacements in
1420      * @return the string with unicode escapes replaced with actual characters
1421      */
1422     @NonNull
1423     public static String replaceUnicodeEscapes(@NonNull String s) {
1424         // Handle unicode escapes
1425         if (s.indexOf("\\u") != -1) { //$NON-NLS-1$
1426             StringBuilder sb = new StringBuilder(s.length());
1427             for (int i = 0, n = s.length(); i < n; i++) {
1428                 char c = s.charAt(i);
1429                 if (c == '\\' && i < n - 1) {
1430                     char next = s.charAt(i + 1);
1431                     if (next == 'u' && i < n - 5) { // case sensitive
1432                         String hex = s.substring(i + 2, i + 6);
1433                         try {
1434                             int unicodeValue = Integer.parseInt(hex, 16);
1435                             sb.append((char) unicodeValue);
1436                             i += 5;
1437                             continue;
1438                         } catch (NumberFormatException nufe) {
1439                             // Invalid escape: Just proceed to literally transcribe it
1440                             sb.append(c);
1441                         }
1442                     } else {
1443                         sb.append(c);
1444                         sb.append(next);
1445                         i++;
1446                         continue;
1447                     }
1448                 } else {
1449                     sb.append(c);
1450                 }
1451             }
1452             s = sb.toString();
1453         }
1454 
1455         return s;
1456     }
1457 
1458     /**
1459      * Looks up the {@link ResourceFolderType} corresponding to a given
1460      * {@link ResourceType}: the folder where those resources can be found.
1461      * <p>
1462      * Note that {@link ResourceType#ID} is a special case: it can not just
1463      * be defined in {@link ResourceFolderType#VALUES}, but it can also be
1464      * defined inline via {@code @+id} in {@link ResourceFolderType#LAYOUT} and
1465      * {@link ResourceFolderType#MENU} folders.
1466      *
1467      * @param type the resource type
1468      * @return the corresponding resource folder type
1469      */
1470     @NonNull
1471     public static ResourceFolderType getFolderTypeFor(@NonNull ResourceType type) {
1472         switch (type) {
1473             case ANIM:
1474                 return ResourceFolderType.ANIM;
1475             case ANIMATOR:
1476                 return ResourceFolderType.ANIMATOR;
1477             case ARRAY:
1478                 return ResourceFolderType.VALUES;
1479             case COLOR:
1480                 return ResourceFolderType.COLOR;
1481             case DRAWABLE:
1482                 return ResourceFolderType.DRAWABLE;
1483             case INTERPOLATOR:
1484                 return ResourceFolderType.INTERPOLATOR;
1485             case LAYOUT:
1486                 return ResourceFolderType.LAYOUT;
1487             case MENU:
1488                 return ResourceFolderType.MENU;
1489             case MIPMAP:
1490                 return ResourceFolderType.MIPMAP;
1491             case RAW:
1492                 return ResourceFolderType.RAW;
1493             case XML:
1494                 return ResourceFolderType.XML;
1495             case ATTR:
1496             case BOOL:
1497             case DECLARE_STYLEABLE:
1498             case DIMEN:
1499             case FRACTION:
1500             case ID:
1501             case INTEGER:
1502             case PLURALS:
1503             case PUBLIC:
1504             case STRING:
1505             case STYLE:
1506             case STYLEABLE:
1507                 return ResourceFolderType.VALUES;
1508             default:
1509                 assert false : type;
1510             return ResourceFolderType.VALUES;
1511 
1512         }
1513     }
1514 
1515     /**
1516      * Looks up the {@link ResourceType} defined in a given {@link ResourceFolderType}.
1517      * <p>
1518      * Note that for {@link ResourceFolderType#VALUES} there are many, many
1519      * different types of resources that can be defined, so this method returns
1520      * {@code null} for that scenario.
1521      * <p>
1522      * Note also that {@link ResourceType#ID} is a special case: it can not just
1523      * be defined in {@link ResourceFolderType#VALUES}, but it can also be
1524      * defined inline via {@code @+id} in {@link ResourceFolderType#LAYOUT} and
1525      * {@link ResourceFolderType#MENU} folders.
1526      *
1527      * @param folderType the resource folder type
1528      * @return the corresponding resource type, or null if {@code folderType} is
1529      *         {@link ResourceFolderType#VALUES}
1530      */
1531     @Nullable
1532     public static ResourceType getResourceTypeFor(@NonNull ResourceFolderType folderType) {
1533         switch (folderType) {
1534             case ANIM:
1535                 return ResourceType.ANIM;
1536             case ANIMATOR:
1537                 return ResourceType.ANIMATOR;
1538             case COLOR:
1539                 return ResourceType.COLOR;
1540             case DRAWABLE:
1541                 return ResourceType.DRAWABLE;
1542             case INTERPOLATOR:
1543                 return ResourceType.INTERPOLATOR;
1544             case LAYOUT:
1545                 return ResourceType.LAYOUT;
1546             case MENU:
1547                 return ResourceType.MENU;
1548             case MIPMAP:
1549                 return ResourceType.MIPMAP;
1550             case RAW:
1551                 return ResourceType.RAW;
1552             case XML:
1553                 return ResourceType.XML;
1554             case VALUES:
1555                 return null;
1556             default:
1557                 assert false : folderType;
1558                 return null;
1559         }
1560     }
1561 }
1562