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