Home | History | Annotate | Download | only in repository
      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 contained in 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          */
     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          */
     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 
     89         private Os(String uiName) {
     90             mUiName = uiName;
     91         }
     92 
     93         /** Returns the UI name of the OS. */
     94         public String getUiName() {
     95             return mUiName;
     96         }
     97 
     98         /** Returns the XML name of the OS. */
     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          */
    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. */
    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 
    141         private Arch(String uiName) {
    142             mUiName = uiName;
    143         }
    144 
    145         /** Returns the UI name of the architecture. */
    146         public String getUiName() {
    147             return mUiName;
    148         }
    149 
    150         /** Returns the XML name of the architecture. */
    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          */
    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. */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    266     public String getChecksum() {
    267         return mChecksum;
    268     }
    269 
    270     /**
    271      * Returns the checksum type, always {@link ChecksumType#SHA1} right now.
    272      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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")
    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      */
    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      */
   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      */
   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      */
   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      */
   1081     private void setExecutablePermission(File file) throws IOException {
   1082         Runtime.getRuntime().exec(new String[] {
   1083            "chmod", "777", file.getAbsolutePath()
   1084         });
   1085     }
   1086 }
   1087