SDK Updater: ask for confirmation before wiping modified samples.
SDK Bug: 2401466
Change-Id: I09d596b44b3daf3a079c51f76db3f9f8d376a8b3
diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkConstants.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkConstants.java
index da67cc9..83ad9bd 100644
--- a/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkConstants.java
+++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkConstants.java
@@ -118,6 +118,8 @@
/** properties file for SDK Updater packages */
public final static String FN_SOURCE_PROP = "source.properties"; //$NON-NLS-1$
+ /** properties file for content hash of installed packages */
+ public final static String FN_CONTENT_HASH_PROP = "content_hash.properties"; //$NON-NLS-1$
/* Folder Names for Android Projects . */
diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonPackage.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonPackage.java
index 218ced0..4055cbc 100755
--- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonPackage.java
+++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonPackage.java
@@ -284,13 +284,16 @@
* Makes sure the base /add-ons folder exists before installing.
*/
@Override
- public void preInstallHook(String osSdkRoot, Archive archive) {
- super.preInstallHook(osSdkRoot, archive);
-
+ public boolean preInstallHook(Archive archive,
+ ITaskMonitor monitor,
+ String osSdkRoot,
+ File installFolder) {
File addonsRoot = new File(osSdkRoot, SdkConstants.FD_ADDONS);
if (!addonsRoot.isDirectory()) {
addonsRoot.mkdir();
}
+
+ return super.preInstallHook(archive, monitor, osSdkRoot, installFolder);
}
@Override
diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Archive.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Archive.java
index 01b52d1..285262d 100755
--- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Archive.java
+++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Archive.java
@@ -405,8 +405,7 @@
archiveFile = downloadFile(osSdkRoot, monitor, forceHttp);
if (archiveFile != null) {
- pkg.preInstallHook(osSdkRoot, this);
- // Unarchive will call the postInstallHook on completion.
+ // Unarchive calls the pre/postInstallHook methods.
if (unarchive(osSdkRoot, archiveFile, sdkManager, monitor)) {
monitor.setResult("Installed %1$s", name);
// Delete the temp archive if it exists, only on success
@@ -747,6 +746,11 @@
return false;
}
+ if (!pkg.preInstallHook(this, monitor, osSdkRoot, destFolder)) {
+ monitor.setResult("Skipping archive: %1$s", pkgName);
+ return false;
+ }
+
// Swap the old folder by the new one.
// We have 2 "folder rename" (aka moves) to do.
// They must both succeed in the right order.
diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Package.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Package.java
index 3dbfefc..657bfe6 100755
--- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Package.java
+++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Package.java
@@ -404,14 +404,27 @@
/**
* Hook called right before an archive is installed. The archive has already
- * been downloaded succesfully and will be installed in the directory specified by
- * {@link #getInstallFolder(String, String, SdkManager)} when this call returns.
+ * been downloaded successfully and will be installed in the directory specified by
+ * <var>installFolder</var> when this call returns.
+ * <p/>
+ * The hook lets the package decide if installation of this specific archive should
+ * be continue. The installer will still install the remaining packages if possible.
+ * <p/>
+ * The base implementation always return true.
*
- * @param osSdkRoot The OS path of the SDK root folder.
* @param archive The archive that will be installed
+ * @param monitor The {@link ITaskMonitor} to display errors.
+ * @param osSdkRoot The OS path of the SDK root folder.
+ * @param installFolder The folder where the archive will be installed. Note that this
+ * is <em>not</em> the folder where the archive was temporary
+ * unzipped. The installFolder, if it exists, contains the old
+ * archive that will soon be replaced by the new one.
+ * @return True if installing this archive shall continue, false if it should be skipped.
*/
- public void preInstallHook(String osSdkRoot, Archive archive) {
+ public boolean preInstallHook(Archive archive, ITaskMonitor monitor,
+ String osSdkRoot, File installFolder) {
// Nothing to do in base class.
+ return true;
}
/**
diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/PlatformPackage.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/PlatformPackage.java
index 3d13c93..4e0618d 100755
--- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/PlatformPackage.java
+++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/PlatformPackage.java
@@ -189,13 +189,16 @@
* Makes sure the base /platforms folder exists before installing.
*/
@Override
- public void preInstallHook(String osSdkRoot, Archive archive) {
- super.preInstallHook(osSdkRoot, archive);
-
+ public boolean preInstallHook(Archive archive,
+ ITaskMonitor monitor,
+ String osSdkRoot,
+ File installFolder) {
File platformsRoot = new File(osSdkRoot, SdkConstants.FD_PLATFORMS);
if (!platformsRoot.isDirectory()) {
platformsRoot.mkdir();
}
+
+ return super.preInstallHook(archive, monitor, osSdkRoot, installFolder);
}
@Override
diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SamplePackage.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SamplePackage.java
index afb91e1..0d215e8 100755
--- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SamplePackage.java
+++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SamplePackage.java
@@ -28,6 +28,12 @@
import org.w3c.dom.Node;
import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.Properties;
@@ -239,19 +245,6 @@
return folder;
}
- /**
- * Makes sure the base /samples folder exists before installing.
- */
- @Override
- public void preInstallHook(String osSdkRoot, Archive archive) {
- super.preInstallHook(osSdkRoot, archive);
-
- File samplesRoot = new File(osSdkRoot, SdkConstants.FD_SAMPLES);
- if (!samplesRoot.isDirectory()) {
- samplesRoot.mkdir();
- }
- }
-
@Override
public boolean sameItemAs(Package pkg) {
if (pkg instanceof SamplePackage) {
@@ -263,4 +256,214 @@
return false;
}
+
+ /**
+ * Makes sure the base /samples folder exists before installing.
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean preInstallHook(Archive archive,
+ ITaskMonitor monitor,
+ String osSdkRoot,
+ File installFolder) {
+ File samplesRoot = new File(osSdkRoot, SdkConstants.FD_SAMPLES);
+ if (!samplesRoot.isDirectory()) {
+ samplesRoot.mkdir();
+ }
+
+ if (installFolder != null && installFolder.isDirectory()) {
+ // Get the hash computed during the last installation
+ String storedHash = readContentHash(installFolder);
+ if (storedHash != null && storedHash.length() > 0) {
+
+ // Get the hash of the folder now
+ String currentHash = computeContentHash(installFolder);
+
+ if (!storedHash.equals(currentHash)) {
+ // The hashes differ. The content was modified.
+ // Ask the user if we should still wipe the old samples.
+
+ String pkgName = archive.getParentPackage().getShortDescription();
+
+ String msg = String.format(
+ "-= Warning ! =-\n" +
+ "You are about to replace the content of the folder:\n " +
+ " %1$s\n" +
+ "by the new package:\n" +
+ " %2$s.\n" +
+ "\n" +
+ "However it seems that the content of the existing samples " +
+ "has been modified since it was last installed. Are you sure " +
+ "you want to DELETE the existing samples? This cannot be undone.\n" +
+ "Please select YES to delete the existing sample and replace them " +
+ "by the new ones.\n" +
+ "Please select NO to skip this package. You can always install it later.",
+ installFolder.getAbsolutePath(),
+ pkgName);
+
+ // Returns true if we can wipe & replace.
+ return monitor.displayPrompt("SDK Manager: overwrite samples?", msg);
+ }
+ }
+ }
+
+ // The default is to allow installation
+ return super.preInstallHook(archive, monitor, osSdkRoot, installFolder);
+ }
+
+ /**
+ * Computes a hash of the installed content (in case of successful install.)
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ public void postInstallHook(Archive archive, ITaskMonitor monitor, File installFolder) {
+ super.postInstallHook(archive, monitor, installFolder);
+
+ if (installFolder == null) {
+ return;
+ }
+
+ String h = computeContentHash(installFolder);
+ saveContentHash(installFolder, h);
+ }
+
+ /**
+ * Reads the hash from the properties file, if it exists.
+ * Returns null if something goes wrong, e.g. there's no property file or
+ * it doesn't contain our hash. Returns an empty string if the hash wasn't
+ * correctly computed last time by {@link #saveContentHash(File, String)}.
+ */
+ private String readContentHash(File folder) {
+ Properties props = new Properties();
+
+ FileInputStream fis = null;
+ try {
+ File f = new File(folder, SdkConstants.FN_CONTENT_HASH_PROP);
+ if (f.isFile()) {
+ fis = new FileInputStream(f);
+ props.load(fis);
+ return props.getProperty("content-hash", null); //$NON-NLS-1$
+ }
+ } catch (Exception e) {
+ // ignore
+ } finally {
+ if (fis != null) {
+ try {
+ fis.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Saves the hash using a properties file
+ */
+ private void saveContentHash(File folder, String hash) {
+ Properties props = new Properties();
+
+ props.setProperty("content-hash", hash == null ? "" : hash); //$NON-NLS-1$ //$NON-NLS-2$
+
+ FileOutputStream fos = null;
+ try {
+ File f = new File(folder, SdkConstants.FN_CONTENT_HASH_PROP);
+ fos = new FileOutputStream(f);
+ props.store( fos, "## Android - hash of this archive."); //$NON-NLS-1$
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ if (fos != null) {
+ try {
+ fos.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+ }
+
+ /**
+ * Computes a hash of the files names and sizes installed in the folder
+ * using the SHA-1 digest.
+ * Returns null if the digest algorithm is not available.
+ */
+ private String computeContentHash(File installFolder) {
+ MessageDigest md = null;
+ try {
+ // SHA-1 is a standard algorithm.
+ // http://java.sun.com/j2se/1.4.2/docs/guide/security/CryptoSpec.html#AppB
+ md = MessageDigest.getInstance("SHA-1"); //$NON-NLS-1$
+ } catch (NoSuchAlgorithmException e) {
+ // We're unlikely to get there unless this JVM is not spec conforming
+ // in which case there won't be any hash available.
+ }
+
+ if (md != null) {
+ hashDirectoryContent(installFolder, md);
+ return getDigestHexString(md);
+ }
+
+ return null;
+ }
+
+ /**
+ * Computes a hash of the *content* of this directory. The hash only uses
+ * the files names and the file sizes.
+ */
+ private void hashDirectoryContent(File folder, MessageDigest md) {
+ if (folder == null || md == null || !folder.isDirectory()) {
+ return;
+ }
+
+ for (File f : folder.listFiles()) {
+ if (f.isDirectory()) {
+ hashDirectoryContent(f, md);
+
+ } else {
+ String name = f.getName();
+
+ // Skip the file we use to store the content hash
+ if (name == null || SdkConstants.FN_CONTENT_HASH_PROP.equals(name)) {
+ continue;
+ }
+
+ try {
+ md.update(name.getBytes("UTF-8")); //$NON-NLS-1$
+ } catch (UnsupportedEncodingException e) {
+ // There is no valid reason for UTF-8 to be unsupported. Ignore.
+ }
+ try {
+ long len = f.length();
+ md.update((byte) (len & 0x0FF));
+ md.update((byte) ((len >> 8) & 0x0FF));
+ md.update((byte) ((len >> 16) & 0x0FF));
+ md.update((byte) ((len >> 24) & 0x0FF));
+
+ } catch (SecurityException e) {
+ // Might happen if file is not readable. Ignore.
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns a digest as an hex string.
+ */
+ private String getDigestHexString(MessageDigest digester) {
+ // Create an hex string from the digest
+ byte[] digest = digester.digest();
+ int n = digest.length;
+ String hex = "0123456789abcdef"; //$NON-NLS-1$
+ char[] hexDigest = new char[n * 2];
+ for (int i = 0; i < n; i++) {
+ int b = digest[i] & 0x0FF;
+ hexDigest[i*2 + 0] = hex.charAt(b >>> 4);
+ hexDigest[i*2 + 1] = hex.charAt(b & 0x0f);
+ }
+
+ return new String(hexDigest);
+ }
}