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);

+    }

 }