diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ITaskMonitor.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ITaskMonitor.java index 05c982d62..c8f7c06a6 100755 --- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ITaskMonitor.java +++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ITaskMonitor.java @@ -54,10 +54,16 @@ public interface ITaskMonitor { */ public void incProgress(int delta); + /** + * Returns the current value of the progress bar, + * between 0 and up to {@link #setProgressMax(int)} - 1. + */ + public int getProgress(); + /** * Returns true if the user requested to cancel the operation. * It is up to the task thread to pool this and exit as soon * as possible. */ - public boolean cancelRequested(); + public boolean isCancelRequested(); } diff --git a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/LocalPackagesPage.java b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/LocalPackagesPage.java index 6e9cef8ac..abc729ead 100755 --- a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/LocalPackagesPage.java +++ b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/LocalPackagesPage.java @@ -232,7 +232,7 @@ public class LocalPackagesPage extends Composite { monitor.setProgressMax(100); int n = 0; int d = 1; - while(!monitor.cancelRequested()) { + while(!monitor.isCancelRequested()) { monitor.incProgress(d); n += d; if (n == 0 || n == 100) d = -d; diff --git a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/ProgressDialog.java b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/ProgressDialog.java index b1f5da786..a5cb86fb3 100755 --- a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/ProgressDialog.java +++ b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/ProgressDialog.java @@ -148,7 +148,7 @@ final class ProgressDialog extends Dialog { }); mResultText = new Text(mRootComposite, - SWT.BORDER | SWT.READ_ONLY | SWT.V_SCROLL | SWT.MULTI); + SWT.BORDER | SWT.READ_ONLY | SWT.WRAP | SWT.H_SCROLL | SWT.V_SCROLL | SWT.CANCEL | SWT.MULTI); mResultText.setEditable(true); mResultText.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1)); } @@ -286,7 +286,15 @@ final class ProgressDialog extends Dialog { public void run() { if (!mResultText.isDisposed()) { mResultText.setVisible(true); - mResultText.setText(String.format(resultFormat, args)); + String newText = String.format(resultFormat, args); + String lastText = mResultText.getText(); + if (lastText != null && + lastText.length() > 0 && + !lastText.endsWith("\n") && + !newText.startsWith("\n")) { + mResultText.append("\n"); + } + mResultText.append(newText); } } }); @@ -327,6 +335,27 @@ final class ProgressDialog extends Dialog { } } + /** + * Returns the current value of the progress bar, + * between 0 and up to {@link #setProgressMax(int)} - 1. + * This method can be invoked from a non-UI thread. + */ + public int getProgress() { + final int[] result = new int[] { 0 }; + + if (!mDialogShell.isDisposed()) { + mDialogShell.getDisplay().syncExec(new Runnable() { + public void run() { + if (!mProgressBar.isDisposed()) { + result[0] = mProgressBar.getSelection(); + } + } + }); + } + + return result[0]; + } + /** * Starts the thread that runs the task. * This is deferred till the UI is created. diff --git a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/ProgressTask.java b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/ProgressTask.java index b2599efaa..709cce04c 100755 --- a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/ProgressTask.java +++ b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/ProgressTask.java @@ -80,11 +80,21 @@ class ProgressTask implements ITaskMonitor { mDialog.incProgress(delta); } + /** + * Returns the current value of the progress bar, + * between 0 and up to {@link #setProgressMax(int)} - 1. + * + * This method can be invoked from a non-UI thread. + */ + public int getProgress() { + return mDialog.getProgress(); + } + /** * Returns true if the "Cancel" button was selected. * It is up to the task thread to pool this and exit. */ - public boolean cancelRequested() { + public boolean isCancelRequested() { return mDialog.isCancelRequested(); } diff --git a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterWindowImpl.java b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterWindowImpl.java index a7223b51b..ddf53f04a 100755 --- a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterWindowImpl.java +++ b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterWindowImpl.java @@ -43,6 +43,7 @@ import org.eclipse.swt.widgets.List; import org.eclipse.swt.widgets.Shell; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -51,13 +52,15 @@ import java.net.URL; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Collection; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; /** * This is the private implementation of the UpdateWindow. */ public class UpdaterWindowImpl { - private static final int NUM_FETCH_URL_MONITOR_INC = 100; + private static final int NUM_MONITOR_INC = 100; /** Internal data shared between the window and its pages. */ private final UpdaterData mUpdaterData = new UpdaterData(); @@ -338,49 +341,66 @@ public class UpdaterWindowImpl { * @param archives The archives to install. Incompatible ones will be skipped. */ public void installArchives(final Collection archives) { + + // TODO filter the archive list to: a/ display a list of what is going to be installed, + // b/ display licenses and c/ check that the selected packages are actually upgrades + // or ask user to confirm downgrades. All this should be done in a separate class+window + // which will then call this method with the final list. + // TODO move most parts to SdkLib, maybe as part of Archive, making archives self-installing. mTaskFactory.start("Installing Archives", new ITask() { public void run(ITaskMonitor monitor) { - monitor.setProgressMax(archives.size() * (NUM_FETCH_URL_MONITOR_INC + 10)); + final int progressPerArchive = 2 * NUM_MONITOR_INC + 10; + monitor.setProgressMax(archives.size() * progressPerArchive); monitor.setDescription("Preparing to install archives"); - int num_installed = 0; + int numInstalled = 0; for (Archive archive : archives) { - if (!archive.isCompatible()) { - monitor.setResult("Skipping incompatible archive: %1$s", - archive.getParentPackage().getShortDescription()); - monitor.incProgress(NUM_FETCH_URL_MONITOR_INC + 10); - continue; - } - + int nextProgress = monitor.getProgress() + progressPerArchive; File archiveFile = null; try { + if (monitor.isCancelRequested()) { + break; + } + + String name = archive.getParentPackage().getShortDescription(); + + // TODO: we should not see this test fail if we had the filter UI above. + if (!archive.isCompatible()) { + monitor.setResult("Skipping incompatible archive: %1$s", name); + continue; + } + archiveFile = downloadArchive(archive, monitor); if (archiveFile != null) { if (installArchive(archive, archiveFile, monitor)) { - monitor.setResult("Installed: %1$s", - archive.getParentPackage().getShortDescription()); - num_installed++; + monitor.setResult("Installed: %1$s", name); + numInstalled++; } } - monitor.incProgress(10); } catch (Throwable t) { // Display anything unexpected in the monitor. monitor.setResult("Unexpected Error: %1$s", t.getMessage()); } finally { - if (archiveFile != null) { - if (!archiveFile.delete()) { - archiveFile.deleteOnExit(); - } - } + // Delete the temp archive if it exists + deleteFileOrFolder(archiveFile); + + // Always move the progress bar to the desired position. + // This allows internal methods to not have to care in case + // they abort early + monitor.incProgress(nextProgress - monitor.getProgress()); } } - if (num_installed == 0) { - monitor.setResult("Nothing was installed."); + if (numInstalled == 0) { + monitor.setDescription("Done. Nothing was installed."); + } else { + monitor.setDescription("Done. %1$d %2$s installed.", + numInstalled, + numInstalled == 1 ? "package" : "packages"); } } }); @@ -397,8 +417,9 @@ public class UpdaterWindowImpl { File tmpFile = File.createTempFile("sdkupload", ".bin"); //$NON-NLS-1$ //$NON-NLS-2$ tmpFileToDelete = tmpFile; - monitor.setDescription("Downloading %1$s", - archive.getParentPackage().getShortDescription()); + String name = archive.getParentPackage().getShortDescription(); + String desc = String.format("Downloading %1$s", name); + monitor.setDescription(desc); String link = archive.getUrl(); if (!link.startsWith("http://") //$NON-NLS-1$ @@ -408,8 +429,7 @@ public class UpdaterWindowImpl { Package pkg = archive.getParentPackage(); RepoSource src = pkg.getParentSource(); if (src == null) { - monitor.setResult("Internal error: no source for archive %1$s", - archive.getShortDescription()); + monitor.setResult("Internal error: no source for archive %1$s", name); return null; } @@ -422,7 +442,7 @@ public class UpdaterWindowImpl { link = base + link; } - if (fetchUrl(tmpFile, archive, link, monitor)) { + if (fetchUrl(tmpFile, archive, link, desc, monitor)) { // Fetching was successful, don't delete the temp file here! tmpFileToDelete = null; return tmpFile; @@ -432,11 +452,7 @@ public class UpdaterWindowImpl { monitor.setResult(e.getMessage()); } finally { - if (tmpFileToDelete != null) { - if (!tmpFileToDelete.delete()) { - tmpFileToDelete.deleteOnExit(); - } - } + deleteFileOrFolder(tmpFileToDelete); } return null; } @@ -448,11 +464,17 @@ public class UpdaterWindowImpl { * Success is defined as downloading as many bytes as was expected and having the same * SHA1 as expected. Returns true on success or false if any of those checks fail. *

- * Increments the monitor by {@link #NUM_FETCH_URL_MONITOR_INC} (which is 10). + * Increments the monitor by {@link #NUM_MONITOR_INC}. */ - private boolean fetchUrl(File tmpFile, Archive archive, String urlString, ITaskMonitor monitor) { + private boolean fetchUrl(File tmpFile, + Archive archive, + String urlString, + String description, + ITaskMonitor monitor) { URL url; + description += " (%1$d%%, %2$.0f KiB/s, %3$d %4$s left)"; + FileOutputStream os = null; InputStream is = null; try { @@ -467,22 +489,49 @@ public class UpdaterWindowImpl { long total = 0; long size = archive.getSize(); - long inc = size / NUM_FETCH_URL_MONITOR_INC; + long inc = size / NUM_MONITOR_INC; long next_inc = inc; + long startMs = System.currentTimeMillis(); + long nextMs = startMs + 2000; // start update after 2 seconds + while ((n = is.read(buf)) >= 0) { if (n > 0) { os.write(buf, 0, n); digester.update(buf, 0, n); } + long timeMs = System.currentTimeMillis(); + total += n; if (total >= next_inc) { monitor.incProgress(1); next_inc += inc; } - if (monitor.cancelRequested()) { + if (timeMs > nextMs) { + long delta = timeMs - startMs; + if (total > 0 && delta > 0) { + // percent left to download + int percent = (int) (100 * total / size); + // speed in KiB/s + float speed = (float)total / (float)delta * (1000.f / 1024.f); + // time left to download the rest at the current KiB/s rate + int timeLeft = (speed > 1e-3) ? + (int)(((size - total) / 1024.0f) / speed) : + 0; + String timeUnit = "seconds"; + if (timeLeft > 120) { + timeUnit = "minutes"; + timeLeft /= 60; + } + + monitor.setDescription(description, percent, speed, timeLeft, timeUnit); + } + nextMs = timeMs + 1000; // update every second + } + + if (monitor.isCancelRequested()) { monitor.setResult("Download aborted by user at %1$d bytes.", total); return false; } @@ -540,14 +589,234 @@ public class UpdaterWindowImpl { return false; } + /** + * Install the given archive in the given folder. + */ private boolean installArchive(Archive archive, File archiveFile, ITaskMonitor monitor) { - monitor.setDescription("Installing %1$s", archive.getShortDescription()); + String name = archive.getParentPackage().getShortDescription(); + String desc = String.format("Installing %1$s", name); + monitor.setDescription(desc); File destFolder = archive.getParentPackage().getInstallFolder(mUpdaterData.getOsSdkRoot()); + File unzipDestFolder = destFolder; + File renamedDestFolder = null; + + try { + // If this folder already exists, unzip in a temporary folder and then move/unlink. + if (destFolder.exists()) { + // Find a new temp folder that doesn't exist yet + unzipDestFolder = findTempFolder(destFolder, "new"); //$NON-NLS-1$ + + if (unzipDestFolder == null) { + // this should not seriously happen. + monitor.setResult("Failed to find a suitable temp directory similar to %1$s.", + destFolder.getPath()); + return false; + } + + if (!unzipDestFolder.mkdirs()) { + monitor.setResult("Failed to create temp directory %1$s", + unzipDestFolder.getPath()); + return false; + } + } + + if (!unzipFolder(archiveFile, archive.getSize(), unzipDestFolder, desc, monitor)) { + return false; + } + + if (unzipDestFolder != destFolder) { + // Swap the old folder by the new one. + // Both original folders will be deleted in the finally clause below. + renamedDestFolder = findTempFolder(destFolder, "old"); //$NON-NLS-1$ + if (renamedDestFolder == null) { + // this should not seriously happen. + monitor.setResult("Failed to find a suitable temp directory similar to %1$s.", + destFolder.getPath()); + return false; + } + + if (!destFolder.renameTo(renamedDestFolder)) { + monitor.setResult("Failed to rename directory %1$s to %2$s", + destFolder.getPath(), renamedDestFolder.getPath()); + return false; + + } + if (!unzipDestFolder.renameTo(destFolder)) { + monitor.setResult("Failed to rename directory %1$s to %2$s", + unzipDestFolder.getPath(), destFolder.getPath()); + return false; + } + } + + return true; + + } finally { + // Cleanup if the unzip folder is still set. + deleteFileOrFolder(renamedDestFolder); + if (unzipDestFolder != destFolder) { + deleteFileOrFolder(unzipDestFolder); + } + } + } + + private boolean unzipFolder(File archiveFile, + long compressedSize, + File unzipDestFolder, + String description, + ITaskMonitor monitor) { + + description += " (%1$d%%)"; + + FileInputStream fis = null; + ZipInputStream zis = null; + try { + fis = new FileInputStream(archiveFile); + zis = new ZipInputStream(fis); + + // To advance the percent and the progress bar, we don't know the number of + // items left to unzip. However we know the size of the archive and the size of + // each uncompressed item. The zip file format overhead is negligible so that's + // a good approximation. + long incStep = compressedSize / NUM_MONITOR_INC; + long incTotal = 0; + long incCurr = 0; + int lastPercent = 0; + + byte[] buf = new byte[65536]; + + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + + String name = entry.getName(); + + // ZipFile entries should have forward slashes, but not all Zip + // implementations can be expected to do that. + name = name.replace('\\', '/'); + + File destFile = new File(unzipDestFolder, name); + + if (name.endsWith("/")) { //$NON-NLS-1$ + // Create directory if it doesn't exist yet. This allows us to create + // empty directories. + if (!destFile.isDirectory() && !destFile.mkdirs()) { + monitor.setResult("Failed to create temp directory %1$s", + destFile.getPath()); + return false; + } + continue; + } else if (name.indexOf('/') != -1) { + // Otherwise it's a file in a sub-directory. + // Make sure the parent directory has been created. + File parentDir = destFile.getParentFile(); + if (!parentDir.isDirectory()) { + if (!parentDir.mkdirs()) { + monitor.setResult("Failed to create temp directory %1$s", + parentDir.getPath()); + return false; + } + } + } + + FileOutputStream fos = null; + try { + fos = new FileOutputStream(destFile); + int n; + while ((n = zis.read(buf)) != -1) { + if (n > 0) { + fos.write(buf, 0, n); + } + } + } finally { + if (fos != null) { + fos.close(); + } + } + + // Increment progress bar to match. We update only between files. + for(incTotal += entry.getCompressedSize(); incCurr < incTotal; incCurr += incStep) { + monitor.incProgress(1); + } + + int percent = (int) (100 * incTotal / compressedSize); + if (percent != lastPercent) { + monitor.setDescription(description, percent); + lastPercent = percent; + } + + if (monitor.isCancelRequested()) { + return false; + } + } + + return true; + + } catch (IOException e) { + monitor.setResult("Unzip failed: %1$s", e.getMessage()); + + } finally { + if (zis != null) { + try { + zis.close(); + } catch (IOException e) { + // pass + } + } + if (fis != null) { + try { + fis.close(); + } catch (IOException e) { + // pass + } + } + } + return false; } + /** + * Finds a temp folder which name is similar to the one of the ideal folder + * and with a ".tmpN" appended. + *

+ * This operation is not atomic so there's no guarantee the folder can't get + * created in between. This is however unlikely and the caller can assume the + * returned folder does not exist yet. + *

+ * Returns null if no such folder can be found (e.g. if all candidates exist), + * which is rather unlikely. + */ + private File findTempFolder(File idealFolder, String suffix) { + String basePath = idealFolder.getPath(); + + for (int i = 1; i < 100; i++) { + File folder = new File(String.format("%1$s.%2$s%3$02d", basePath, suffix, i)); //$NON-NLS-1$ + if (!folder.exists()) { + return folder; + } + } + return null; + } + + /** + * Deletes a file or a directory. + * Directories are deleted recursively. + * The argument can be null. + */ + private void deleteFileOrFolder(File fileOrFolder) { + if (fileOrFolder != null) { + if (fileOrFolder.isDirectory()) { + // Must delete content recursively first + for (File item : fileOrFolder.listFiles()) { + deleteFileOrFolder(item); + } + } + if (!fileOrFolder.delete()) { + fileOrFolder.deleteOnExit(); + } + } + } + // End of hiding from SWT Designer //$hide<<$ }