OSDN Git Service

Merge: SDK Manager: Split install logic out of the Archive class.
authorRaphael Moll <ralf@android.com>
Wed, 29 Dec 2010 04:12:55 +0000 (20:12 -0800)
committerRaphael Moll <ralf@android.com>
Sat, 8 Jan 2011 02:16:25 +0000 (18:16 -0800)
Change-Id: I75a616dfcb957b915f68679dbe3371d7abf2b3bc

sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Archive.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ArchiveInstaller.java [new file with mode: 0755]
sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterData.java

index 2bab0b5..7739f8c 100755 (executable)
 \r
 package com.android.sdklib.internal.repository;\r
 \r
-import com.android.sdklib.SdkConstants;\r
-import com.android.sdklib.SdkManager;\r
-import com.android.sdklib.repository.RepoConstants;\r
-\r
-import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;\r
-import org.apache.commons.compress.archivers.zip.ZipFile;\r
-\r
 import java.io.File;\r
-import java.io.FileInputStream;\r
-import java.io.FileNotFoundException;\r
-import java.io.FileOutputStream;\r
-import java.io.IOException;\r
-import java.io.InputStream;\r
-import java.net.URL;\r
 import java.security.MessageDigest;\r
 import java.security.NoSuchAlgorithmException;\r
-import java.util.Enumeration;\r
 import java.util.Properties;\r
 \r
 \r
@@ -44,10 +30,10 @@ import java.util.Properties;
  * which represent the downloadable bits.\r
  * <p/>\r
  * Packages are offered by a {@link SdkSource} (a download site).\r
