\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
* 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
*/\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
--- /dev/null
+/*\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
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
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
}\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