1 /* 2 * Copyright (c) 2003, 2015, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package sun.awt.shell; 27 28 import java.awt.Image; 29 import java.awt.Toolkit; 30 import java.awt.image.AbstractMultiResolutionImage; 31 import java.awt.image.BufferedImage; 32 import java.awt.image.ImageObserver; 33 import java.io.File; 34 import java.io.FileNotFoundException; 35 import java.io.IOException; 36 import java.util.*; 37 import java.util.concurrent.*; 38 import javax.swing.SwingConstants; 39 40 // NOTE: This class supersedes Win32ShellFolder, which was removed from 41 // distribution after version 1.4.2. 42 43 /** 44 * Win32 Shell Folders 45 * <P> 46 * <BR> 47 * There are two fundamental types of shell folders : file system folders 48 * and non-file system folders. File system folders are relatively easy 49 * to deal with. Non-file system folders are items such as My Computer, 50 * Network Neighborhood, and the desktop. Some of these non-file system 51 * folders have special values and properties. 52 * <P> 53 * <BR> 54 * Win32 keeps two basic data structures for shell folders. The first 55 * of these is called an ITEMIDLIST. Usually a pointer, called an 56 * LPITEMIDLIST, or more frequently just "PIDL". This structure holds 57 * a series of identifiers and can be either relative to the desktop 58 * (an absolute PIDL), or relative to the shell folder that contains them. 59 * Some Win32 functions can take absolute or relative PIDL values, and 60 * others can only accept relative values. 61 * <BR> 62 * The second data structure is an IShellFolder COM interface. Using 63 * this interface, one can enumerate the relative PIDLs in a shell 64 * folder, get attributes, etc. 65 * <BR> 66 * All Win32ShellFolder2 objects which are folder types (even non-file 67 * system folders) contain an IShellFolder object. Files are named in 68 * directories via relative PIDLs. 69 * 70 * @author Michael Martak 71 * @author Leif Samuelsson 72 * @author Kenneth Russell 73 * @since 1.4 */ 74 @SuppressWarnings("serial") // JDK-implementation class 75 final class Win32ShellFolder2 extends ShellFolder { 76 77 private static native void initIDs(); 78 79 static { 80 initIDs(); 81 } 82 83 // Win32 Shell Folder Constants 84 public static final int DESKTOP = 0x0000; 85 public static final int INTERNET = 0x0001; 86 public static final int PROGRAMS = 0x0002; 87 public static final int CONTROLS = 0x0003; 88 public static final int PRINTERS = 0x0004; 89 public static final int PERSONAL = 0x0005; 90 public static final int FAVORITES = 0x0006; 91 public static final int STARTUP = 0x0007; 92 public static final int RECENT = 0x0008; 93 public static final int SENDTO = 0x0009; 94 public static final int BITBUCKET = 0x000a; 95 public static final int STARTMENU = 0x000b; 96 public static final int DESKTOPDIRECTORY = 0x0010; 97 public static final int DRIVES = 0x0011; 98 public static final int NETWORK = 0x0012; 99 public static final int NETHOOD = 0x0013; 100 public static final int FONTS = 0x0014; 101 public static final int TEMPLATES = 0x0015; 102 public static final int COMMON_STARTMENU = 0x0016; 103 public static final int COMMON_PROGRAMS = 0X0017; 104 public static final int COMMON_STARTUP = 0x0018; 105 public static final int COMMON_DESKTOPDIRECTORY = 0x0019; 106 public static final int APPDATA = 0x001a; 107 public static final int PRINTHOOD = 0x001b; 108 public static final int ALTSTARTUP = 0x001d; 109 public static final int COMMON_ALTSTARTUP = 0x001e; 110 public static final int COMMON_FAVORITES = 0x001f; 111 public static final int INTERNET_CACHE = 0x0020; 112 public static final int COOKIES = 0x0021; 113 public static final int HISTORY = 0x0022; 114 115 // Win32 shell folder attributes 116 public static final int ATTRIB_CANCOPY = 0x00000001; 117 public static final int ATTRIB_CANMOVE = 0x00000002; 118 public static final int ATTRIB_CANLINK = 0x00000004; 119 public static final int ATTRIB_CANRENAME = 0x00000010; 120 public static final int ATTRIB_CANDELETE = 0x00000020; 121 public static final int ATTRIB_HASPROPSHEET = 0x00000040; 122 public static final int ATTRIB_DROPTARGET = 0x00000100; 123 public static final int ATTRIB_LINK = 0x00010000; 124 public static final int ATTRIB_SHARE = 0x00020000; 125 public static final int ATTRIB_READONLY = 0x00040000; 126 public static final int ATTRIB_GHOSTED = 0x00080000; 127 public static final int ATTRIB_HIDDEN = 0x00080000; 128 public static final int ATTRIB_FILESYSANCESTOR = 0x10000000; 129 public static final int ATTRIB_FOLDER = 0x20000000; 130 public static final int ATTRIB_FILESYSTEM = 0x40000000; 131 public static final int ATTRIB_HASSUBFOLDER = 0x80000000; 132 public static final int ATTRIB_VALIDATE = 0x01000000; 133 public static final int ATTRIB_REMOVABLE = 0x02000000; 134 public static final int ATTRIB_COMPRESSED = 0x04000000; 135 public static final int ATTRIB_BROWSABLE = 0x08000000; 136 public static final int ATTRIB_NONENUMERATED = 0x00100000; 137 public static final int ATTRIB_NEWCONTENT = 0x00200000; 138 139 // IShellFolder::GetDisplayNameOf constants 140 public static final int SHGDN_NORMAL = 0; 141 public static final int SHGDN_INFOLDER = 1; 142 public static final int SHGDN_INCLUDE_NONFILESYS= 0x2000; 143 public static final int SHGDN_FORADDRESSBAR = 0x4000; 144 public static final int SHGDN_FORPARSING = 0x8000; 145 146 /** The referent to be registered with the Disposer. */ 147 private Object disposerReferent = new Object(); 148 149 // Values for system call LoadIcon() 150 public enum SystemIcon { 151 IDI_APPLICATION(32512), 152 IDI_HAND(32513), 153 IDI_ERROR(32513), 154 IDI_QUESTION(32514), 155 IDI_EXCLAMATION(32515), 156 IDI_WARNING(32515), 157 IDI_ASTERISK(32516), 158 IDI_INFORMATION(32516), 159 IDI_WINLOGO(32517); 160 161 private final int iconID; 162 163 SystemIcon(int iconID) { 164 this.iconID = iconID; 165 } 166 167 public int getIconID() { 168 return iconID; 169 } 170 } 171 172 // Known Folder data 173 static class KnownFolderDefinition { 174 String guid; 175 int category; 176 String name; 177 String description; 178 String parent; 179 String relativePath; 180 String parsingName; 181 String tooltip; 182 String localizedName; 183 String icon; 184 String security; 185 long attributes; 186 int defenitionFlags; 187 String ftidType; 188 String path; 189 String saveLocation; 190 static final List<KnownFolderDefinition> libraries = getLibraries(); 191 } 192 193 static class FolderDisposer implements sun.java2d.DisposerRecord { 194 /* 195 * This is cached as a concession to getFolderType(), which needs 196 * an absolute PIDL. 197 */ 198 long absolutePIDL; 199 /* 200 * We keep track of shell folders through the IShellFolder 201 * interface of their parents plus their relative PIDL. 202 */ 203 long pIShellFolder; 204 long relativePIDL; 205 206 boolean disposed; 207 public void dispose() { 208 if (disposed) return; 209 invoke(new Callable<Void>() { 210 public Void call() { 211 if (relativePIDL != 0) { 212 releasePIDL(relativePIDL); 213 } 214 if (absolutePIDL != 0) { 215 releasePIDL(absolutePIDL); 216 } 217 if (pIShellFolder != 0) { 218 releaseIShellFolder(pIShellFolder); 219 } 220 return null; 221 } 222 }); 223 disposed = true; 224 } 225 } 226 FolderDisposer disposer = new FolderDisposer(); 227 private void setIShellFolder(long pIShellFolder) { 228 disposer.pIShellFolder = pIShellFolder; 229 } 230 private void setRelativePIDL(long relativePIDL) { 231 disposer.relativePIDL = relativePIDL; 232 } 233 /* 234 * The following are for caching various shell folder properties. 235 */ 236 private long pIShellIcon = -1L; 237 private String folderType = null; 238 private String displayName = null; 239 private Image smallIcon = null; 240 private Image largeIcon = null; 241 private Boolean isDir = null; 242 private final boolean isLib; 243 private static final String FNAME = COLUMN_NAME; 244 private static final String FSIZE = COLUMN_SIZE; 245 private static final String FTYPE = "FileChooser.fileTypeHeaderText"; 246 private static final String FDATE = COLUMN_DATE; 247 248 /* 249 * The following is to identify the My Documents folder as being special 250 */ 251 private boolean isPersonal; 252 253 private static String composePathForCsidl(int csidl) throws IOException, InterruptedException { 254 String path = getFileSystemPath(csidl); 255 return path == null 256 ? ("ShellFolder: 0x" + Integer.toHexString(csidl)) 257 : path; 258 } 259 260 /** 261 * Create a system special shell folder, such as the 262 * desktop or Network Neighborhood. 263 */ 264 Win32ShellFolder2(final int csidl) throws IOException, InterruptedException { 265 // Desktop is parent of DRIVES and NETWORK, not necessarily 266 // other special shell folders. 267 super(null, composePathForCsidl(csidl)); 268 isLib = false; 269 270 invoke(new Callable<Void>() { 271 public Void call() throws InterruptedException { 272 if (csidl == DESKTOP) { 273 initDesktop(); 274 } else { 275 initSpecial(getDesktop().getIShellFolder(), csidl); 276 // At this point, the native method initSpecial() has set our relativePIDL 277 // relative to the Desktop, which may not be our immediate parent. We need 278 // to traverse this ID list and break it into a chain of shell folders from 279 // the top, with each one having an immediate parent and a relativePIDL 280 // relative to that parent. 281 long pIDL = disposer.relativePIDL; 282 parent = getDesktop(); 283 while (pIDL != 0) { 284 // Get a child pidl relative to 'parent' 285 long childPIDL = copyFirstPIDLEntry(pIDL); 286 if (childPIDL != 0) { 287 // Get a handle to the rest of the ID list 288 // i,e, parent's grandchilren and down 289 pIDL = getNextPIDLEntry(pIDL); 290 if (pIDL != 0) { 291 // Now we know that parent isn't immediate to 'this' because it 292 // has a continued ID list. Create a shell folder for this child 293 // pidl and make it the new 'parent'. 294 parent = createShellFolder((Win32ShellFolder2) parent, childPIDL); 295 } else { 296 // No grandchildren means we have arrived at the parent of 'this', 297 // and childPIDL is directly relative to parent. 298 disposer.relativePIDL = childPIDL; 299 } 300 } else { 301 break; 302 } 303 } 304 } 305 return null; 306 } 307 }, InterruptedException.class); 308 309 sun.java2d.Disposer.addObjectRecord(disposerReferent, disposer); 310 } 311 312 313 /** 314 * Create a system shell folder 315 */ 316 Win32ShellFolder2(Win32ShellFolder2 parent, long pIShellFolder, long relativePIDL, String path, boolean isLib) { 317 super(parent, (path != null) ? path : "ShellFolder: "); 318 this.isLib = isLib; 319 this.disposer.pIShellFolder = pIShellFolder; 320 this.disposer.relativePIDL = relativePIDL; 321 sun.java2d.Disposer.addObjectRecord(disposerReferent, disposer); 322 } 323 324 325 /** 326 * Creates a shell folder with a parent and relative PIDL 327 */ 328 static Win32ShellFolder2 createShellFolder(Win32ShellFolder2 parent, long pIDL) 329 throws InterruptedException { 330 String path = invoke(new Callable<String>() { 331 public String call() { 332 return getFileSystemPath(parent.getIShellFolder(), pIDL); 333 } 334 }, RuntimeException.class); 335 String libPath = resolveLibrary(path); 336 if (libPath == null) { 337 return new Win32ShellFolder2(parent, 0, pIDL, path, false); 338 } else { 339 return new Win32ShellFolder2(parent, 0, pIDL, libPath, true); 340 } 341 } 342 343 // Initializes the desktop shell folder 344 // NOTE: this method uses COM and must be called on the 'COM thread'. See ComInvoker for the details 345 private native void initDesktop(); 346 347 // Initializes a special, non-file system shell folder 348 // from one of the above constants 349 // NOTE: this method uses COM and must be called on the 'COM thread'. See ComInvoker for the details 350 private native void initSpecial(long desktopIShellFolder, int csidl); 351 352 /** Marks this folder as being the My Documents (Personal) folder */ 353 public void setIsPersonal() { 354 isPersonal = true; 355 } 356 357 /** 358 * This method is implemented to make sure that no instances 359 * of {@code ShellFolder} are ever serialized. If {@code isFileSystem()} returns 360 * {@code true}, then the object is representable with an instance of 361 * {@code java.io.File} instead. If not, then the object depends 362 * on native PIDL state and should not be serialized. 363 * 364 * @return a {@code java.io.File} replacement object. If the folder 365 * is a not a normal directory, then returns the first non-removable 366 * drive (normally "C:\"). 367 */ 368 protected Object writeReplace() throws java.io.ObjectStreamException { 369 return invoke(new Callable<File>() { 370 public File call() { 371 if (isFileSystem()) { 372 return new File(getPath()); 373 } else { 374 Win32ShellFolder2 drives = Win32ShellFolderManager2.getDrives(); 375 if (drives != null) { 376 File[] driveRoots = drives.listFiles(); 377 if (driveRoots != null) { 378 for (int i = 0; i < driveRoots.length; i++) { 379 if (driveRoots[i] instanceof Win32ShellFolder2) { 380 Win32ShellFolder2 sf = (Win32ShellFolder2) driveRoots[i]; 381 if (sf.isFileSystem() && !sf.hasAttribute(ATTRIB_REMOVABLE)) { 382 return new File(sf.getPath()); 383 } 384 } 385 } 386 } 387 } 388 // Ouch, we have no hard drives. Return something "valid" anyway. 389 return new File("C:\\"); 390 } 391 } 392 }); 393 } 394 395 396 /** 397 * Finalizer to clean up any COM objects or PIDLs used by this object. 398 */ 399 protected void dispose() { 400 disposer.dispose(); 401 } 402 403 404 // Given a (possibly multi-level) relative PIDL (with respect to 405 // the desktop, at least in all of the usage cases in this code), 406 // return a pointer to the next entry. Does not mutate the PIDL in 407 // any way. Returns 0 if the null terminator is reached. 408 // Needs to be accessible to Win32ShellFolderManager2 409 static native long getNextPIDLEntry(long pIDL); 410 411 // Given a (possibly multi-level) relative PIDL (with respect to 412 // the desktop, at least in all of the usage cases in this code), 413 // copy the first entry into a newly-allocated PIDL. Returns 0 if 414 // the PIDL is at the end of the list. 415 // Needs to be accessible to Win32ShellFolderManager2 416 static native long copyFirstPIDLEntry(long pIDL); 417 418 // Given a parent's absolute PIDL and our relative PIDL, build an absolute PIDL 419 private static native long combinePIDLs(long ppIDL, long pIDL); 420 421 // Release a PIDL object 422 // Needs to be accessible to Win32ShellFolderManager2 423 static native void releasePIDL(long pIDL); 424 425 // Release an IShellFolder object 426 // NOTE: this method uses COM and must be called on the 'COM thread'. See ComInvoker for the details 427 private static native void releaseIShellFolder(long pIShellFolder); 428 429 /** 430 * Accessor for IShellFolder 431 */ 432 private long getIShellFolder() { 433 if (disposer.pIShellFolder == 0) { 434 try { 435 disposer.pIShellFolder = invoke(new Callable<Long>() { 436 public Long call() { 437 assert(isDirectory()); 438 assert(parent != null); 439 long parentIShellFolder = getParentIShellFolder(); 440 if (parentIShellFolder == 0) { 441 throw new InternalError("Parent IShellFolder was null for " 442 + getAbsolutePath()); 443 } 444 // We are a directory with a parent and a relative PIDL. 445 // We want to bind to the parent so we get an 446 // IShellFolder instance associated with us. 447 long pIShellFolder = bindToObject(parentIShellFolder, 448 disposer.relativePIDL); 449 if (pIShellFolder == 0) { 450 throw new InternalError("Unable to bind " 451 + getAbsolutePath() + " to parent"); 452 } 453 return pIShellFolder; 454 } 455 }, RuntimeException.class); 456 } catch (InterruptedException e) { 457 // Ignore error 458 } 459 } 460 return disposer.pIShellFolder; 461 } 462 463 /** 464 * Get the parent ShellFolder's IShellFolder interface 465 */ 466 public long getParentIShellFolder() { 467 Win32ShellFolder2 parent = (Win32ShellFolder2)getParentFile(); 468 if (parent == null) { 469 // Parent should only be null if this is the desktop, whose 470 // relativePIDL is relative to its own IShellFolder. 471 return getIShellFolder(); 472 } 473 return parent.getIShellFolder(); 474 } 475 476 /** 477 * Accessor for relative PIDL 478 */ 479 public long getRelativePIDL() { 480 if (disposer.relativePIDL == 0) { 481 throw new InternalError("Should always have a relative PIDL"); 482 } 483 return disposer.relativePIDL; 484 } 485 486 private long getAbsolutePIDL() { 487 if (parent == null) { 488 // This is the desktop 489 return getRelativePIDL(); 490 } else { 491 if (disposer.absolutePIDL == 0) { 492 disposer.absolutePIDL = combinePIDLs(((Win32ShellFolder2)parent).getAbsolutePIDL(), getRelativePIDL()); 493 } 494 495 return disposer.absolutePIDL; 496 } 497 } 498 499 /** 500 * Helper function to return the desktop 501 */ 502 public Win32ShellFolder2 getDesktop() { 503 return Win32ShellFolderManager2.getDesktop(); 504 } 505 506 /** 507 * Helper function to return the desktop IShellFolder interface 508 */ 509 public long getDesktopIShellFolder() { 510 return getDesktop().getIShellFolder(); 511 } 512 513 private static boolean pathsEqual(String path1, String path2) { 514 // Same effective implementation as Win32FileSystem 515 return path1.equalsIgnoreCase(path2); 516 } 517 518 /** 519 * Check to see if two ShellFolder objects are the same 520 */ 521 public boolean equals(Object o) { 522 if (o == null || !(o instanceof Win32ShellFolder2)) { 523 // Short-circuit circuitous delegation path 524 if (!(o instanceof File)) { 525 return super.equals(o); 526 } 527 return pathsEqual(getPath(), ((File) o).getPath()); 528 } 529 Win32ShellFolder2 rhs = (Win32ShellFolder2) o; 530 if ((parent == null && rhs.parent != null) || 531 (parent != null && rhs.parent == null)) { 532 return false; 533 } 534 535 if (isFileSystem() && rhs.isFileSystem()) { 536 // Only folders with identical parents can be equal 537 return (pathsEqual(getPath(), rhs.getPath()) && 538 (parent == rhs.parent || parent.equals(rhs.parent))); 539 } 540 541 if (parent == rhs.parent || parent.equals(rhs.parent)) { 542 try { 543 return pidlsEqual(getParentIShellFolder(), disposer.relativePIDL, rhs.disposer.relativePIDL); 544 } catch (InterruptedException e) { 545 return false; 546 } 547 } 548 549 return false; 550 } 551 552 private static boolean pidlsEqual(final long pIShellFolder, final long pidl1, final long pidl2) 553 throws InterruptedException { 554 return invoke(new Callable<Boolean>() { 555 public Boolean call() { 556 return compareIDs(pIShellFolder, pidl1, pidl2) == 0; 557 } 558 }, RuntimeException.class); 559 } 560 561 // NOTE: this method uses COM and must be called on the 'COM thread'. See ComInvoker for the details 562 private static native int compareIDs(long pParentIShellFolder, long pidl1, long pidl2); 563 564 private volatile Boolean cachedIsFileSystem; 565 566 /** 567 * @return Whether this is a file system shell folder 568 */ 569 public boolean isFileSystem() { 570 if (cachedIsFileSystem == null) { 571 cachedIsFileSystem = hasAttribute(ATTRIB_FILESYSTEM); 572 } 573 574 return cachedIsFileSystem; 575 } 576 577 /** 578 * Return whether the given attribute flag is set for this object 579 */ 580 public boolean hasAttribute(final int attribute) { 581 Boolean result = invoke(new Callable<Boolean>() { 582 public Boolean call() { 583 // Caching at this point doesn't seem to be cost efficient 584 return (getAttributes0(getParentIShellFolder(), 585 getRelativePIDL(), attribute) 586 & attribute) != 0; 587 } 588 }); 589 590 return result != null && result; 591 } 592 593 /** 594 * Returns the queried attributes specified in attrsMask. 595 * 596 * Could plausibly be used for attribute caching but have to be 597 * very careful not to touch network drives and file system roots 598 * with a full attrsMask 599 * NOTE: this method uses COM and must be called on the 'COM thread'. See ComInvoker for the details 600 */ 601 602 private static native int getAttributes0(long pParentIShellFolder, long pIDL, int attrsMask); 603 604 // Return the path to the underlying file system object 605 // Should be called from the COM thread 606 private static String getFileSystemPath(final long parentIShellFolder, final long relativePIDL) { 607 int linkedFolder = ATTRIB_LINK | ATTRIB_FOLDER; 608 if (parentIShellFolder == Win32ShellFolderManager2.getNetwork().getIShellFolder() && 609 getAttributes0(parentIShellFolder, relativePIDL, linkedFolder) == linkedFolder) { 610 611 String s = 612 getFileSystemPath(Win32ShellFolderManager2.getDesktop().getIShellFolder(), 613 getLinkLocation(parentIShellFolder, relativePIDL, false)); 614 if (s != null && s.startsWith("\\\\")) { 615 return s; 616 } 617 } 618 String path = getDisplayNameOf(parentIShellFolder, relativePIDL, 619 SHGDN_FORPARSING); 620 return path; 621 } 622 623 private static String resolveLibrary(String path) { 624 // if this is a library its default save location is taken as a path 625 // this is a temp fix until java.io starts support Libraries 626 if( path != null && path.startsWith("::{") && 627 path.toLowerCase().endsWith(".library-ms")) { 628 for (KnownFolderDefinition kf : KnownFolderDefinition.libraries) { 629 if (path.toLowerCase().endsWith( 630 "\\" + kf.relativePath.toLowerCase()) && 631 path.toUpperCase().startsWith( 632 kf.parsingName.substring(0, 40).toUpperCase())) { 633 return kf.saveLocation; 634 } 635 } 636 } 637 return null; 638 } 639 640 // Needs to be accessible to Win32ShellFolderManager2 641 static String getFileSystemPath(final int csidl) throws IOException, InterruptedException { 642 String path = invoke(new Callable<String>() { 643 public String call() throws IOException { 644 return getFileSystemPath0(csidl); 645 } 646 }, IOException.class); 647 if (path != null) { 648 SecurityManager security = System.getSecurityManager(); 649 if (security != null) { 650 security.checkRead(path); 651 } 652 } 653 return path; 654 } 655 656 // NOTE: this method uses COM and must be called on the 'COM thread'. See ComInvoker for the details 657 private static native String getFileSystemPath0(int csidl) throws IOException; 658 659 // Return whether the path is a network root. 660 // Path is assumed to be non-null 661 private static boolean isNetworkRoot(String path) { 662 return (path.equals("\\\\") || path.equals("\\") || path.equals("//") || path.equals("/")); 663 } 664 665 /** 666 * @return The parent shell folder of this shell folder, null if 667 * there is no parent 668 */ 669 public File getParentFile() { 670 return parent; 671 } 672 673 public boolean isDirectory() { 674 if (isDir == null) { 675 // Folders with SFGAO_BROWSABLE have "shell extension" handlers and are 676 // not traversable in JFileChooser. 677 if (hasAttribute(ATTRIB_FOLDER) && !hasAttribute(ATTRIB_BROWSABLE)) { 678 isDir = Boolean.TRUE; 679 } else if (isLink()) { 680 ShellFolder linkLocation = getLinkLocation(false); 681 isDir = Boolean.valueOf(linkLocation != null && linkLocation.isDirectory()); 682 } else { 683 isDir = Boolean.FALSE; 684 } 685 } 686 return isDir.booleanValue(); 687 } 688 689 /* 690 * Functions for enumerating an IShellFolder's children 691 */ 692 // Returns an IEnumIDList interface for an IShellFolder. The value 693 // returned must be released using releaseEnumObjects(). 694 private long getEnumObjects(final boolean includeHiddenFiles) throws InterruptedException { 695 return invoke(new Callable<Long>() { 696 public Long call() { 697 boolean isDesktop = disposer.pIShellFolder == getDesktopIShellFolder(); 698 699 return getEnumObjects(disposer.pIShellFolder, isDesktop, includeHiddenFiles); 700 } 701 }, RuntimeException.class); 702 } 703 704 // Returns an IEnumIDList interface for an IShellFolder. The value 705 // returned must be released using releaseEnumObjects(). 706 // NOTE: this method uses COM and must be called on the 'COM thread'. See ComInvoker for the details 707 private native long getEnumObjects(long pIShellFolder, boolean isDesktop, 708 boolean includeHiddenFiles); 709 // Returns the next sequential child as a relative PIDL 710 // from an IEnumIDList interface. The value returned must 711 // be released using releasePIDL(). 712 // NOTE: this method uses COM and must be called on the 'COM thread'. See ComInvoker for the details 713 private native long getNextChild(long pEnumObjects); 714 // Releases the IEnumIDList interface 715 // NOTE: this method uses COM and must be called on the 'COM thread'. See ComInvoker for the details 716 private native void releaseEnumObjects(long pEnumObjects); 717 718 // Returns the IShellFolder of a child from a parent IShellFolder 719 // and a relative PIDL. The value returned must be released 720 // using releaseIShellFolder(). 721 // NOTE: this method uses COM and must be called on the 'COM thread'. See ComInvoker for the details 722 private static native long bindToObject(long parentIShellFolder, long pIDL); 723 724 /** 725 * @return An array of shell folders that are children of this shell folder 726 * object. The array will be empty if the folder is empty. Returns 727 * {@code null} if this shellfolder does not denote a directory. 728 */ 729 public File[] listFiles(final boolean includeHiddenFiles) { 730 SecurityManager security = System.getSecurityManager(); 731 if (security != null) { 732 security.checkRead(getPath()); 733 } 734 735 try { 736 return invoke(new Callable<File[]>() { 737 public File[] call() throws InterruptedException { 738 if (!isDirectory()) { 739 return null; 740 } 741 // Links to directories are not directories and cannot be parents. 742 // This does not apply to folders in My Network Places (NetHood) 743 // because they are both links and real directories! 744 if (isLink() && !hasAttribute(ATTRIB_FOLDER)) { 745 return new File[0]; 746 } 747 748 Win32ShellFolder2 desktop = Win32ShellFolderManager2.getDesktop(); 749 Win32ShellFolder2 personal = Win32ShellFolderManager2.getPersonal(); 750 751 // If we are a directory, we have a parent and (at least) a 752 // relative PIDL. We must first ensure we are bound to the 753 // parent so we have an IShellFolder to query. 754 long pIShellFolder = getIShellFolder(); 755 // Now we can enumerate the objects in this folder. 756 ArrayList<Win32ShellFolder2> list = new ArrayList<Win32ShellFolder2>(); 757 long pEnumObjects = getEnumObjects(includeHiddenFiles); 758 if (pEnumObjects != 0) { 759 try { 760 long childPIDL; 761 int testedAttrs = ATTRIB_FILESYSTEM | ATTRIB_FILESYSANCESTOR; 762 do { 763 childPIDL = getNextChild(pEnumObjects); 764 boolean releasePIDL = true; 765 if (childPIDL != 0 && 766 (getAttributes0(pIShellFolder, childPIDL, testedAttrs) & testedAttrs) != 0) { 767 Win32ShellFolder2 childFolder; 768 if (Win32ShellFolder2.this.equals(desktop) 769 && personal != null 770 && pidlsEqual(pIShellFolder, childPIDL, personal.disposer.relativePIDL)) { 771 childFolder = personal; 772 } else { 773 childFolder = createShellFolder(Win32ShellFolder2.this, childPIDL); 774 releasePIDL = false; 775 } 776 list.add(childFolder); 777 } 778 if (releasePIDL) { 779 releasePIDL(childPIDL); 780 } 781 } while (childPIDL != 0 && !Thread.currentThread().isInterrupted()); 782 } finally { 783 releaseEnumObjects(pEnumObjects); 784 } 785 } 786 return Thread.currentThread().isInterrupted() 787 ? new File[0] 788 : list.toArray(new ShellFolder[list.size()]); 789 } 790 }, InterruptedException.class); 791 } catch (InterruptedException e) { 792 return new File[0]; 793 } 794 } 795 796 797 /** 798 * Look for (possibly special) child folder by it's path 799 * 800 * @return The child shellfolder, or null if not found. 801 */ 802 Win32ShellFolder2 getChildByPath(final String filePath) throws InterruptedException { 803 return invoke(new Callable<Win32ShellFolder2>() { 804 public Win32ShellFolder2 call() throws InterruptedException { 805 long pIShellFolder = getIShellFolder(); 806 long pEnumObjects = getEnumObjects(true); 807 Win32ShellFolder2 child = null; 808 long childPIDL; 809 810 while ((childPIDL = getNextChild(pEnumObjects)) != 0) { 811 if (getAttributes0(pIShellFolder, childPIDL, ATTRIB_FILESYSTEM) != 0) { 812 String path = getFileSystemPath(pIShellFolder, childPIDL); 813 if(isLib) path = resolveLibrary( path ); 814 if (path != null && path.equalsIgnoreCase(filePath)) { 815 long childIShellFolder = bindToObject(pIShellFolder, childPIDL); 816 child = new Win32ShellFolder2(Win32ShellFolder2.this, 817 childIShellFolder, childPIDL, path, isLib); 818 break; 819 } 820 } 821 releasePIDL(childPIDL); 822 } 823 releaseEnumObjects(pEnumObjects); 824 return child; 825 } 826 }, InterruptedException.class); 827 } 828 829 private volatile Boolean cachedIsLink; 830 831 /** 832 * @return Whether this shell folder is a link 833 */ 834 public boolean isLink() { 835 if (cachedIsLink == null) { 836 cachedIsLink = hasAttribute(ATTRIB_LINK); 837 } 838 839 return cachedIsLink; 840 } 841 842 /** 843 * @return Whether this shell folder is marked as hidden 844 */ 845 public boolean isHidden() { 846 return hasAttribute(ATTRIB_HIDDEN); 847 } 848 849 850 // Return the link location of a shell folder 851 // NOTE: this method uses COM and must be called on the 'COM thread'. See ComInvoker for the details 852 private static native long getLinkLocation(long parentIShellFolder, 853 long relativePIDL, boolean resolve); 854 855 /** 856 * @return The shell folder linked to by this shell folder, or null 857 * if this shell folder is not a link or is a broken or invalid link 858 */ 859 public ShellFolder getLinkLocation() { 860 return getLinkLocation(true); 861 } 862 863 private Win32ShellFolder2 getLinkLocation(final boolean resolve) { 864 return invoke(new Callable<Win32ShellFolder2>() { 865 public Win32ShellFolder2 call() { 866 if (!isLink()) { 867 return null; 868 } 869 870 Win32ShellFolder2 location = null; 871 long linkLocationPIDL = getLinkLocation(getParentIShellFolder(), 872 getRelativePIDL(), resolve); 873 if (linkLocationPIDL != 0) { 874 try { 875 location = 876 Win32ShellFolderManager2.createShellFolderFromRelativePIDL(getDesktop(), 877 linkLocationPIDL); 878 } catch (InterruptedException e) { 879 // Return null 880 } catch (InternalError e) { 881 // Could be a link to a non-bindable object, such as a network connection 882 // TODO: getIShellFolder() should throw FileNotFoundException instead 883 } 884 } 885 return location; 886 } 887 }); 888 } 889 890 // Parse a display name into a PIDL relative to the current IShellFolder. 891 long parseDisplayName(final String name) throws IOException, InterruptedException { 892 return invoke(new Callable<Long>() { 893 public Long call() throws IOException { 894 return parseDisplayName0(getIShellFolder(), name); 895 } 896 }, IOException.class); 897 } 898 899 // NOTE: this method uses COM and must be called on the 'COM thread'. See ComInvoker for the details 900 private static native long parseDisplayName0(long pIShellFolder, String name) throws IOException; 901 902 // Return the display name of a shell folder 903 // NOTE: this method uses COM and must be called on the 'COM thread'. See ComInvoker for the details 904 private static native String getDisplayNameOf(long parentIShellFolder, 905 long relativePIDL, 906 int attrs); 907 908 // Returns data of all Known Folders registered in the system 909 private static native KnownFolderDefinition[] loadKnownFolders(); 910 911 /** 912 * @return The name used to display this shell folder 913 */ 914 public String getDisplayName() { 915 if (displayName == null) { 916 displayName = 917 invoke(new Callable<String>() { 918 public String call() { 919 return getDisplayNameOf(getParentIShellFolder(), 920 getRelativePIDL(), SHGDN_NORMAL); 921 } 922 }); 923 } 924 return displayName; 925 } 926 927 // Return the folder type of a shell folder 928 // NOTE: this method uses COM and must be called on the 'COM thread'. See ComInvoker for the details 929 private static native String getFolderType(long pIDL); 930 931 /** 932 * @return The type of shell folder as a string 933 */ 934 public String getFolderType() { 935 if (folderType == null) { 936 final long absolutePIDL = getAbsolutePIDL(); 937 folderType = 938 invoke(new Callable<String>() { 939 public String call() { 940 return getFolderType(absolutePIDL); 941 } 942 }); 943 } 944 return folderType; 945 } 946 947 // Return the executable type of a file system shell folder 948 private native String getExecutableType(String path); 949 950 /** 951 * @return The executable type as a string 952 */ 953 public String getExecutableType() { 954 if (!isFileSystem()) { 955 return null; 956 } 957 return getExecutableType(getAbsolutePath()); 958 } 959 960 961 962 // Icons 963 964 private static Map<Integer, Image> smallSystemImages = new HashMap<>(); 965 private static Map<Integer, Image> largeSystemImages = new HashMap<>(); 966 private static Map<Integer, Image> smallLinkedSystemImages = new HashMap<>(); 967 private static Map<Integer, Image> largeLinkedSystemImages = new HashMap<>(); 968 969 // NOTE: this method uses COM and must be called on the 'COM thread'. See ComInvoker for the details 970 private static native long getIShellIcon(long pIShellFolder); 971 972 // NOTE: this method uses COM and must be called on the 'COM thread'. See ComInvoker for the details 973 private static native int getIconIndex(long parentIShellIcon, long relativePIDL); 974 975 // Return the icon of a file system shell folder in the form of an HICON 976 private static native long getIcon(String absolutePath, boolean getLargeIcon); 977 978 // NOTE: this method uses COM and must be called on the 'COM thread'. See ComInvoker for the details 979 private static native long extractIcon(long parentIShellFolder, long relativePIDL, 980 boolean getLargeIcon, boolean getDefaultIcon); 981 982 // Returns an icon from the Windows system icon list in the form of an HICON 983 private static native long getSystemIcon(int iconID); 984 private static native long getIconResource(String libName, int iconID, 985 int cxDesired, int cyDesired, 986 boolean useVGAColors); 987 // Note: useVGAColors is ignored on XP and later 988 989 // Return the bits from an HICON. This has a side effect of setting 990 // the imageHash variable for efficient caching / comparing. 991 private static native int[] getIconBits(long hIcon); 992 // Dispose the HICON 993 private static native void disposeIcon(long hIcon); 994 995 // Get buttons from native toolbar implementation. 996 static native int[] getStandardViewButton0(int iconIndex, boolean small); 997 998 // Should be called from the COM thread 999 private long getIShellIcon() { 1000 if (pIShellIcon == -1L) { 1001 pIShellIcon = getIShellIcon(getIShellFolder()); 1002 } 1003 1004 return pIShellIcon; 1005 } 1006 1007 private static Image makeIcon(long hIcon, boolean getLargeIcon) { 1008 if (hIcon != 0L && hIcon != -1L) { 1009 // Get the bits. This has the side effect of setting the imageHash value for this object. 1010 final int[] iconBits = getIconBits(hIcon); 1011 if (iconBits != null) { 1012 // icons are always square 1013 final int size = (int) Math.sqrt(iconBits.length); 1014 final int baseSize = getLargeIcon ? 32 : 16; 1015 final BufferedImage img = 1016 new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); 1017 img.setRGB(0, 0, size, size, iconBits, 0, size); 1018 return size == baseSize 1019 ? img 1020 : new MultiResolutionIconImage(baseSize, img); 1021 } 1022 } 1023 return null; 1024 } 1025 1026 1027 /** 1028 * @return The icon image used to display this shell folder 1029 */ 1030 public Image getIcon(final boolean getLargeIcon) { 1031 Image icon = getLargeIcon ? largeIcon : smallIcon; 1032 if (icon == null) { 1033 icon = 1034 invoke(new Callable<Image>() { 1035 public Image call() { 1036 Image newIcon = null; 1037 if (isLink()) { 1038 Win32ShellFolder2 folder = getLinkLocation(false); 1039 if (folder != null && folder.isLibrary()) { 1040 return folder.getIcon(getLargeIcon); 1041 } 1042 } 1043 if (isFileSystem() || isLibrary()) { 1044 long parentIShellIcon = (parent != null) 1045 ? ((Win32ShellFolder2) parent).getIShellIcon() 1046 : 0L; 1047 long relativePIDL = getRelativePIDL(); 1048 1049 // These are cached per type (using the index in the system image list) 1050 int index = getIconIndex(parentIShellIcon, relativePIDL); 1051 if (index > 0) { 1052 Map<Integer, Image> imageCache; 1053 if (isLink()) { 1054 imageCache = getLargeIcon ? largeLinkedSystemImages : smallLinkedSystemImages; 1055 } else { 1056 imageCache = getLargeIcon ? largeSystemImages : smallSystemImages; 1057 } 1058 newIcon = imageCache.get(Integer.valueOf(index)); 1059 if (newIcon == null) { 1060 long hIcon = getIcon(getAbsolutePath(), getLargeIcon); 1061 newIcon = makeIcon(hIcon, getLargeIcon); 1062 disposeIcon(hIcon); 1063 if (newIcon != null) { 1064 imageCache.put(Integer.valueOf(index), newIcon); 1065 } 1066 } 1067 } 1068 } 1069 1070 if (newIcon == null) { 1071 // These are only cached per object 1072 long hIcon = extractIcon(getParentIShellFolder(), 1073 getRelativePIDL(), getLargeIcon, false); 1074 // E_PENDING: loading can take time so get the default 1075 if(hIcon <= 0) { 1076 hIcon = extractIcon(getParentIShellFolder(), 1077 getRelativePIDL(), getLargeIcon, true); 1078 if(hIcon <= 0) { 1079 if (isDirectory()) { 1080 return getShell32Icon(4, getLargeIcon); 1081 } else { 1082 return getShell32Icon(1, getLargeIcon); 1083 } 1084 } 1085 } 1086 newIcon = makeIcon(hIcon, getLargeIcon); 1087 disposeIcon(hIcon); 1088 } 1089 1090 if (newIcon == null) { 1091 newIcon = Win32ShellFolder2.super.getIcon(getLargeIcon); 1092 } 1093 return newIcon; 1094 } 1095 }); 1096 if (getLargeIcon) { 1097 largeIcon = icon; 1098 } else { 1099 smallIcon = icon; 1100 } 1101 } 1102 return icon; 1103 } 1104 1105 /** 1106 * Gets an icon from the Windows system icon list as an {@code Image} 1107 */ 1108 static Image getSystemIcon(SystemIcon iconType) { 1109 long hIcon = getSystemIcon(iconType.getIconID()); 1110 Image icon = makeIcon(hIcon, true); 1111 disposeIcon(hIcon); 1112 return icon; 1113 } 1114 1115 /** 1116 * Gets an icon from the Windows system icon list as an {@code Image} 1117 */ 1118 static Image getShell32Icon(int iconID, boolean getLargeIcon) { 1119 boolean useVGAColors = true; // Will be ignored on XP and later 1120 1121 int size = getLargeIcon ? 32 : 16; 1122 1123 Toolkit toolkit = Toolkit.getDefaultToolkit(); 1124 String shellIconBPP = (String)toolkit.getDesktopProperty("win.icon.shellIconBPP"); 1125 if (shellIconBPP != null) { 1126 useVGAColors = shellIconBPP.equals("4"); 1127 } 1128 1129 long hIcon = getIconResource("shell32.dll", iconID, size, size, useVGAColors); 1130 if (hIcon != 0) { 1131 Image icon = makeIcon(hIcon, getLargeIcon); 1132 disposeIcon(hIcon); 1133 return icon; 1134 } 1135 return null; 1136 } 1137 1138 /** 1139 * Returns the canonical form of this abstract pathname. Equivalent to 1140 * <code>new Win32ShellFolder2(getParentFile(), this.{@link java.io.File#getCanonicalPath}())</code>. 1141 * 1142 * @see java.io.File#getCanonicalFile 1143 */ 1144 public File getCanonicalFile() throws IOException { 1145 return this; 1146 } 1147 1148 /* 1149 * Indicates whether this is a special folder (includes My Documents) 1150 */ 1151 public boolean isSpecial() { 1152 return isPersonal || !isFileSystem() || (this == getDesktop()); 1153 } 1154 1155 /** 1156 * Compares this object with the specified object for order. 1157 * 1158 * @see sun.awt.shell.ShellFolder#compareTo(File) 1159 */ 1160 public int compareTo(File file2) { 1161 if (!(file2 instanceof Win32ShellFolder2)) { 1162 if (isFileSystem() && !isSpecial()) { 1163 return super.compareTo(file2); 1164 } else { 1165 return -1; // Non-file shellfolders sort before files 1166 } 1167 } 1168 return Win32ShellFolderManager2.compareShellFolders(this, (Win32ShellFolder2) file2); 1169 } 1170 1171 // native constants from commctrl.h 1172 private static final int LVCFMT_LEFT = 0; 1173 private static final int LVCFMT_RIGHT = 1; 1174 private static final int LVCFMT_CENTER = 2; 1175 1176 public ShellFolderColumnInfo[] getFolderColumns() { 1177 ShellFolder library = resolveLibrary(); 1178 if (library != null) return library.getFolderColumns(); 1179 return invoke(new Callable<ShellFolderColumnInfo[]>() { 1180 public ShellFolderColumnInfo[] call() { 1181 ShellFolderColumnInfo[] columns = doGetColumnInfo(getIShellFolder()); 1182 1183 if (columns != null) { 1184 List<ShellFolderColumnInfo> notNullColumns = 1185 new ArrayList<ShellFolderColumnInfo>(); 1186 for (int i = 0; i < columns.length; i++) { 1187 ShellFolderColumnInfo column = columns[i]; 1188 if (column != null) { 1189 column.setAlignment(column.getAlignment() == LVCFMT_RIGHT 1190 ? SwingConstants.RIGHT 1191 : column.getAlignment() == LVCFMT_CENTER 1192 ? SwingConstants.CENTER 1193 : SwingConstants.LEADING); 1194 1195 column.setComparator(new ColumnComparator(Win32ShellFolder2.this, i)); 1196 1197 notNullColumns.add(column); 1198 } 1199 } 1200 columns = new ShellFolderColumnInfo[notNullColumns.size()]; 1201 notNullColumns.toArray(columns); 1202 } 1203 return columns; 1204 } 1205 }); 1206 } 1207 1208 public Object getFolderColumnValue(final int column) { 1209 if(!isLibrary()) { 1210 ShellFolder library = resolveLibrary(); 1211 if (library != null) return library.getFolderColumnValue(column); 1212 } 1213 return invoke(new Callable<Object>() { 1214 public Object call() { 1215 return doGetColumnValue(getParentIShellFolder(), getRelativePIDL(), column); 1216 } 1217 }); 1218 } 1219 1220 boolean isLibrary() { 1221 return isLib; 1222 } 1223 1224 private ShellFolder resolveLibrary() { 1225 for (ShellFolder f = this; f != null; f = f.parent) { 1226 if (!f.isFileSystem()) { 1227 if (f instanceof Win32ShellFolder2 && 1228 ((Win32ShellFolder2)f).isLibrary()) { 1229 try { 1230 return getShellFolder(new File(getPath())); 1231 } catch (FileNotFoundException e) { 1232 } 1233 } 1234 break; 1235 } 1236 } 1237 return null; 1238 } 1239 1240 // NOTE: this method uses COM and must be called on the 'COM thread'. See ComInvoker for the details 1241 private native ShellFolderColumnInfo[] doGetColumnInfo(long iShellFolder2); 1242 1243 // NOTE: this method uses COM and must be called on the 'COM thread'. See ComInvoker for the details 1244 private native Object doGetColumnValue(long parentIShellFolder2, long childPIDL, int columnIdx); 1245 1246 // NOTE: this method uses COM and must be called on the 'COM thread'. See ComInvoker for the details 1247 private static native int compareIDsByColumn(long pParentIShellFolder, long pidl1, long pidl2, int columnIdx); 1248 1249 1250 public void sortChildren(final List<? extends File> files) { 1251 // To avoid loads of synchronizations with Invoker and improve performance we 1252 // synchronize the whole code of the sort method once 1253 invoke(new Callable<Void>() { 1254 public Void call() { 1255 Collections.sort(files, new ColumnComparator(Win32ShellFolder2.this, 0)); 1256 1257 return null; 1258 } 1259 }); 1260 } 1261 1262 private static class ColumnComparator implements Comparator<File> { 1263 private final Win32ShellFolder2 shellFolder; 1264 1265 private final int columnIdx; 1266 1267 public ColumnComparator(Win32ShellFolder2 shellFolder, int columnIdx) { 1268 this.shellFolder = shellFolder; 1269 this.columnIdx = columnIdx; 1270 } 1271 1272 // compares 2 objects within this folder by the specified column 1273 public int compare(final File o, final File o1) { 1274 Integer result = invoke(new Callable<Integer>() { 1275 public Integer call() { 1276 if (o instanceof Win32ShellFolder2 1277 && o1 instanceof Win32ShellFolder2) { 1278 // delegates comparison to native method 1279 return compareIDsByColumn(shellFolder.getIShellFolder(), 1280 ((Win32ShellFolder2) o).getRelativePIDL(), 1281 ((Win32ShellFolder2) o1).getRelativePIDL(), 1282 columnIdx); 1283 } 1284 return 0; 1285 } 1286 }); 1287 1288 return result == null ? 0 : result; 1289 } 1290 } 1291 1292 // Extracts libraries and their default save locations from Known Folders list 1293 private static List<KnownFolderDefinition> getLibraries() { 1294 return invoke(new Callable<List<KnownFolderDefinition>>() { 1295 @Override 1296 public List<KnownFolderDefinition> call() throws Exception { 1297 KnownFolderDefinition[] all = loadKnownFolders(); 1298 List<KnownFolderDefinition> folders = new ArrayList<>(); 1299 if (all != null) { 1300 for (KnownFolderDefinition kf : all) { 1301 if (kf.relativePath == null || kf.parsingName == null || 1302 kf.saveLocation == null) { 1303 continue; 1304 } 1305 folders.add(kf); 1306 } 1307 } 1308 return folders; 1309 } 1310 }); 1311 } 1312 1313 static class MultiResolutionIconImage extends AbstractMultiResolutionImage { 1314 1315 final int baseSize; 1316 final Image resolutionVariant; 1317 1318 public MultiResolutionIconImage(int baseSize, Image resolutionVariant) { 1319 this.baseSize = baseSize; 1320 this.resolutionVariant = resolutionVariant; 1321 } 1322 1323 @Override 1324 public int getWidth(ImageObserver observer) { 1325 return baseSize; 1326 } 1327 1328 @Override 1329 public int getHeight(ImageObserver observer) { 1330 return baseSize; 1331 } 1332 1333 @Override 1334 protected Image getBaseImage() { 1335 return resolutionVariant; 1336 } 1337 1338 @Override 1339 public Image getResolutionVariant(double width, double height) { 1340 return resolutionVariant; 1341 } 1342 1343 @Override 1344 public List<Image> getResolutionVariants() { 1345 return Arrays.asList(resolutionVariant); 1346 } 1347 } 1348 }