+ * The {@link ArchiveInstaller} takes care of downloading, unpacking and installing an archive.\r
  */\r
 public class Archive implements IDescription, Comparable<Archive> {\r
 \r
-    public static final int NUM_MONITOR_INC = 100;\r
     private static final String PROP_OS   = "Archive.Os";       //$NON-NLS-1$\r
     private static final String PROP_ARCH = "Archive.Arch";     //$NON-NLS-1$\r
 \r
@@ -378,730 +364,11 @@ public class Archive implements IDescription, Comparable<Archive> {
      */\r
     public void deleteLocal() {\r
         if (isLocal()) {\r
-            deleteFileOrFolder(new File(getLocalOsPath()));\r
-        }\r
-    }\r
-\r
-    /**\r
-     * Install this {@link Archive}s.\r
-     * The archive will be skipped if it is incompatible.\r
-     *\r
-     * @return True if the archive was installed, false otherwise.\r
-     */\r
-    public boolean install(String osSdkRoot,\r
-            boolean forceHttp,\r
-            SdkManager sdkManager,\r
-            ITaskMonitor monitor) {\r
-\r
-        Package pkg = getParentPackage();\r
-\r
-        File archiveFile = null;\r
-        String name = pkg.getShortDescription();\r
-\r
-        if (pkg instanceof ExtraPackage && !((ExtraPackage) pkg).isPathValid()) {\r
-            monitor.setResult("Skipping %1$s: %2$s is not a valid install path.",\r
-                    name,\r
-                    ((ExtraPackage) pkg).getPath());\r
-            return false;\r
-        }\r
-\r
-        if (isLocal()) {\r
-            // This should never happen.\r
-            monitor.setResult("Skipping already installed archive: %1$s for %2$s",\r
-                    name,\r
-                    getOsDescription());\r
-            return false;\r
-        }\r
-\r
-        if (!isCompatible()) {\r
-            monitor.setResult("Skipping incompatible archive: %1$s for %2$s",\r
-                    name,\r
-                    getOsDescription());\r
-            return false;\r
-        }\r
-\r
-        archiveFile = downloadFile(osSdkRoot, monitor, forceHttp);\r
-        if (archiveFile != null) {\r
-            // Unarchive calls the pre/postInstallHook methods.\r
-            if (unarchive(osSdkRoot, archiveFile, sdkManager, monitor)) {\r
-                monitor.setResult("Installed %1$s", name);\r
-                // Delete the temp archive if it exists, only on success\r
-                deleteFileOrFolder(archiveFile);\r
-                return true;\r
-            }\r
-        }\r
-\r
-        return false;\r
-    }\r
-\r
-    /**\r
-     * Downloads an archive and returns the temp file with it.\r
-     * Caller is responsible with deleting the temp file when done.\r
-     */\r
-    private File downloadFile(String osSdkRoot, ITaskMonitor monitor, boolean forceHttp) {\r
-\r
-        String name = getParentPackage().getShortDescription();\r
-        String desc = String.format("Downloading %1$s", name);\r
-        monitor.setDescription(desc);\r
-        monitor.setResult(desc);\r
-\r
-        String link = getUrl();\r
-        if (!link.startsWith("http://")                          //$NON-NLS-1$\r
-                && !link.startsWith("https://")                  //$NON-NLS-1$\r
-                && !link.startsWith("ftp://")) {                 //$NON-NLS-1$\r
-            // Make the URL absolute by prepending the source\r
-            Package pkg = getParentPackage();\r
-            SdkSource src = pkg.getParentSource();\r
-            if (src == null) {\r
-                monitor.setResult("Internal error: no source for archive %1$s", name);\r
-                return null;\r
-            }\r
-\r
-            // take the URL to the repository.xml and remove the last component\r
-            // to get the base\r
-            String repoXml = src.getUrl();\r
-            int pos = repoXml.lastIndexOf('/');\r
-            String base = repoXml.substring(0, pos + 1);\r
-\r
-            link = base + link;\r
-        }\r
-\r
-        if (forceHttp) {\r
-            link = link.replaceAll("https://", "http://");  //$NON-NLS-1$ //$NON-NLS-2$\r
-        }\r
-\r
-        // Get the basename of the file we're downloading, i.e. the last component\r
-        // of the URL\r
-        int pos = link.lastIndexOf('/');\r
-        String base = link.substring(pos + 1);\r
-\r
-        // Rather than create a real temp file in the system, we simply use our\r
-        // temp folder (in the SDK base folder) and use the archive name for the\r
-        // download. This allows us to reuse or continue downloads.\r
-\r
-        File tmpFolder = getTempFolder(osSdkRoot);\r
-        if (!tmpFolder.isDirectory()) {\r
-            if (tmpFolder.isFile()) {\r
-                deleteFileOrFolder(tmpFolder);\r
-            }\r
-            if (!tmpFolder.mkdirs()) {\r
-                monitor.setResult("Failed to create directory %1$s", tmpFolder.getPath());\r
-                return null;\r
-            }\r
-        }\r
-        File tmpFile = new File(tmpFolder, base);\r
-\r
-        // if the file exists, check if its checksum & size. Use it if complete\r
-        if (tmpFile.exists()) {\r
-            if (tmpFile.length() == getSize() &&\r
-                    fileChecksum(tmpFile, monitor).equalsIgnoreCase(getChecksum())) {\r
-                // File is good, let's use it.\r
-                return tmpFile;\r
-            }\r
-\r
-            // Existing file is either of different size or content.\r
-            // TODO: continue download when we support continue mode.\r
-            // Right now, let's simply remove the file and start over.\r
-            deleteFileOrFolder(tmpFile);\r
-        }\r
-\r
-        if (fetchUrl(tmpFile, link, desc, monitor)) {\r
-            // Fetching was successful, let's use this file.\r
-            return tmpFile;\r
-        } else {\r
-            // Delete the temp file if we aborted the download\r
-            // TODO: disable this when we want to support partial downloads!\r
-            deleteFileOrFolder(tmpFile);\r
-            return null;\r
-        }\r
-    }\r
-\r
-    /**\r
-     * Computes the SHA-1 checksum of the content of the given file.\r
-     * Returns an empty string on error (rather than null).\r
-     */\r
-    private String fileChecksum(File tmpFile, ITaskMonitor monitor) {\r
-        InputStream is = null;\r
-        try {\r
-            is = new FileInputStream(tmpFile);\r
-\r
-            MessageDigest digester = getChecksumType().getMessageDigest();\r
-\r
-            byte[] buf = new byte[65536];\r
-            int n;\r
-\r
-            while ((n = is.read(buf)) >= 0) {\r
-                if (n > 0) {\r
-                    digester.update(buf, 0, n);\r
-                }\r
-            }\r
-\r
-            return getDigestChecksum(digester);\r
-\r
-        } catch (FileNotFoundException e) {\r
-            // The FNF message is just the URL. Make it a bit more useful.\r
-            monitor.setResult("File not found: %1$s", e.getMessage());\r
-\r
-        } catch (Exception e) {\r
-            monitor.setResult(e.getMessage());\r
-\r
-        } finally {\r
-            if (is != null) {\r
-                try {\r
-                    is.close();\r
-                } catch (IOException e) {\r
-                    // pass\r
-                }\r
-            }\r
-        }\r
-\r
-        return "";  //$NON-NLS-1$\r
-    }\r
-\r
-    /**\r
-     * Returns the SHA-1 from a {@link MessageDigest} as an hex string\r
-     * that can be compared with {@link #getChecksum()}.\r
-     */\r
-    private String getDigestChecksum(MessageDigest digester) {\r
-        int n;\r
-        // Create an hex string from the digest\r
-        byte[] digest = digester.digest();\r
-        n = digest.length;\r
-        String hex = "0123456789abcdef";                     //$NON-NLS-1$\r
-        char[] hexDigest = new char[n * 2];\r
-        for (int i = 0; i < n; i++) {\r
-            int b = digest[i] & 0x0FF;\r
-            hexDigest[i*2 + 0] = hex.charAt(b >>> 4);\r
-            hexDigest[i*2 + 1] = hex.charAt(b & 0x0f);\r
-        }\r
-\r
-        return new String(hexDigest);\r
-    }\r
-\r
-    /**\r
-     * Actually performs the download.\r
-     * Also computes the SHA1 of the file on the fly.\r
-     * <p/>\r
-     * Success is defined as downloading as many bytes as was expected and having the same\r
-     * SHA1 as expected. Returns true on success or false if any of those checks fail.\r
-     * <p/>\r
-     * Increments the monitor by {@link #NUM_MONITOR_INC}.\r
-     */\r
-    private boolean fetchUrl(File tmpFile,\r
-            String urlString,\r
-            String description,\r
-            ITaskMonitor monitor) {\r
-        URL url;\r
-\r
-        description += " (%1$d%%, %2$.0f KiB/s, %3$d %4$s left)";\r
-\r
-        FileOutputStream os = null;\r
-        InputStream is = null;\r
-        try {\r
-            url = new URL(urlString);\r
-            is = url.openStream();\r
-            os = new FileOutputStream(tmpFile);\r
-\r
-            MessageDigest digester = getChecksumType().getMessageDigest();\r
-\r
-            byte[] buf = new byte[65536];\r
-            int n;\r
-\r
-            long total = 0;\r
-            long size = getSize();\r
-            long inc = size / NUM_MONITOR_INC;\r
-            long next_inc = inc;\r
-\r
-            long startMs = System.currentTimeMillis();\r
-            long nextMs = startMs + 2000;  // start update after 2 seconds\r
-\r
-            while ((n = is.read(buf)) >= 0) {\r
-                if (n > 0) {\r
-                    os.write(buf, 0, n);\r
-                    digester.update(buf, 0, n);\r
-                }\r
-\r
-                long timeMs = System.currentTimeMillis();\r
-\r
-                total += n;\r
-                if (total >= next_inc) {\r
-                    monitor.incProgress(1);\r
-                    next_inc += inc;\r
-                }\r
-\r
-                if (timeMs > nextMs) {\r
-                    long delta = timeMs - startMs;\r
-                    if (total > 0 && delta > 0) {\r
-                        // percent left to download\r
-                        int percent = (int) (100 * total / size);\r
-                        // speed in KiB/s\r
-                        float speed = (float)total / (float)delta * (1000.f / 1024.f);\r
-                        // time left to download the rest at the current KiB/s rate\r
-                        int timeLeft = (speed > 1e-3) ?\r
-                                               (int)(((size - total) / 1024.0f) / speed) :\r
-                                               0;\r
-                        String timeUnit = "seconds";\r
-                        if (timeLeft > 120) {\r
-                            timeUnit = "minutes";\r
-                            timeLeft /= 60;\r
-                        }\r
-\r
-                        monitor.setDescription(description, percent, speed, timeLeft, timeUnit);\r
-                    }\r
-                    nextMs = timeMs + 1000;  // update every second\r
-                }\r
-\r
-                if (monitor.isCancelRequested()) {\r
-                    monitor.setResult("Download aborted by user at %1$d bytes.", total);\r
-                    return false;\r
-                }\r
-\r
-            }\r
-\r
-            if (total != size) {\r
-                monitor.setResult("Download finished with wrong size. Expected %1$d bytes, got %2$d bytes.",\r
-                        size, total);\r
-                return false;\r
-            }\r
-\r
-            // Create an hex string from the digest\r
-            String actual   = getDigestChecksum(digester);\r
-            String expected = getChecksum();\r
-            if (!actual.equalsIgnoreCase(expected)) {\r
-                monitor.setResult("Download finished with wrong checksum. Expected %1$s, got %2$s.",\r
-                        expected, actual);\r
-                return false;\r
-            }\r
-\r
-            return true;\r
-\r
-        } catch (FileNotFoundException e) {\r
-            // The FNF message is just the URL. Make it a bit more useful.\r
-            monitor.setResult("File not found: %1$s", e.getMessage());\r
-\r
-        } catch (Exception e) {\r
-            monitor.setResult(e.getMessage());\r
-\r
-        } finally {\r
-            if (os != null) {\r
-                try {\r
-                    os.close();\r
-                } catch (IOException e) {\r
-                    // pass\r
-                }\r
-            }\r
-\r
-            if (is != null) {\r
-                try {\r
-                    is.close();\r
-                } catch (IOException e) {\r
-                    // pass\r
-                }\r
-            }\r
-        }\r
-\r
-        return false;\r
-    }\r
-\r
-    /**\r
-     * Install the given archive in the given folder.\r
-     */\r
-    private boolean unarchive(String osSdkRoot,\r
-            File archiveFile,\r
-            SdkManager sdkManager,\r
-            ITaskMonitor monitor) {\r
-        boolean success = false;\r
-        Package pkg = getParentPackage();\r
-        String pkgName = pkg.getShortDescription();\r
-        String pkgDesc = String.format("Installing %1$s", pkgName);\r
-        monitor.setDescription(pkgDesc);\r
-        monitor.setResult(pkgDesc);\r
-\r
-        // We always unzip in a temp folder which name depends on the package type\r
-        // (e.g. addon, tools, etc.) and then move the folder to the destination folder.\r
-        // If the destination folder exists, it will be renamed and deleted at the very\r
-        // end if everything succeeded.\r
-\r
-        String pkgKind = pkg.getClass().getSimpleName();\r
-\r
-        File destFolder = null;\r
-        File unzipDestFolder = null;\r
-        File oldDestFolder = null;\r
-\r
-        try {\r
-            // Find a new temp folder that doesn't exist yet\r
-            unzipDestFolder = createTempFolder(osSdkRoot, pkgKind, "new");  //$NON-NLS-1$\r
-\r
-            if (unzipDestFolder == null) {\r
-                // this should not seriously happen.\r
-                monitor.setResult("Failed to find a temp directory in %1$s.", osSdkRoot);\r
-                return false;\r
-            }\r
-\r
-            if (!unzipDestFolder.mkdirs()) {\r
-                monitor.setResult("Failed to create directory %1$s", unzipDestFolder.getPath());\r
-                return false;\r
-            }\r
-\r
-            String[] zipRootFolder = new String[] { null };\r
-            if (!unzipFolder(archiveFile, getSize(),\r
-                    unzipDestFolder, pkgDesc,\r
-                    zipRootFolder, monitor)) {\r
-                return false;\r
-            }\r
-\r
-            if (!generateSourceProperties(unzipDestFolder)) {\r
-                return false;\r
-            }\r
-\r
-            // Compute destination directory\r
-            destFolder = pkg.getInstallFolder(osSdkRoot, zipRootFolder[0], sdkManager);\r
-\r
-            if (destFolder == null) {\r
-                // this should not seriously happen.\r
-                monitor.setResult("Failed to compute installation directory for %1$s.", pkgName);\r
-                return false;\r
-            }\r
-\r
-            if (!pkg.preInstallHook(this, monitor, osSdkRoot, destFolder)) {\r
-                monitor.setResult("Skipping archive: %1$s", pkgName);\r
-                return false;\r
-            }\r
-\r
-            // Swap the old folder by the new one.\r
-            // We have 2 "folder rename" (aka moves) to do.\r
-            // They must both succeed in the right order.\r
-            boolean move1done = false;\r
-            boolean move2done = false;\r
-            while (!move1done || !move2done) {\r
-                File renameFailedForDir = null;\r
-\r
-                // Case where the dest dir already exists\r
-                if (!move1done) {\r
-                    if (destFolder.isDirectory()) {\r
-                        // Create a new temp/old dir\r
-                        if (oldDestFolder == null) {\r
-                            oldDestFolder = createTempFolder(osSdkRoot, pkgKind, "old");  //$NON-NLS-1$\r
-                        }\r
-                        if (oldDestFolder == null) {\r
-                            // this should not seriously happen.\r
-                            monitor.setResult("Failed to find a temp directory in %1$s.", osSdkRoot);\r
-                            return false;\r
-                        }\r
-\r
-                        // try to move the current dest dir to the temp/old one\r
-                        if (!destFolder.renameTo(oldDestFolder)) {\r
-                            monitor.setResult("Failed to rename directory %1$s to %2$s.",\r
-                                    destFolder.getPath(), oldDestFolder.getPath());\r
-                            renameFailedForDir = destFolder;\r
-                        }\r
-                    }\r
-\r
-                    move1done = (renameFailedForDir == null);\r
-                }\r
-\r
-                // Case where there's no dest dir or we successfully moved it to temp/old\r
-                // We now try to move the temp/unzip to the dest dir\r
-                if (move1done && !move2done) {\r
-                    if (renameFailedForDir == null && !unzipDestFolder.renameTo(destFolder)) {\r
-                        monitor.setResult("Failed to rename directory %1$s to %2$s",\r
-                                unzipDestFolder.getPath(), destFolder.getPath());\r
-                        renameFailedForDir = unzipDestFolder;\r
-                    }\r
-\r
-                    move2done = (renameFailedForDir == null);\r
-                }\r
-\r
-                if (renameFailedForDir != null) {\r
-                    if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) {\r
-\r
-                        String msg = String.format(\r
-                                "-= Warning ! =-\n" +\r
-                                "A folder failed to be renamed or moved. On Windows this " +\r
-                                "typically means that a program is using that folder (for example " +\r
-                                "Windows Explorer or your anti-virus software.)\n" +\r
-                                "Please momentarily deactivate your anti-virus software.\n" +\r
-                                "Please also close any running programs that may be accessing " +\r
-                                "the directory '%1$s'.\n" +\r
-                                "When ready, press YES to try again.",\r
-                                renameFailedForDir.getPath());\r
-\r
-                        if (monitor.displayPrompt("SDK Manager: failed to install", msg)) {\r
-                            // loop, trying to rename the temp dir into the destination\r
-                            continue;\r
-                        }\r
-\r
-                    }\r
-                    return false;\r
-                }\r
-                break;\r
-            }\r
-\r
-            unzipDestFolder = null;\r
-            success = true;\r
-            pkg.postInstallHook(this, monitor, destFolder);\r
-            return true;\r
-\r
-        } finally {\r
-            // Cleanup if the unzip folder is still set.\r
-            deleteFileOrFolder(oldDestFolder);\r
-            deleteFileOrFolder(unzipDestFolder);\r
-\r
-            // In case of failure, we call the postInstallHool with a null directory\r
-            if (!success) {\r
-                pkg.postInstallHook(this, monitor, null /*installDir*/);\r
-            }\r
+            new ArchiveInstaller().deleteFileOrFolder(new File(getLocalOsPath()));\r
         }\r
     }\r
 \r
     /**\r
-     * Unzips a zip file into the given destination directory.\r
-     *\r
-     * The archive file MUST have a unique "root" folder. This root folder is skipped when\r
-     * unarchiving. However we return that root folder name to the caller, as it can be used\r
-     * as a template to know what destination directory to use in the Add-on case.\r
-     */\r
-    @SuppressWarnings("unchecked")\r
-    private boolean unzipFolder(File archiveFile,\r
-            long compressedSize,\r
-            File unzipDestFolder,\r
-            String description,\r
-            String[] outZipRootFolder,\r
-            ITaskMonitor monitor) {\r
-\r
-        description += " (%1$d%%)";\r
-\r
-        ZipFile zipFile = null;\r
-        try {\r
-            zipFile = new ZipFile(archiveFile);\r
-\r
-            // figure if we'll need to set the unix permission\r
-            boolean usingUnixPerm = SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN ||\r
-                    SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_LINUX;\r
-\r
-            // To advance the percent and the progress bar, we don't know the number of\r
-            // items left to unzip. However we know the size of the archive and the size of\r
-            // each uncompressed item. The zip file format overhead is negligible so that's\r
-            // a good approximation.\r
-            long incStep = compressedSize / NUM_MONITOR_INC;\r
-            long incTotal = 0;\r
-            long incCurr = 0;\r
-            int lastPercent = 0;\r
-\r
-            byte[] buf = new byte[65536];\r
-\r
-            Enumeration<ZipArchiveEntry> entries = zipFile.getEntries();\r
-            while (entries.hasMoreElements()) {\r
-                ZipArchiveEntry entry = entries.nextElement();\r
-\r
-                String name = entry.getName();\r
-\r
-                // ZipFile entries should have forward slashes, but not all Zip\r
-                // implementations can be expected to do that.\r
-                name = name.replace('\\', '/');\r
-\r
-                // Zip entries are always packages in a top-level directory\r
-                // (e.g. docs/index.html). However we want to use our top-level\r
-                // directory so we drop the first segment of the path name.\r
-                int pos = name.indexOf('/');\r
-                if (pos < 0 || pos == name.length() - 1) {\r
-                    continue;\r
-                } else {\r
-                    if (outZipRootFolder[0] == null && pos > 0) {\r
-                        outZipRootFolder[0] = name.substring(0, pos);\r
-                    }\r
-                    name = name.substring(pos + 1);\r
-                }\r
-\r
-                File destFile = new File(unzipDestFolder, name);\r
-\r
-                if (name.endsWith("/")) {  //$NON-NLS-1$\r
-                    // Create directory if it doesn't exist yet. This allows us to create\r
-                    // empty directories.\r
-                    if (!destFile.isDirectory() && !destFile.mkdirs()) {\r
-                        monitor.setResult("Failed to create temp directory %1$s",\r
-                                destFile.getPath());\r
-                        return false;\r
-                    }\r
-                    continue;\r
-                } else if (name.indexOf('/') != -1) {\r
-                    // Otherwise it's a file in a sub-directory.\r
-                    // Make sure the parent directory has been created.\r
-                    File parentDir = destFile.getParentFile();\r
-                    if (!parentDir.isDirectory()) {\r
-                        if (!parentDir.mkdirs()) {\r
-                            monitor.setResult("Failed to create temp directory %1$s",\r
-                                    parentDir.getPath());\r
-                            return false;\r
-                        }\r
-                    }\r
-                }\r
-\r
-                FileOutputStream fos = null;\r
-                try {\r
-                    fos = new FileOutputStream(destFile);\r
-                    int n;\r
-                    InputStream entryContent = zipFile.getInputStream(entry);\r
-                    while ((n = entryContent.read(buf)) != -1) {\r
-                        if (n > 0) {\r
-                            fos.write(buf, 0, n);\r
-                        }\r
-                    }\r
-                } finally {\r
-                    if (fos != null) {\r
-                        fos.close();\r
-                    }\r
-                }\r
-\r
-                // if needed set the permissions.\r
-                if (usingUnixPerm && destFile.isFile()) {\r
-                    // get the mode and test if it contains the executable bit\r
-                    int mode = entry.getUnixMode();\r
-                    if ((mode & 0111) != 0) {\r
-                        setExecutablePermission(destFile);\r
-                    }\r
-                }\r
-\r
-                // Increment progress bar to match. We update only between files.\r
-                for(incTotal += entry.getCompressedSize(); incCurr < incTotal; incCurr += incStep) {\r
-                    monitor.incProgress(1);\r
-                }\r
-\r
-                int percent = (int) (100 * incTotal / compressedSize);\r
-                if (percent != lastPercent) {\r
-                    monitor.setDescription(description, percent);\r
-                    lastPercent = percent;\r
-                }\r
-\r
-                if (monitor.isCancelRequested()) {\r
-                    return false;\r
-                }\r
-            }\r
-\r
-            return true;\r
-\r
-        } catch (IOException e) {\r
-            monitor.setResult("Unzip failed: %1$s", e.getMessage());\r
-\r
-        } finally {\r
-            if (zipFile != null) {\r
-                try {\r
-                    zipFile.close();\r
-                } catch (IOException e) {\r
-                    // pass\r
-                }\r
-            }\r
-        }\r
-\r
-        return false;\r
-    }\r
-\r
-    /**\r
-     * Creates a temp folder in the form of osBasePath/temp/prefix.suffixNNN.\r
-     * <p/>\r
-     * This operation is not atomic so there's no guarantee the folder can't get\r
-     * created in between. This is however unlikely and the caller can assume the\r
-     * returned folder does not exist yet.\r
-     * <p/>\r
-     * Returns null if no such folder can be found (e.g. if all candidates exist,\r
-     * which is rather unlikely) or if the base temp folder cannot be created.\r
-     */\r
-    private File createTempFolder(String osBasePath, String prefix, String suffix) {\r
-        File baseTempFolder = getTempFolder(osBasePath);\r
-\r
-        if (!baseTempFolder.isDirectory()) {\r
-            if (baseTempFolder.isFile()) {\r
-                deleteFileOrFolder(baseTempFolder);\r
-            }\r
-            if (!baseTempFolder.mkdirs()) {\r
-                return null;\r
-            }\r
-        }\r
-\r
-        for (int i = 1; i < 100; i++) {\r
-            File folder = new File(baseTempFolder,\r
-                    String.format("%1$s.%2$s%3$02d", prefix, suffix, i));  //$NON-NLS-1$\r
-            if (!folder.exists()) {\r
-                return folder;\r
-            }\r
-        }\r
-        return null;\r
-    }\r
-\r
-    /**\r
-     * Returns the temp folder used by the SDK Manager.\r
-     * This folder is always at osBasePath/temp.\r
-     */\r
-    private File getTempFolder(String osBasePath) {\r
-        File baseTempFolder = new File(osBasePath, RepoConstants.FD_TEMP);\r
-        return baseTempFolder;\r
-    }\r
-\r
-    /**\r
-     * Deletes a file or a directory.\r
-     * Directories are deleted recursively.\r
-     * The argument can be null.\r
-     */\r
-    private void deleteFileOrFolder(File fileOrFolder) {\r
-        if (fileOrFolder != null) {\r
-            if (fileOrFolder.isDirectory()) {\r
-                // Must delete content recursively first\r
-                for (File item : fileOrFolder.listFiles()) {\r
-                    deleteFileOrFolder(item);\r
-                }\r
-            }\r
-            if (!fileOrFolder.delete()) {\r
-                fileOrFolder.deleteOnExit();\r
-            }\r
-        }\r
-    }\r
-\r
-    /**\r
-     * Generates a source.properties in the destination folder that contains all the infos\r
-     * relevant to this archive, this package and the source so that we can reload them\r
-     * locally later.\r
-     */\r
-    private boolean generateSourceProperties(File unzipDestFolder) {\r
-        Properties props = new Properties();\r
-\r
-        saveProperties(props);\r
-        mPackage.saveProperties(props);\r
-\r
-        FileOutputStream fos = null;\r
-        try {\r
-            File f = new File(unzipDestFolder, SdkConstants.FN_SOURCE_PROP);\r
-\r
-            fos = new FileOutputStream(f);\r
-\r
-            props.store( fos, "## Android Tool: Source of this archive.");  //$NON-NLS-1$\r
-\r
-            return true;\r
-        } catch (IOException e) {\r
-            e.printStackTrace();\r
-        } finally {\r
-            if (fos != null) {\r
-                try {\r
-                    fos.close();\r
-                } catch (IOException e) {\r
-                }\r
-            }\r
-        }\r
-\r
-        return false;\r
-    }\r
-\r
-    /**\r
-     * Sets the executable Unix permission (0777) on a file or folder.\r
-     * @param file The file to set permissions on.\r
-     * @throws IOException If an I/O error occurs\r
-     */\r
-    private void setExecutablePermission(File file) throws IOException {\r
-        Runtime.getRuntime().exec(new String[] {\r
-           "chmod", "777", file.getAbsolutePath()\r
-        });\r
-    }\r
-\r
-    /**\r
      * Archives are compared using their {@link Package} ordering.\r
      *\r
      * @see Package#compareTo(Package)\r
diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ArchiveInstaller.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ArchiveInstaller.java
new file mode 100755 (executable)
index 0000000..b98f731
--- /dev/null
@@ -0,0 +1,781 @@
+/*\r
+ * Copyright (C) 2009 The Android Open Source Project\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *      http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+\r
+package com.android.sdklib.internal.repository;\r
+\r
+import com.android.sdklib.SdkConstants;\r
+import com.android.sdklib.SdkManager;\r
+import com.android.sdklib.repository.RepoConstants;\r
+\r
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;\r
+import org.apache.commons.compress.archivers.zip.ZipFile;\r
+\r
+import java.io.File;\r
+import java.io.FileInputStream;\r
+import java.io.FileNotFoundException;\r
+import java.io.FileOutputStream;\r
+import java.io.IOException;\r
+import java.io.InputStream;\r
+import java.net.URL;\r
+import java.security.MessageDigest;\r
+import java.security.NoSuchAlgorithmException;\r
+import java.util.Enumeration;\r
+import java.util.Properties;\r
+\r
+\r
+/**\r
+ * Performs the work of installing a given {@link Archive}.\r
+ */\r
+public class ArchiveInstaller {\r
+\r
+    public static final int NUM_MONITOR_INC = 100;\r
+\r
+    /**\r
+     * Install this {@link ArchiveInstaller}s.\r
+     * The archive will be skipped if it is incompatible.\r
+     *\r
+     * @return True if the archive was installed, false otherwise.\r
+     */\r
+    public boolean install(Archive archive,\r
+            String osSdkRoot,\r
+            boolean forceHttp,\r
+            SdkManager sdkManager,\r
+            ITaskMonitor monitor) {\r
+\r
+        Package pkg = archive.getParentPackage();\r
+\r
+        File archiveFile = null;\r
+        String name = pkg.getShortDescription();\r
+\r
+        if (pkg instanceof ExtraPackage && !((ExtraPackage) pkg).isPathValid()) {\r
+            monitor.setResult("Skipping %1$s: %2$s is not a valid install path.",\r
+                    name,\r
+                    ((ExtraPackage) pkg).getPath());\r
+            return false;\r
+        }\r
+\r
+        if (archive.isLocal()) {\r
+            // This should never happen.\r
+            monitor.setResult("Skipping already installed archive: %1$s for %2$s",\r
+                    name,\r
+                    archive.getOsDescription());\r
+            return false;\r
+        }\r
+\r
+        if (!archive.isCompatible()) {\r
+            monitor.setResult("Skipping incompatible archive: %1$s for %2$s",\r
+                    name,\r
+                    archive.getOsDescription());\r
+            return false;\r
+        }\r
+\r
+        archiveFile = downloadFile(archive, osSdkRoot, monitor, forceHttp);\r
+        if (archiveFile != null) {\r
+            // Unarchive calls the pre/postInstallHook methods.\r
+            if (unarchive(archive, osSdkRoot, archiveFile, sdkManager, monitor)) {\r
+                monitor.setResult("Installed %1$s", name);\r
+                // Delete the temp archive if it exists, only on success\r
+                deleteFileOrFolder(archiveFile);\r
+                return true;\r
+            }\r
+        }\r
+\r
+        return false;\r
+    }\r
+\r
+    /**\r
+     * Downloads an archive and returns the temp file with it.\r
+     * Caller is responsible with deleting the temp file when done.\r
+     */\r
+    private File downloadFile(Archive archive,\r
+            String osSdkRoot,\r
+            ITaskMonitor monitor,\r
+            boolean forceHttp) {\r
+\r
+        String name = archive.getParentPackage().getShortDescription();\r
+        String desc = String.format("Downloading %1$s", name);\r
+        monitor.setDescription(desc);\r
+        monitor.setResult(desc);\r
+\r
+        String link = archive.getUrl();\r
+        if (!link.startsWith("http://")                          //$NON-NLS-1$\r
+                && !link.startsWith("https://")                  //$NON-NLS-1$\r
+                && !link.startsWith("ftp://")) {                 //$NON-NLS-1$\r
+            // Make the URL absolute by prepending the source\r
+            Package pkg = archive.getParentPackage();\r
+            SdkSource src = pkg.getParentSource();\r
+            if (src == null) {\r
+                monitor.setResult("Internal error: no source for archive %1$s", name);\r
+                return null;\r
+            }\r
+\r
+            // take the URL to the repository.xml and remove the last component\r
+            // to get the base\r
+            String repoXml = src.getUrl();\r
+            int pos = repoXml.lastIndexOf('/');\r
+            String base = repoXml.substring(0, pos + 1);\r
+\r
+            link = base + link;\r
+        }\r
+\r
+        if (forceHttp) {\r
+            link = link.replaceAll("https://", "http://");  //$NON-NLS-1$ //$NON-NLS-2$\r
+        }\r
+\r
+        // Get the basename of the file we're downloading, i.e. the last component\r
+        // of the URL\r
+        int pos = link.lastIndexOf('/');\r
+        String base = link.substring(pos + 1);\r
+\r
+        // Rather than create a real temp file in the system, we simply use our\r
+        // temp folder (in the SDK base folder) and use the archive name for the\r
+        // download. This allows us to reuse or continue downloads.\r
+\r
+        File tmpFolder = getTempFolder(osSdkRoot);\r
+        if (!tmpFolder.isDirectory()) {\r
+            if (tmpFolder.isFile()) {\r
+                deleteFileOrFolder(tmpFolder);\r
+            }\r
+            if (!tmpFolder.mkdirs()) {\r
+                monitor.setResult("Failed to create directory %1$s", tmpFolder.getPath());\r
+                return null;\r
+            }\r
+        }\r
+        File tmpFile = new File(tmpFolder, base);\r
+\r
+        // if the file exists, check its checksum & size. Use it if complete\r
+        if (tmpFile.exists()) {\r
+            if (tmpFile.length() == archive.getSize()) {\r
+                String chksum = "";\r
+                try {\r
+                    chksum = fileChecksum(archive.getChecksumType().getMessageDigest(),\r
+                                          tmpFile,\r
+                                          monitor);\r
+                } catch (NoSuchAlgorithmException e) {\r
+                    // Ignore.\r
+                }\r
+                if (chksum.equalsIgnoreCase(archive.getChecksum())) {\r
+                    // File is good, let's use it.\r
+                    return tmpFile;\r
+                }\r
+            }\r
+\r
+            // Existing file is either of different size or content.\r
+            // TODO: continue download when we support continue mode.\r
+            // Right now, let's simply remove the file and start over.\r
+            deleteFileOrFolder(tmpFile);\r
+        }\r
+\r
+        if (fetchUrl(archive, tmpFile, link, desc, monitor)) {\r
+            // Fetching was successful, let's use this file.\r
+            return tmpFile;\r
+        } else {\r
+            // Delete the temp file if we aborted the download\r
+            // TODO: disable this when we want to support partial downloads!\r
+            deleteFileOrFolder(tmpFile);\r
+            return null;\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Computes the SHA-1 checksum of the content of the given file.\r
+     * Returns an empty string on error (rather than null).\r
+     */\r
+    private String fileChecksum(MessageDigest digester, File tmpFile, ITaskMonitor monitor) {\r
+        InputStream is = null;\r
+        try {\r
+            is = new FileInputStream(tmpFile);\r
+\r
+            byte[] buf = new byte[65536];\r
+            int n;\r
+\r
+            while ((n = is.read(buf)) >= 0) {\r
+                if (n > 0) {\r
+                    digester.update(buf, 0, n);\r
+                }\r
+            }\r
+\r
+            return getDigestChecksum(digester);\r
+\r
+        } catch (FileNotFoundException e) {\r
+            // The FNF message is just the URL. Make it a bit more useful.\r
+            monitor.setResult("File not found: %1$s", e.getMessage());\r
+\r
+        } catch (Exception e) {\r
+            monitor.setResult(e.getMessage());\r
+\r
+        } finally {\r
+            if (is != null) {\r
+                try {\r
+                    is.close();\r
+                } catch (IOException e) {\r
+                    // pass\r
+                }\r
+            }\r
+        }\r
+\r
+        return "";  //$NON-NLS-1$\r
+    }\r
+\r
+    /**\r
+     * Returns the SHA-1 from a {@link MessageDigest} as an hex string\r
+     * that can be compared with {@link Archive#getChecksum()}.\r
+     */\r
+    private String getDigestChecksum(MessageDigest digester) {\r
+        int n;\r
+        // Create an hex string from the digest\r
+        byte[] digest = digester.digest();\r
+        n = digest.length;\r
+        String hex = "0123456789abcdef";                     //$NON-NLS-1$\r
+        char[] hexDigest = new char[n * 2];\r
+        for (int i = 0; i < n; i++) {\r
+            int b = digest[i] & 0x0FF;\r
+            hexDigest[i*2 + 0] = hex.charAt(b >>> 4);\r
+            hexDigest[i*2 + 1] = hex.charAt(b & 0x0f);\r
+        }\r
+\r
+        return new String(hexDigest);\r
+    }\r
+\r
+    /**\r
+     * Actually performs the download.\r
+     * Also computes the SHA1 of the file on the fly.\r
+     * <p/>\r
+     * Success is defined as downloading as many bytes as was expected and having the same\r
+     * SHA1 as expected. Returns true on success or false if any of those checks fail.\r
+     * <p/>\r
+     * Increments the monitor by {@link #NUM_MONITOR_INC}.\r
+     */\r
+    private boolean fetchUrl(Archive archive,\r
+            File tmpFile,\r
+            String urlString,\r
+            String description,\r
+            ITaskMonitor monitor) {\r
+        URL url;\r
+\r
+        description += " (%1$d%%, %2$.0f KiB/s, %3$d %4$s left)";\r
+\r
+        FileOutputStream os = null;\r
+        InputStream is = null;\r
+        try {\r
+            url = new URL(urlString);\r
+            is = url.openStream();\r
+            os = new FileOutputStream(tmpFile);\r
+\r
+            MessageDigest digester = archive.getChecksumType().getMessageDigest();\r
+\r
+            byte[] buf = new byte[65536];\r
+            int n;\r
+\r
+            long total = 0;\r
+            long size = archive.getSize();\r
+            long inc = size / NUM_MONITOR_INC;\r
+            long next_inc = inc;\r
+\r
+            long startMs = System.currentTimeMillis();\r
+            long nextMs = startMs + 2000;  // start update after 2 seconds\r
+\r
+            while ((n = is.read(buf)) >= 0) {\r
+                if (n > 0) {\r
+                    os.write(buf, 0, n);\r
+                    digester.update(buf, 0, n);\r
+                }\r
+\r
+                long timeMs = System.currentTimeMillis();\r
+\r
+                total += n;\r
+                if (total >= next_inc) {\r
+                    monitor.incProgress(1);\r
+                    next_inc += inc;\r
+                }\r
+\r
+                if (timeMs > nextMs) {\r
+                    long delta = timeMs - startMs;\r
+                    if (total > 0 && delta > 0) {\r
+                        // percent left to download\r
+                        int percent = (int) (100 * total / size);\r
+                        // speed in KiB/s\r
+                        float speed = (float)total / (float)delta * (1000.f / 1024.f);\r
+                        // time left to download the rest at the current KiB/s rate\r
+                        int timeLeft = (speed > 1e-3) ?\r
+                                               (int)(((size - total) / 1024.0f) / speed) :\r
+                                               0;\r
+                        String timeUnit = "seconds";\r
+                        if (timeLeft > 120) {\r
+                            timeUnit = "minutes";\r
+                            timeLeft /= 60;\r
+                        }\r
+\r
+                        monitor.setDescription(description, percent, speed, timeLeft, timeUnit);\r
+                    }\r
+                    nextMs = timeMs + 1000;  // update every second\r
+                }\r
+\r
+                if (monitor.isCancelRequested()) {\r
+                    monitor.setResult("Download aborted by user at %1$d bytes.", total);\r
+                    return false;\r
+                }\r
+\r
+            }\r
+\r
+            if (total != size) {\r
+                monitor.setResult("Download finished with wrong size. Expected %1$d bytes, got %2$d bytes.",\r
+                        size, total);\r
+                return false;\r
+            }\r
+\r
+            // Create an hex string from the digest\r
+            String actual   = getDigestChecksum(digester);\r
+            String expected = archive.getChecksum();\r
+            if (!actual.equalsIgnoreCase(expected)) {\r
+                monitor.setResult("Download finished with wrong checksum. Expected %1$s, got %2$s.",\r
+                        expected, actual);\r
+                return false;\r
+            }\r
+\r
+            return true;\r
+\r
+        } catch (FileNotFoundException e) {\r
+            // The FNF message is just the URL. Make it a bit more useful.\r
+            monitor.setResult("File not found: %1$s", e.getMessage());\r
+\r
+        } catch (Exception e) {\r
+            monitor.setResult(e.getMessage());\r
+\r
+        } finally {\r
+            if (os != null) {\r
+                try {\r
+                    os.close();\r
+                } catch (IOException e) {\r
+                    // pass\r
+                }\r
+            }\r
+\r
+            if (is != null) {\r
+                try {\r
+                    is.close();\r
+                } catch (IOException e) {\r
+                    // pass\r
+                }\r
+            }\r
+        }\r
+\r
+        return false;\r
+    }\r
+\r
+    /**\r
+     * Install the given archive in the given folder.\r
+     */\r
+    private boolean unarchive(Archive archive,\r
+            String osSdkRoot,\r
+            File archiveFile,\r
+            SdkManager sdkManager,\r
+            ITaskMonitor monitor) {\r
+        boolean success = false;\r
+        Package pkg = archive.getParentPackage();\r
+        String pkgName = pkg.getShortDescription();\r
+        String pkgDesc = String.format("Installing %1$s", pkgName);\r
+        monitor.setDescription(pkgDesc);\r
+        monitor.setResult(pkgDesc);\r
+\r
+        // We always unzip in a temp folder which name depends on the package type\r
+        // (e.g. addon, tools, etc.) and then move the folder to the destination folder.\r
+        // If the destination folder exists, it will be renamed and deleted at the very\r
+        // end if everything succeeded.\r
+\r
+        String pkgKind = pkg.getClass().getSimpleName();\r
+\r
+        File destFolder = null;\r
+        File unzipDestFolder = null;\r
+        File oldDestFolder = null;\r
+\r
+        try {\r
+            // Find a new temp folder that doesn't exist yet\r
+            unzipDestFolder = createTempFolder(osSdkRoot, pkgKind, "new");  //$NON-NLS-1$\r
+\r
+            if (unzipDestFolder == null) {\r
+                // this should not seriously happen.\r
+                monitor.setResult("Failed to find a temp directory in %1$s.", osSdkRoot);\r
+                return false;\r
+            }\r
+\r
+            if (!unzipDestFolder.mkdirs()) {\r
+                monitor.setResult("Failed to create directory %1$s", unzipDestFolder.getPath());\r
+                return false;\r
+            }\r
+\r
+            String[] zipRootFolder = new String[] { null };\r
+            if (!unzipFolder(archiveFile, archive.getSize(),\r
+                    unzipDestFolder, pkgDesc,\r
+                    zipRootFolder, monitor)) {\r
+                return false;\r
+            }\r
+\r
+            if (!generateSourceProperties(archive, unzipDestFolder)) {\r
+                return false;\r
+            }\r
+\r
+            // Compute destination directory\r
+            destFolder = pkg.getInstallFolder(osSdkRoot, zipRootFolder[0], sdkManager);\r
+\r
+            if (destFolder == null) {\r
+                // this should not seriously happen.\r
+                monitor.setResult("Failed to compute installation directory for %1$s.", pkgName);\r
+                return false;\r
+            }\r
+\r
+            if (!pkg.preInstallHook(archive, monitor, osSdkRoot, destFolder)) {\r
+                monitor.setResult("Skipping archive: %1$s", pkgName);\r
+                return false;\r
+            }\r
+\r
+            // Swap the old folder by the new one.\r
+            // We have 2 "folder rename" (aka moves) to do.\r
+            // They must both succeed in the right order.\r
+            boolean move1done = false;\r
+            boolean move2done = false;\r
+            while (!move1done || !move2done) {\r
+                File renameFailedForDir = null;\r
+\r
+                // Case where the dest dir already exists\r
+                if (!move1done) {\r
+                    if (destFolder.isDirectory()) {\r
+                        // Create a new temp/old dir\r
+                        if (oldDestFolder == null) {\r
+                            oldDestFolder = createTempFolder(osSdkRoot, pkgKind, "old");  //$NON-NLS-1$\r
+                        }\r
+                        if (oldDestFolder == null) {\r
+                            // this should not seriously happen.\r
+                            monitor.setResult("Failed to find a temp directory in %1$s.", osSdkRoot);\r
+                            return false;\r
+                        }\r
+\r
+                        // try to move the current dest dir to the temp/old one\r
+                        if (!destFolder.renameTo(oldDestFolder)) {\r
+                            monitor.setResult("Failed to rename directory %1$s to %2$s.",\r
+                                    destFolder.getPath(), oldDestFolder.getPath());\r
+                            renameFailedForDir = destFolder;\r
+                        }\r
+                    }\r
+\r
+                    move1done = (renameFailedForDir == null);\r
+                }\r
+\r
+                // Case where there's no dest dir or we successfully moved it to temp/old\r
+                // We now try to move the temp/unzip to the dest dir\r
+                if (move1done && !move2done) {\r
+                    if (renameFailedForDir == null && !unzipDestFolder.renameTo(destFolder)) {\r
+                        monitor.setResult("Failed to rename directory %1$s to %2$s",\r
+                                unzipDestFolder.getPath(), destFolder.getPath());\r
+                        renameFailedForDir = unzipDestFolder;\r
+                    }\r
+\r
+                    move2done = (renameFailedForDir == null);\r
+                }\r
+\r
+                if (renameFailedForDir != null) {\r
+                    if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) {\r
+\r
+                        String msg = String.format(\r
+                                "-= Warning ! =-\n" +\r
+                                "A folder failed to be renamed or moved. On Windows this " +\r
+                                "typically means that a program is using that folder (for example " +\r
+                                "Windows Explorer or your anti-virus software.)\n" +\r
+                                "Please momentarily deactivate your anti-virus software.\n" +\r
+                                "Please also close any running programs that may be accessing " +\r
+                                "the directory '%1$s'.\n" +\r
+                                "When ready, press YES to try again.",\r
+                                renameFailedForDir.getPath());\r
+\r
+                        if (monitor.displayPrompt("SDK Manager: failed to install", msg)) {\r
+                            // loop, trying to rename the temp dir into the destination\r
+                            continue;\r
+                        }\r
+\r
+                    }\r
+                    return false;\r
+                }\r
+                break;\r
+            }\r
+\r
+            unzipDestFolder = null;\r
+            success = true;\r
+            pkg.postInstallHook(archive, monitor, destFolder);\r
+            return true;\r
+\r
+        } finally {\r
+            // Cleanup if the unzip folder is still set.\r
+            deleteFileOrFolder(oldDestFolder);\r
+            deleteFileOrFolder(unzipDestFolder);\r
+\r
+            // In case of failure, we call the postInstallHool with a null directory\r
+            if (!success) {\r
+                pkg.postInstallHook(archive, monitor, null /*installDir*/);\r
+            }\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Unzips a zip file into the given destination directory.\r
+     *\r
+     * The archive file MUST have a unique "root" folder. This root folder is skipped when\r
+     * unarchiving. However we return that root folder name to the caller, as it can be used\r
+     * as a template to know what destination directory to use in the Add-on case.\r
+     */\r
+    @SuppressWarnings("unchecked")\r
+    private boolean unzipFolder(File archiveFile,\r
+            long compressedSize,\r
+            File unzipDestFolder,\r
+            String description,\r
+            String[] outZipRootFolder,\r
+            ITaskMonitor monitor) {\r
+\r
+        description += " (%1$d%%)";\r
+\r
+        ZipFile zipFile = null;\r
+        try {\r
+            zipFile = new ZipFile(archiveFile);\r
+\r
+            // figure if we'll need to set the unix permission\r
+            boolean usingUnixPerm = SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN ||\r
+                    SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_LINUX;\r
+\r
+            // To advance the percent and the progress bar, we don't know the number of\r
+            // items left to unzip. However we know the size of the archive and the size of\r
+            // each uncompressed item. The zip file format overhead is negligible so that's\r
+            // a good approximation.\r
+            long incStep = compressedSize / NUM_MONITOR_INC;\r
+            long incTotal = 0;\r
+            long incCurr = 0;\r
+            int lastPercent = 0;\r
+\r
+            byte[] buf = new byte[65536];\r
+\r
+            Enumeration<ZipArchiveEntry> entries = zipFile.getEntries();\r
+            while (entries.hasMoreElements()) {\r
+                ZipArchiveEntry entry = entries.nextElement();\r
+\r
+                String name = entry.getName();\r
+\r
+                // ZipFile entries should have forward slashes, but not all Zip\r
+                // implementations can be expected to do that.\r
+                name = name.replace('\\', '/');\r
+\r
+                // Zip entries are always packages in a top-level directory\r
+                // (e.g. docs/index.html). However we want to use our top-level\r
+                // directory so we drop the first segment of the path name.\r
+                int pos = name.indexOf('/');\r
+                if (pos < 0 || pos == name.length() - 1) {\r
+                    continue;\r
+                } else {\r
+                    if (outZipRootFolder[0] == null && pos > 0) {\r
+                        outZipRootFolder[0] = name.substring(0, pos);\r
+                    }\r
+                    name = name.substring(pos + 1);\r
+                }\r
+\r
+                File destFile = new File(unzipDestFolder, name);\r
+\r
+                if (name.endsWith("/")) {  //$NON-NLS-1$\r
+                    // Create directory if it doesn't exist yet. This allows us to create\r
+                    // empty directories.\r
+                    if (!destFile.isDirectory() && !destFile.mkdirs()) {\r
+                        monitor.setResult("Failed to create temp directory %1$s",\r
+                                destFile.getPath());\r
+                        return false;\r
+                    }\r
+                    continue;\r
+                } else if (name.indexOf('/') != -1) {\r
+                    // Otherwise it's a file in a sub-directory.\r
+                    // Make sure the parent directory has been created.\r
+                    File parentDir = destFile.getParentFile();\r
+                    if (!parentDir.isDirectory()) {\r
+                        if (!parentDir.mkdirs()) {\r
+                            monitor.setResult("Failed to create temp directory %1$s",\r
+                                    parentDir.getPath());\r
+                            return false;\r
+                        }\r
+                    }\r
+                }\r
+\r
+                FileOutputStream fos = null;\r
+                try {\r
+                    fos = new FileOutputStream(destFile);\r
+                    int n;\r
+                    InputStream entryContent = zipFile.getInputStream(entry);\r
+                    while ((n = entryContent.read(buf)) != -1) {\r
+                        if (n > 0) {\r
+                            fos.write(buf, 0, n);\r
+                        }\r
+                    }\r
+                } finally {\r
+                    if (fos != null) {\r
+                        fos.close();\r
+                    }\r
+                }\r
+\r
+                // if needed set the permissions.\r
+                if (usingUnixPerm && destFile.isFile()) {\r
+                    // get the mode and test if it contains the executable bit\r
+                    int mode = entry.getUnixMode();\r
+                    if ((mode & 0111) != 0) {\r
+                        setExecutablePermission(destFile);\r
+                    }\r
+                }\r
+\r
+                // Increment progress bar to match. We update only between files.\r
+                for(incTotal += entry.getCompressedSize(); incCurr < incTotal; incCurr += incStep) {\r
+                    monitor.incProgress(1);\r
+                }\r
+\r
+                int percent = (int) (100 * incTotal / compressedSize);\r
+                if (percent != lastPercent) {\r
+                    monitor.setDescription(description, percent);\r
+                    lastPercent = percent;\r
+                }\r
+\r
+                if (monitor.isCancelRequested()) {\r
+                    return false;\r
+                }\r
+            }\r
+\r
+            return true;\r
+\r
+        } catch (IOException e) {\r
+            monitor.setResult("Unzip failed: %1$s", e.getMessage());\r
+\r
+        } finally {\r
+            if (zipFile != null) {\r
+                try {\r
+                    zipFile.close();\r
+                } catch (IOException e) {\r
+                    // pass\r
+                }\r
+            }\r
+        }\r
+\r
+        return false;\r
+    }\r
+\r
+    /**\r
+     * Creates a temp folder in the form of osBasePath/temp/prefix.suffixNNN.\r
+     * <p/>\r
+     * This operation is not atomic so there's no guarantee the folder can't get\r
+     * created in between. This is however unlikely and the caller can assume the\r
+     * returned folder does not exist yet.\r
+     * <p/>\r
+     * Returns null if no such folder can be found (e.g. if all candidates exist,\r
+     * which is rather unlikely) or if the base temp folder cannot be created.\r
+     */\r
+    private File createTempFolder(String osBasePath, String prefix, String suffix) {\r
+        File baseTempFolder = getTempFolder(osBasePath);\r
+\r
+        if (!baseTempFolder.isDirectory()) {\r
+            if (baseTempFolder.isFile()) {\r
+                deleteFileOrFolder(baseTempFolder);\r
+            }\r
+            if (!baseTempFolder.mkdirs()) {\r
+                return null;\r
+            }\r
+        }\r
+\r
+        for (int i = 1; i < 100; i++) {\r
+            File folder = new File(baseTempFolder,\r
+                    String.format("%1$s.%2$s%3$02d", prefix, suffix, i));  //$NON-NLS-1$\r
+            if (!folder.exists()) {\r
+                return folder;\r
+            }\r
+        }\r
+        return null;\r
+    }\r
+\r
+    /**\r
+     * Returns the temp folder used by the SDK Manager.\r
+     * This folder is always at osBasePath/temp.\r
+     */\r
+    private File getTempFolder(String osBasePath) {\r
+        File baseTempFolder = new File(osBasePath, RepoConstants.FD_TEMP);\r
+        return baseTempFolder;\r
+    }\r
+\r
+    /**\r
+     * Deletes a file or a directory.\r
+     * Directories are deleted recursively.\r
+     * The argument can be null.\r
+     */\r
+    /*package*/ void deleteFileOrFolder(File fileOrFolder) {\r
+        if (fileOrFolder != null) {\r
+            if (fileOrFolder.isDirectory()) {\r
+                // Must delete content recursively first\r
+                for (File item : fileOrFolder.listFiles()) {\r
+                    deleteFileOrFolder(item);\r
+                }\r
+            }\r
+            if (!fileOrFolder.delete()) {\r
+                fileOrFolder.deleteOnExit();\r
+            }\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Generates a source.properties in the destination folder that contains all the infos\r
+     * relevant to this archive, this package and the source so that we can reload them\r
+     * locally later.\r
+     */\r
+    private boolean generateSourceProperties(Archive archive, File unzipDestFolder) {\r
+        Properties props = new Properties();\r
+\r
+        archive.saveProperties(props);\r
+\r
+        Package pkg = archive.getParentPackage();\r
+        if (pkg != null) {\r
+            pkg.saveProperties(props);\r
+        }\r
+\r
+        FileOutputStream fos = null;\r
+        try {\r
+            File f = new File(unzipDestFolder, SdkConstants.FN_SOURCE_PROP);\r
+\r
+            fos = new FileOutputStream(f);\r
+\r
+            props.store( fos, "## Android Tool: Source of this archive.");  //$NON-NLS-1$\r
+\r
+            return true;\r
+        } catch (IOException e) {\r
+            e.printStackTrace();\r
+        } finally {\r
+            if (fos != null) {\r
+                try {\r
+                    fos.close();\r
+                } catch (IOException e) {\r
+                }\r
+            }\r
+        }\r
+\r
+        return false;\r
+    }\r
+\r
+    /**\r
+     * Sets the executable Unix permission (0777) on a file or folder.\r
+     * @param file The file to set permissions on.\r
+     * @throws IOException If an I/O error occurs\r
+     */\r
+    private void setExecutablePermission(File file) throws IOException {\r
+        Runtime.getRuntime().exec(new String[] {\r
+           "chmod", "777", file.getAbsolutePath()\r
+        });\r
+    }\r
+}\r
index a1c089b..76ff7ba 100755 (executable)
@@ -24,6 +24,7 @@ import com.android.sdklib.internal.avd.AvdManager;
 import com.android.sdklib.internal.repository.AddonPackage;\r
 import com.android.sdklib.internal.repository.AddonsListFetcher;\r
 import com.android.sdklib.internal.repository.Archive;\r
+import com.android.sdklib.internal.repository.ArchiveInstaller;\r
 import com.android.sdklib.internal.repository.ITask;\r
 import com.android.sdklib.internal.repository.ITaskFactory;\r
 import com.android.sdklib.internal.repository.ITaskMonitor;\r
@@ -404,7 +405,7 @@ class UpdaterData implements IUpdaterData {
         mTaskFactory.start("Installing Archives", new ITask() {\r
             public void run(ITaskMonitor monitor) {\r
 \r
-                final int progressPerArchive = 2 * Archive.NUM_MONITOR_INC;\r
+                final int progressPerArchive = 2 * ArchiveInstaller.NUM_MONITOR_INC;\r
                 monitor.setProgressMax(result.size() * progressPerArchive);\r
                 monitor.setDescription("Preparing to install archives");\r
 \r
@@ -457,7 +458,12 @@ class UpdaterData implements IUpdaterData {
                             }\r
                         }\r
 \r
-                        if (archive.install(mOsSdkRoot, forceHttp, mSdkManager, monitor)) {\r
+                        ArchiveInstaller installer = new ArchiveInstaller();\r
+                        if (installer.install(archive,\r
+                                              mOsSdkRoot,\r
+                                              forceHttp,\r
+                                              mSdkManager,\r
+                                              monitor)) {\r
                             // We installed this archive.\r
                             installedArchives.add(archive);\r
                             numInstalled++;\r