• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2009 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
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.sdklib.internal.repository;
18 
19 import com.android.sdklib.SdkConstants;
20 import com.android.sdklib.SdkManager;
21 
22 import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
23 import org.apache.commons.compress.archivers.zip.ZipFile;
24 
25 import java.io.File;
26 import java.io.FileInputStream;
27 import java.io.FileNotFoundException;
28 import java.io.FileOutputStream;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.net.URL;
32 import java.security.MessageDigest;
33 import java.security.NoSuchAlgorithmException;
34 import java.util.Enumeration;
35 import java.util.Properties;
36 
37 
38 /**
39  * A {@link Archive} is the base class for "something" that can be downloaded from
40  * the SDK repository.
41  * <p/>
42  * A package has some attributes (revision, description) and a list of archives
43  * which represent the downloadable bits.
44  * <p/>
45  * Packages are offered by a {@link RepoSource} (a download site).
46  */
47 public class Archive implements IDescription {
48 
49     public static final int NUM_MONITOR_INC = 100;
50     private static final String PROP_OS   = "Archive.Os";       //$NON-NLS-1$
51     private static final String PROP_ARCH = "Archive.Arch";     //$NON-NLS-1$
52 
53     /** The checksum type. */
54     public enum ChecksumType {
55         /** A SHA1 checksum, represented as a 40-hex string. */
56         SHA1("SHA-1");  //$NON-NLS-1$
57 
58         private final String mAlgorithmName;
59 
60         /**
61          * Constructs a {@link ChecksumType} with the algorigth name
62          * suitable for {@link MessageDigest#getInstance(String)}.
63          * <p/>
64          * These names are officially documented at
65          * http://java.sun.com/javase/6/docs/technotes/guides/security/StandardNames.html#MessageDigest
66          */
ChecksumType(String algorithmName)67         private ChecksumType(String algorithmName) {
68             mAlgorithmName = algorithmName;
69         }
70 
71         /**
72          * Returns a new {@link MessageDigest} instance for this checksum type.
73          * @throws NoSuchAlgorithmException if this algorithm is not available.
74          */
getMessageDigest()75         public MessageDigest getMessageDigest() throws NoSuchAlgorithmException {
76             return MessageDigest.getInstance(mAlgorithmName);
77         }
78     }
79 
80     /** The OS that this archive can be downloaded on. */
81     public enum Os {
82         ANY("Any"),
83         LINUX("Linux"),
84         MACOSX("MacOS X"),
85         WINDOWS("Windows");
86 
87         private final String mUiName;
88 
Os(String uiName)89         private Os(String uiName) {
90             mUiName = uiName;
91         }
92 
93         /** Returns the UI name of the OS. */
getUiName()94         public String getUiName() {
95             return mUiName;
96         }
97 
98         /** Returns the XML name of the OS. */
getXmlName()99         public String getXmlName() {
100             return toString().toLowerCase();
101         }
102 
103         /**
104          * Returns the current OS as one of the {@link Os} enum values or null.
105          */
getCurrentOs()106         public static Os getCurrentOs() {
107             String os = System.getProperty("os.name");          //$NON-NLS-1$
108             if (os.startsWith("Mac")) {                         //$NON-NLS-1$
109                 return Os.MACOSX;
110 
111             } else if (os.startsWith("Windows")) {              //$NON-NLS-1$
112                 return Os.WINDOWS;
113 
114             } else if (os.startsWith("Linux")) {                //$NON-NLS-1$
115                 return Os.LINUX;
116             }
117 
118             return null;
119         }
120 
121         /** Returns true if this OS is compatible with the current one. */
isCompatible()122         public boolean isCompatible() {
123             if (this == ANY) {
124                 return true;
125             }
126 
127             Os os = getCurrentOs();
128             return this == os;
129         }
130     }
131 
132     /** The Architecture that this archive can be downloaded on. */
133     public enum Arch {
134         ANY("Any"),
135         PPC("PowerPC"),
136         X86("x86"),
137         X86_64("x86_64");
138 
139         private final String mUiName;
140 
Arch(String uiName)141         private Arch(String uiName) {
142             mUiName = uiName;
143         }
144 
145         /** Returns the UI name of the architecture. */
getUiName()146         public String getUiName() {
147             return mUiName;
148         }
149 
150         /** Returns the XML name of the architecture. */
getXmlName()151         public String getXmlName() {
152             return toString().toLowerCase();
153         }
154 
155         /**
156          * Returns the current architecture as one of the {@link Arch} enum values or null.
157          */
getCurrentArch()158         public static Arch getCurrentArch() {
159             // Values listed from http://lopica.sourceforge.net/os.html
160             String arch = System.getProperty("os.arch");
161 
162             if (arch.equalsIgnoreCase("x86_64") || arch.equalsIgnoreCase("amd64")) {
163                 return Arch.X86_64;
164 
165             } else if (arch.equalsIgnoreCase("x86")
166                     || arch.equalsIgnoreCase("i386")
167                     || arch.equalsIgnoreCase("i686")) {
168                 return Arch.X86;
169 
170             } else if (arch.equalsIgnoreCase("ppc") || arch.equalsIgnoreCase("PowerPC")) {
171                 return Arch.PPC;
172             }
173 
174             return null;
175         }
176 
177         /** Returns true if this architecture is compatible with the current one. */
isCompatible()178         public boolean isCompatible() {
179             if (this == ANY) {
180                 return true;
181             }
182 
183             Arch arch = getCurrentArch();
184             return this == arch;
185         }
186     }
187 
188     private final Os     mOs;
189     private final Arch   mArch;
190     private final String mUrl;
191     private final long   mSize;
192     private final String mChecksum;
193     private final ChecksumType mChecksumType = ChecksumType.SHA1;
194     private final Package mPackage;
195     private final String mLocalOsPath;
196     private final boolean mIsLocal;
197 
198     /**
199      * Creates a new remote archive.
200      */
Archive(Package pkg, Os os, Arch arch, String url, long size, String checksum)201     Archive(Package pkg, Os os, Arch arch, String url, long size, String checksum) {
202         mPackage = pkg;
203         mOs = os;
204         mArch = arch;
205         mUrl = url;
206         mLocalOsPath = null;
207         mSize = size;
208         mChecksum = checksum;
209         mIsLocal = false;
210     }
211 
212     /**
213      * Creates a new local archive.
214      * Uses the properties from props first, if possible. Props can be null.
215      */
Archive(Package pkg, Properties props, Os os, Arch arch, String localOsPath)216     Archive(Package pkg, Properties props, Os os, Arch arch, String localOsPath) {
217         mPackage = pkg;
218 
219         mOs   = props == null ? os   : Os.valueOf(  props.getProperty(PROP_OS,   os.toString()));
220         mArch = props == null ? arch : Arch.valueOf(props.getProperty(PROP_ARCH, arch.toString()));
221 
222         mUrl = null;
223         mLocalOsPath = localOsPath;
224         mSize = 0;
225         mChecksum = "";
226         mIsLocal = true;
227     }
228 
229     /**
230      * Save the properties of the current archive in the give {@link Properties} object.
231      * These properties will later be give the constructor that takes a {@link Properties} object.
232      */
saveProperties(Properties props)233     void saveProperties(Properties props) {
234         props.setProperty(PROP_OS,   mOs.toString());
235         props.setProperty(PROP_ARCH, mArch.toString());
236     }
237 
238     /**
239      * Returns true if this is a locally installed archive.
240      * Returns false if this is a remote archive that needs to be downloaded.
241      */
isLocal()242     public boolean isLocal() {
243         return mIsLocal;
244     }
245 
246     /**
247      * Returns the package that created and owns this archive.
248      * It should generally not be null.
249      */
getParentPackage()250     public Package getParentPackage() {
251         return mPackage;
252     }
253 
254     /**
255      * Returns the archive size, an int > 0.
256      * Size will be 0 if this a local installed folder of unknown size.
257      */
getSize()258     public long getSize() {
259         return mSize;
260     }
261 
262     /**
263      * Returns the SHA1 archive checksum, as a 40-char hex.
264      * Can be empty but not null for local installed folders.
265      */
getChecksum()266     public String getChecksum() {
267         return mChecksum;
268     }
269 
270     /**
271      * Returns the checksum type, always {@link ChecksumType#SHA1} right now.
272      */
getChecksumType()273     public ChecksumType getChecksumType() {
274         return mChecksumType;
275     }
276 
277     /**
278      * Returns the download archive URL, either absolute or relative to the repository xml.
279      * Always return null for a local installed folder.
280      * @see #getLocalOsPath()
281      */
getUrl()282     public String getUrl() {
283         return mUrl;
284     }
285 
286     /**
287      * Returns the local OS folder where a local archive is installed.
288      * Always return null for remote archives.
289      * @see #getUrl()
290      */
getLocalOsPath()291     public String getLocalOsPath() {
292         return mLocalOsPath;
293     }
294 
295     /**
296      * Returns the archive {@link Os} enum.
297      * Can be null for a local installed folder on an unknown OS.
298      */
getOs()299     public Os getOs() {
300         return mOs;
301     }
302 
303     /**
304      * Returns the archive {@link Arch} enum.
305      * Can be null for a local installed folder on an unknown architecture.
306      */
getArch()307     public Arch getArch() {
308         return mArch;
309     }
310 
311     /**
312      * Generates a description for this archive of the OS/Arch supported by this archive.
313      */
getOsDescription()314     public String getOsDescription() {
315         String os;
316         if (mOs == null) {
317             os = "unknown OS";
318         } else if (mOs == Os.ANY) {
319             os = "any OS";
320         } else {
321             os = mOs.getUiName();
322         }
323 
324         String arch = "";                               //$NON-NLS-1$
325         if (mArch != null && mArch != Arch.ANY) {
326             arch = mArch.getUiName();
327         }
328 
329         return String.format("%1$s%2$s%3$s",
330                 os,
331                 arch.length() > 0 ? " " : "",           //$NON-NLS-2$
332                 arch);
333     }
334 
335     /**
336      * Generates a short description for this archive.
337      */
getShortDescription()338     public String getShortDescription() {
339         return String.format("Archive for %1$s", getOsDescription());
340     }
341 
342     /**
343      * Generates a longer description for this archive.
344      */
getLongDescription()345     public String getLongDescription() {
346         return String.format("%1$s\nSize: %2$d MiB\nSHA1: %3$s",
347                 getShortDescription(),
348                 Math.round(getSize() / (1024*1024)),
349                 getChecksum());
350     }
351 
352     /**
353      * Returns true if this archive can be installed on the current platform.
354      */
isCompatible()355     public boolean isCompatible() {
356         return getOs().isCompatible() && getArch().isCompatible();
357     }
358 
359     /**
360      * Delete the archive folder if this is a local archive.
361      */
deleteLocal()362     public void deleteLocal() {
363         if (isLocal()) {
364             deleteFileOrFolder(new File(getLocalOsPath()));
365         }
366     }
367 
368     /**
369      * Install this {@link Archive}s.
370      * The archive will be skipped if it is incompatible.
371      *
372      * @return True if the archive was installed, false otherwise.
373      */
install(String osSdkRoot, boolean forceHttp, SdkManager sdkManager, ITaskMonitor monitor)374     public boolean install(String osSdkRoot,
375             boolean forceHttp,
376             SdkManager sdkManager,
377             ITaskMonitor monitor) {
378 
379         Package pkg = getParentPackage();
380 
381         File archiveFile = null;
382         String name = pkg.getShortDescription();
383 
384         if (pkg instanceof ExtraPackage && !((ExtraPackage) pkg).isPathValid()) {
385             monitor.setResult("Skipping %1$s: %2$s is not a valid install path.",
386                     name,
387                     ((ExtraPackage) pkg).getPath());
388             return false;
389         }
390 
391         if (isLocal()) {
392             // This should never happen.
393             monitor.setResult("Skipping already installed archive: %1$s for %2$s",
394                     name,
395                     getOsDescription());
396             return false;
397         }
398 
399         if (!isCompatible()) {
400             monitor.setResult("Skipping incompatible archive: %1$s for %2$s",
401                     name,
402                     getOsDescription());
403             return false;
404         }
405 
406         archiveFile = downloadFile(osSdkRoot, monitor, forceHttp);
407         if (archiveFile != null) {
408             // Unarchive calls the pre/postInstallHook methods.
409             if (unarchive(osSdkRoot, archiveFile, sdkManager, monitor)) {
410                 monitor.setResult("Installed %1$s", name);
411                 // Delete the temp archive if it exists, only on success
412                 deleteFileOrFolder(archiveFile);
413                 return true;
414             }
415         }
416 
417         return false;
418     }
419 
420     /**
421      * Downloads an archive and returns the temp file with it.
422      * Caller is responsible with deleting the temp file when done.
423      */
downloadFile(String osSdkRoot, ITaskMonitor monitor, boolean forceHttp)424     private File downloadFile(String osSdkRoot, ITaskMonitor monitor, boolean forceHttp) {
425 
426         String name = getParentPackage().getShortDescription();
427         String desc = String.format("Downloading %1$s", name);
428         monitor.setDescription(desc);
429         monitor.setResult(desc);
430 
431         String link = getUrl();
432         if (!link.startsWith("http://")                          //$NON-NLS-1$
433                 && !link.startsWith("https://")                  //$NON-NLS-1$
434                 && !link.startsWith("ftp://")) {                 //$NON-NLS-1$
435             // Make the URL absolute by prepending the source
436             Package pkg = getParentPackage();
437             RepoSource src = pkg.getParentSource();
438             if (src == null) {
439                 monitor.setResult("Internal error: no source for archive %1$s", name);
440                 return null;
441             }
442 
443             // take the URL to the repository.xml and remove the last component
444             // to get the base
445             String repoXml = src.getUrl();
446             int pos = repoXml.lastIndexOf('/');
447             String base = repoXml.substring(0, pos + 1);
448 
449             link = base + link;
450         }
451 
452         if (forceHttp) {
453             link = link.replaceAll("https://", "http://");  //$NON-NLS-1$ //$NON-NLS-2$
454         }
455 
456         // Get the basename of the file we're downloading, i.e. the last component
457         // of the URL
458         int pos = link.lastIndexOf('/');
459         String base = link.substring(pos + 1);
460 
461         // Rather than create a real temp file in the system, we simply use our
462         // temp folder (in the SDK base folder) and use the archive name for the
463         // download. This allows us to reuse or continue downloads.
464 
465         File tmpFolder = getTempFolder(osSdkRoot);
466         if (!tmpFolder.isDirectory()) {
467             if (tmpFolder.isFile()) {
468                 deleteFileOrFolder(tmpFolder);
469             }
470             if (!tmpFolder.mkdirs()) {
471                 monitor.setResult("Failed to create directory %1$s", tmpFolder.getPath());
472                 return null;
473             }
474         }
475         File tmpFile = new File(tmpFolder, base);
476 
477         // if the file exists, check if its checksum & size. Use it if complete
478         if (tmpFile.exists()) {
479             if (tmpFile.length() == getSize() &&
480                     fileChecksum(tmpFile, monitor).equalsIgnoreCase(getChecksum())) {
481                 // File is good, let's use it.
482                 return tmpFile;
483             }
484 
485             // Existing file is either of different size or content.
486             // TODO: continue download when we support continue mode.
487             // Right now, let's simply remove the file and start over.
488             deleteFileOrFolder(tmpFile);
489         }
490 
491         if (fetchUrl(tmpFile, link, desc, monitor)) {
492             // Fetching was successful, let's use this file.
493             return tmpFile;
494         } else {
495             // Delete the temp file if we aborted the download
496             // TODO: disable this when we want to support partial downloads!
497             deleteFileOrFolder(tmpFile);
498             return null;
499         }
500     }
501 
502     /**
503      * Computes the SHA-1 checksum of the content of the given file.
504      * Returns an empty string on error (rather than null).
505      */
fileChecksum(File tmpFile, ITaskMonitor monitor)506     private String fileChecksum(File tmpFile, ITaskMonitor monitor) {
507         InputStream is = null;
508         try {
509             is = new FileInputStream(tmpFile);
510 
511             MessageDigest digester = getChecksumType().getMessageDigest();
512 
513             byte[] buf = new byte[65536];
514             int n;
515 
516             while ((n = is.read(buf)) >= 0) {
517                 if (n > 0) {
518                     digester.update(buf, 0, n);
519                 }
520             }
521 
522             return getDigestChecksum(digester);
523 
524         } catch (FileNotFoundException e) {
525             // The FNF message is just the URL. Make it a bit more useful.
526             monitor.setResult("File not found: %1$s", e.getMessage());
527 
528         } catch (Exception e) {
529             monitor.setResult(e.getMessage());
530 
531         } finally {
532             if (is != null) {
533                 try {
534                     is.close();
535                 } catch (IOException e) {
536                     // pass
537                 }
538             }
539         }
540 
541         return "";  //$NON-NLS-1$
542     }
543 
544     /**
545      * Returns the SHA-1 from a {@link MessageDigest} as an hex string
546      * that can be compared with {@link #getChecksum()}.
547      */
getDigestChecksum(MessageDigest digester)548     private String getDigestChecksum(MessageDigest digester) {
549         int n;
550         // Create an hex string from the digest
551         byte[] digest = digester.digest();
552         n = digest.length;
553         String hex = "0123456789abcdef";                     //$NON-NLS-1$
554         char[] hexDigest = new char[n * 2];
555         for (int i = 0; i < n; i++) {
556             int b = digest[i] & 0x0FF;
557             hexDigest[i*2 + 0] = hex.charAt(b >>> 4);
558             hexDigest[i*2 + 1] = hex.charAt(b & 0x0f);
559         }
560 
561         return new String(hexDigest);
562     }
563 
564     /**
565      * Actually performs the download.
566      * Also computes the SHA1 of the file on the fly.
567      * <p/>
568      * Success is defined as downloading as many bytes as was expected and having the same
569      * SHA1 as expected. Returns true on success or false if any of those checks fail.
570      * <p/>
571      * Increments the monitor by {@link #NUM_MONITOR_INC}.
572      */
fetchUrl(File tmpFile, String urlString, String description, ITaskMonitor monitor)573     private boolean fetchUrl(File tmpFile,
574             String urlString,
575             String description,
576             ITaskMonitor monitor) {
577         URL url;
578 
579         description += " (%1$d%%, %2$.0f KiB/s, %3$d %4$s left)";
580 
581         FileOutputStream os = null;
582         InputStream is = null;
583         try {
584             url = new URL(urlString);
585             is = url.openStream();
586             os = new FileOutputStream(tmpFile);
587 
588             MessageDigest digester = getChecksumType().getMessageDigest();
589 
590             byte[] buf = new byte[65536];
591             int n;
592 
593             long total = 0;
594             long size = getSize();
595             long inc = size / NUM_MONITOR_INC;
596             long next_inc = inc;
597 
598             long startMs = System.currentTimeMillis();
599             long nextMs = startMs + 2000;  // start update after 2 seconds
600 
601             while ((n = is.read(buf)) >= 0) {
602                 if (n > 0) {
603                     os.write(buf, 0, n);
604                     digester.update(buf, 0, n);
605                 }
606 
607                 long timeMs = System.currentTimeMillis();
608 
609                 total += n;
610                 if (total >= next_inc) {
611                     monitor.incProgress(1);
612                     next_inc += inc;
613                 }
614 
615                 if (timeMs > nextMs) {
616                     long delta = timeMs - startMs;
617                     if (total > 0 && delta > 0) {
618                         // percent left to download
619                         int percent = (int) (100 * total / size);
620                         // speed in KiB/s
621                         float speed = (float)total / (float)delta * (1000.f / 1024.f);
622                         // time left to download the rest at the current KiB/s rate
623                         int timeLeft = (speed > 1e-3) ?
624                                                (int)(((size - total) / 1024.0f) / speed) :
625                                                0;
626                         String timeUnit = "seconds";
627                         if (timeLeft > 120) {
628                             timeUnit = "minutes";
629                             timeLeft /= 60;
630                         }
631 
632                         monitor.setDescription(description, percent, speed, timeLeft, timeUnit);
633                     }
634                     nextMs = timeMs + 1000;  // update every second
635                 }
636 
637                 if (monitor.isCancelRequested()) {
638                     monitor.setResult("Download aborted by user at %1$d bytes.", total);
639                     return false;
640                 }
641 
642             }
643 
644             if (total != size) {
645                 monitor.setResult("Download finished with wrong size. Expected %1$d bytes, got %2$d bytes.",
646                         size, total);
647                 return false;
648             }
649 
650             // Create an hex string from the digest
651             String actual   = getDigestChecksum(digester);
652             String expected = getChecksum();
653             if (!actual.equalsIgnoreCase(expected)) {
654                 monitor.setResult("Download finished with wrong checksum. Expected %1$s, got %2$s.",
655                         expected, actual);
656                 return false;
657             }
658 
659             return true;
660 
661         } catch (FileNotFoundException e) {
662             // The FNF message is just the URL. Make it a bit more useful.
663             monitor.setResult("File not found: %1$s", e.getMessage());
664 
665         } catch (Exception e) {
666             monitor.setResult(e.getMessage());
667 
668         } finally {
669             if (os != null) {
670                 try {
671                     os.close();
672                 } catch (IOException e) {
673                     // pass
674                 }
675             }
676 
677             if (is != null) {
678                 try {
679                     is.close();
680                 } catch (IOException e) {
681                     // pass
682                 }
683             }
684         }
685 
686         return false;
687     }
688 
689     /**
690      * Install the given archive in the given folder.
691      */
unarchive(String osSdkRoot, File archiveFile, SdkManager sdkManager, ITaskMonitor monitor)692     private boolean unarchive(String osSdkRoot,
693             File archiveFile,
694             SdkManager sdkManager,
695             ITaskMonitor monitor) {
696         boolean success = false;
697         Package pkg = getParentPackage();
698         String pkgName = pkg.getShortDescription();
699         String pkgDesc = String.format("Installing %1$s", pkgName);
700         monitor.setDescription(pkgDesc);
701         monitor.setResult(pkgDesc);
702 
703         // We always unzip in a temp folder which name depends on the package type
704         // (e.g. addon, tools, etc.) and then move the folder to the destination folder.
705         // If the destination folder exists, it will be renamed and deleted at the very
706         // end if everything succeeded.
707 
708         String pkgKind = pkg.getClass().getSimpleName();
709 
710         File destFolder = null;
711         File unzipDestFolder = null;
712         File oldDestFolder = null;
713 
714         try {
715             // Find a new temp folder that doesn't exist yet
716             unzipDestFolder = createTempFolder(osSdkRoot, pkgKind, "new");  //$NON-NLS-1$
717 
718             if (unzipDestFolder == null) {
719                 // this should not seriously happen.
720                 monitor.setResult("Failed to find a temp directory in %1$s.", osSdkRoot);
721                 return false;
722             }
723 
724             if (!unzipDestFolder.mkdirs()) {
725                 monitor.setResult("Failed to create directory %1$s", unzipDestFolder.getPath());
726                 return false;
727             }
728 
729             String[] zipRootFolder = new String[] { null };
730             if (!unzipFolder(archiveFile, getSize(),
731                     unzipDestFolder, pkgDesc,
732                     zipRootFolder, monitor)) {
733                 return false;
734             }
735 
736             if (!generateSourceProperties(unzipDestFolder)) {
737                 return false;
738             }
739 
740             // Compute destination directory
741             destFolder = pkg.getInstallFolder(osSdkRoot, zipRootFolder[0], sdkManager);
742 
743             if (destFolder == null) {
744                 // this should not seriously happen.
745                 monitor.setResult("Failed to compute installation directory for %1$s.", pkgName);
746                 return false;
747             }
748 
749             if (!pkg.preInstallHook(this, monitor, osSdkRoot, destFolder)) {
750                 monitor.setResult("Skipping archive: %1$s", pkgName);
751                 return false;
752             }
753 
754             // Swap the old folder by the new one.
755             // We have 2 "folder rename" (aka moves) to do.
756             // They must both succeed in the right order.
757             boolean move1done = false;
758             boolean move2done = false;
759             while (!move1done || !move2done) {
760                 File renameFailedForDir = null;
761 
762                 // Case where the dest dir already exists
763                 if (!move1done) {
764                     if (destFolder.isDirectory()) {
765                         // Create a new temp/old dir
766                         if (oldDestFolder == null) {
767                             oldDestFolder = createTempFolder(osSdkRoot, pkgKind, "old");  //$NON-NLS-1$
768                         }
769                         if (oldDestFolder == null) {
770                             // this should not seriously happen.
771                             monitor.setResult("Failed to find a temp directory in %1$s.", osSdkRoot);
772                             return false;
773                         }
774 
775                         // try to move the current dest dir to the temp/old one
776                         if (!destFolder.renameTo(oldDestFolder)) {
777                             monitor.setResult("Failed to rename directory %1$s to %2$s.",
778                                     destFolder.getPath(), oldDestFolder.getPath());
779                             renameFailedForDir = destFolder;
780                         }
781                     }
782 
783                     move1done = (renameFailedForDir == null);
784                 }
785 
786                 // Case where there's no dest dir or we successfully moved it to temp/old
787                 // We now try to move the temp/unzip to the dest dir
788                 if (move1done && !move2done) {
789                     if (renameFailedForDir == null && !unzipDestFolder.renameTo(destFolder)) {
790                         monitor.setResult("Failed to rename directory %1$s to %2$s",
791                                 unzipDestFolder.getPath(), destFolder.getPath());
792                         renameFailedForDir = unzipDestFolder;
793                     }
794 
795                     move2done = (renameFailedForDir == null);
796                 }
797 
798                 if (renameFailedForDir != null) {
799                     if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) {
800 
801                         String msg = String.format(
802                                 "-= Warning ! =-\n" +
803                                 "A folder failed to be renamed or moved. On Windows this " +
804                                 "typically means that a program is using that folder (for example " +
805                                 "Windows Explorer or your anti-virus software.)\n" +
806                                 "Please momentarily deactivate your anti-virus software.\n" +
807                                 "Please also close any running programs that may be accessing " +
808                                 "the directory '%1$s'.\n" +
809                                 "When ready, press YES to try again.",
810                                 renameFailedForDir.getPath());
811 
812                         if (monitor.displayPrompt("SDK Manager: failed to install", msg)) {
813                             // loop, trying to rename the temp dir into the destination
814                             continue;
815                         }
816 
817                     }
818                     return false;
819                 }
820                 break;
821             }
822 
823             unzipDestFolder = null;
824             success = true;
825             pkg.postInstallHook(this, monitor, destFolder);
826             return true;
827 
828         } finally {
829             // Cleanup if the unzip folder is still set.
830             deleteFileOrFolder(oldDestFolder);
831             deleteFileOrFolder(unzipDestFolder);
832 
833             // In case of failure, we call the postInstallHool with a null directory
834             if (!success) {
835                 pkg.postInstallHook(this, monitor, null /*installDir*/);
836             }
837         }
838     }
839 
840     /**
841      * Unzips a zip file into the given destination directory.
842      *
843      * The archive file MUST have a unique "root" folder. This root folder is skipped when
844      * unarchiving. However we return that root folder name to the caller, as it can be used
845      * as a template to know what destination directory to use in the Add-on case.
846      */
847     @SuppressWarnings("unchecked")
unzipFolder(File archiveFile, long compressedSize, File unzipDestFolder, String description, String[] outZipRootFolder, ITaskMonitor monitor)848     private boolean unzipFolder(File archiveFile,
849             long compressedSize,
850             File unzipDestFolder,
851             String description,
852             String[] outZipRootFolder,
853             ITaskMonitor monitor) {
854 
855         description += " (%1$d%%)";
856 
857         ZipFile zipFile = null;
858         try {
859             zipFile = new ZipFile(archiveFile);
860 
861             // figure if we'll need to set the unix permission
862             boolean usingUnixPerm = SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN ||
863                     SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_LINUX;
864 
865             // To advance the percent and the progress bar, we don't know the number of
866             // items left to unzip. However we know the size of the archive and the size of
867             // each uncompressed item. The zip file format overhead is negligible so that's
868             // a good approximation.
869             long incStep = compressedSize / NUM_MONITOR_INC;
870             long incTotal = 0;
871             long incCurr = 0;
872             int lastPercent = 0;
873 
874             byte[] buf = new byte[65536];
875 
876             Enumeration<ZipArchiveEntry> entries = zipFile.getEntries();
877             while (entries.hasMoreElements()) {
878                 ZipArchiveEntry entry = entries.nextElement();
879 
880                 String name = entry.getName();
881 
882                 // ZipFile entries should have forward slashes, but not all Zip
883                 // implementations can be expected to do that.
884                 name = name.replace('\\', '/');
885 
886                 // Zip entries are always packages in a top-level directory
887                 // (e.g. docs/index.html). However we want to use our top-level
888                 // directory so we drop the first segment of the path name.
889                 int pos = name.indexOf('/');
890                 if (pos < 0 || pos == name.length() - 1) {
891                     continue;
892                 } else {
893                     if (outZipRootFolder[0] == null && pos > 0) {
894                         outZipRootFolder[0] = name.substring(0, pos);
895                     }
896                     name = name.substring(pos + 1);
897                 }
898 
899                 File destFile = new File(unzipDestFolder, name);
900 
901                 if (name.endsWith("/")) {  //$NON-NLS-1$
902                     // Create directory if it doesn't exist yet. This allows us to create
903                     // empty directories.
904                     if (!destFile.isDirectory() && !destFile.mkdirs()) {
905                         monitor.setResult("Failed to create temp directory %1$s",
906                                 destFile.getPath());
907                         return false;
908                     }
909                     continue;
910                 } else if (name.indexOf('/') != -1) {
911                     // Otherwise it's a file in a sub-directory.
912                     // Make sure the parent directory has been created.
913                     File parentDir = destFile.getParentFile();
914                     if (!parentDir.isDirectory()) {
915                         if (!parentDir.mkdirs()) {
916                             monitor.setResult("Failed to create temp directory %1$s",
917                                     parentDir.getPath());
918                             return false;
919                         }
920                     }
921                 }
922 
923                 FileOutputStream fos = null;
924                 try {
925                     fos = new FileOutputStream(destFile);
926                     int n;
927                     InputStream entryContent = zipFile.getInputStream(entry);
928                     while ((n = entryContent.read(buf)) != -1) {
929                         if (n > 0) {
930                             fos.write(buf, 0, n);
931                         }
932                     }
933                 } finally {
934                     if (fos != null) {
935                         fos.close();
936                     }
937                 }
938 
939                 // if needed set the permissions.
940                 if (usingUnixPerm && destFile.isFile()) {
941                     // get the mode and test if it contains the executable bit
942                     int mode = entry.getUnixMode();
943                     if ((mode & 0111) != 0) {
944                         setExecutablePermission(destFile);
945                     }
946                 }
947 
948                 // Increment progress bar to match. We update only between files.
949                 for(incTotal += entry.getCompressedSize(); incCurr < incTotal; incCurr += incStep) {
950                     monitor.incProgress(1);
951                 }
952 
953                 int percent = (int) (100 * incTotal / compressedSize);
954                 if (percent != lastPercent) {
955                     monitor.setDescription(description, percent);
956                     lastPercent = percent;
957                 }
958 
959                 if (monitor.isCancelRequested()) {
960                     return false;
961                 }
962             }
963 
964             return true;
965 
966         } catch (IOException e) {
967             monitor.setResult("Unzip failed: %1$s", e.getMessage());
968 
969         } finally {
970             if (zipFile != null) {
971                 try {
972                     zipFile.close();
973                 } catch (IOException e) {
974                     // pass
975                 }
976             }
977         }
978 
979         return false;
980     }
981 
982     /**
983      * Creates a temp folder in the form of osBasePath/temp/prefix.suffixNNN.
984      * <p/>
985      * This operation is not atomic so there's no guarantee the folder can't get
986      * created in between. This is however unlikely and the caller can assume the
987      * returned folder does not exist yet.
988      * <p/>
989      * Returns null if no such folder can be found (e.g. if all candidates exist,
990      * which is rather unlikely) or if the base temp folder cannot be created.
991      */
createTempFolder(String osBasePath, String prefix, String suffix)992     private File createTempFolder(String osBasePath, String prefix, String suffix) {
993         File baseTempFolder = getTempFolder(osBasePath);
994 
995         if (!baseTempFolder.isDirectory()) {
996             if (baseTempFolder.isFile()) {
997                 deleteFileOrFolder(baseTempFolder);
998             }
999             if (!baseTempFolder.mkdirs()) {
1000                 return null;
1001             }
1002         }
1003 
1004         for (int i = 1; i < 100; i++) {
1005             File folder = new File(baseTempFolder,
1006                     String.format("%1$s.%2$s%3$02d", prefix, suffix, i));  //$NON-NLS-1$
1007             if (!folder.exists()) {
1008                 return folder;
1009             }
1010         }
1011         return null;
1012     }
1013 
1014     /**
1015      * Returns the temp folder used by the SDK Manager.
1016      * This folder is always at osBasePath/temp.
1017      */
getTempFolder(String osBasePath)1018     private File getTempFolder(String osBasePath) {
1019         File baseTempFolder = new File(osBasePath, "temp");     //$NON-NLS-1$
1020         return baseTempFolder;
1021     }
1022 
1023     /**
1024      * Deletes a file or a directory.
1025      * Directories are deleted recursively.
1026      * The argument can be null.
1027      */
deleteFileOrFolder(File fileOrFolder)1028     private void deleteFileOrFolder(File fileOrFolder) {
1029         if (fileOrFolder != null) {
1030             if (fileOrFolder.isDirectory()) {
1031                 // Must delete content recursively first
1032                 for (File item : fileOrFolder.listFiles()) {
1033                     deleteFileOrFolder(item);
1034                 }
1035             }
1036             if (!fileOrFolder.delete()) {
1037                 fileOrFolder.deleteOnExit();
1038             }
1039         }
1040     }
1041 
1042     /**
1043      * Generates a source.properties in the destination folder that contains all the infos
1044      * relevant to this archive, this package and the source so that we can reload them
1045      * locally later.
1046      */
generateSourceProperties(File unzipDestFolder)1047     private boolean generateSourceProperties(File unzipDestFolder) {
1048         Properties props = new Properties();
1049 
1050         saveProperties(props);
1051         mPackage.saveProperties(props);
1052 
1053         FileOutputStream fos = null;
1054         try {
1055             File f = new File(unzipDestFolder, SdkConstants.FN_SOURCE_PROP);
1056 
1057             fos = new FileOutputStream(f);
1058 
1059             props.store( fos, "## Android Tool: Source of this archive.");  //$NON-NLS-1$
1060 
1061             return true;
1062         } catch (IOException e) {
1063             e.printStackTrace();
1064         } finally {
1065             if (fos != null) {
1066                 try {
1067                     fos.close();
1068                 } catch (IOException e) {
1069                 }
1070             }
1071         }
1072 
1073         return false;
1074     }
1075 
1076     /**
1077      * Sets the executable Unix permission (0777) on a file or folder.
1078      * @param file The file to set permissions on.
1079      * @throws IOException If an I/O error occurs
1080      */
setExecutablePermission(File file)1081     private void setExecutablePermission(File file) throws IOException {
1082         Runtime.getRuntime().exec(new String[] {
1083            "chmod", "777", file.getAbsolutePath()
1084         });
1085     }
1086 }
1087