OSDN Git Service

original
[gb-231r1-is01/GB_2.3_IS01.git] / sdk / sdkmanager / libs / sdklib / src / com / android / sdklib / internal / repository / SamplePackage.java
diff --git a/sdk/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SamplePackage.java b/sdk/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SamplePackage.java
new file mode 100644 (file)
index 0000000..035677b
--- /dev/null
@@ -0,0 +1,474 @@
+/*\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.AndroidVersion;\r
+import com.android.sdklib.IAndroidTarget;\r
+import com.android.sdklib.SdkConstants;\r
+import com.android.sdklib.SdkManager;\r
+import com.android.sdklib.AndroidVersion.AndroidVersionException;\r
+import com.android.sdklib.internal.repository.Archive.Arch;\r
+import com.android.sdklib.internal.repository.Archive.Os;\r
+import com.android.sdklib.repository.SdkRepoConstants;\r
+\r
+import org.w3c.dom.Node;\r
+\r
+import java.io.File;\r
+import java.io.FileInputStream;\r
+import java.io.FileOutputStream;\r
+import java.io.IOException;\r
+import java.io.UnsupportedEncodingException;\r
+import java.security.MessageDigest;\r
+import java.security.NoSuchAlgorithmException;\r
+import java.util.Map;\r
+import java.util.Properties;\r
+\r
+/**\r
+ * Represents a sample XML node in an SDK repository.\r
+ */\r
+public class SamplePackage extends MinToolsPackage\r
+    implements IPackageVersion, IMinApiLevelDependency, IMinToolsDependency {\r
+\r
+    private static final String PROP_MIN_API_LEVEL = "Sample.MinApiLevel";  //$NON-NLS-1$\r
+\r
+    /** The matching platform version. */\r
+    private final AndroidVersion mVersion;\r
+\r
+    /**\r
+     * The minimal API level required by this extra package, if > 0,\r
+     * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement.\r
+     */\r
+    private final int mMinApiLevel;\r
+\r
+    /**\r
+     * Creates a new sample package from the attributes and elements of the given XML node.\r
+     * This constructor should throw an exception if the package cannot be created.\r
+     *\r
+     * @param source The {@link SdkSource} where this is loaded from.\r
+     * @param packageNode The XML element being parsed.\r
+     * @param nsUri The namespace URI of the originating XML document, to be able to deal with\r
+     *          parameters that vary according to the originating XML schema.\r
+     * @param licenses The licenses loaded from the XML originating document.\r
+     */\r
+    SamplePackage(SdkSource source, Node packageNode, String nsUri, Map<String,String> licenses) {\r
+        super(source, packageNode, nsUri, licenses);\r
+\r
+        int apiLevel = XmlParserUtils.getXmlInt   (packageNode, SdkRepoConstants.NODE_API_LEVEL, 0);\r
+        String codeName = XmlParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_CODENAME);\r
+        if (codeName.length() == 0) {\r
+            codeName = null;\r
+        }\r
+        mVersion = new AndroidVersion(apiLevel, codeName);\r
+\r
+        mMinApiLevel = XmlParserUtils.getXmlInt(packageNode, SdkRepoConstants.NODE_MIN_API_LEVEL,\r
+                MIN_API_LEVEL_NOT_SPECIFIED);\r
+    }\r
+\r
+    /**\r
+     * Creates a new sample package based on an actual {@link IAndroidTarget} (which\r
+     * must have {@link IAndroidTarget#isPlatform()} true) from the {@link SdkManager}.\r
+     * <p/>\r
+     * The target <em>must</em> have an existing sample directory that uses the /samples\r
+     * root form rather than the old form where the samples dir was located under the\r
+     * platform dir.\r
+     * <p/>\r
+     * This is used to list local SDK folders in which case there is one archive which\r
+     * URL is the actual samples path location.\r
+     * <p/>\r
+     * By design, this creates a package with one and only one archive.\r
+     */\r
+    static Package create(IAndroidTarget target, Properties props) {\r
+        return new SamplePackage(target, props);\r
+    }\r
+\r
+    private SamplePackage(IAndroidTarget target, Properties props) {\r
+        super(  null,                                   //source\r
+                props,                                  //properties\r
+                0,                                      //revision will be taken from props\r
+                null,                                   //license\r
+                null,                                   //description\r
+                null,                                   //descUrl\r
+                Os.ANY,                                 //archiveOs\r
+                Arch.ANY,                               //archiveArch\r
+                target.getPath(IAndroidTarget.SAMPLES)  //archiveOsPath\r
+                );\r
+\r
+        mVersion = target.getVersion();\r
+\r
+        mMinApiLevel = Integer.parseInt(\r
+            getProperty(props, PROP_MIN_API_LEVEL, Integer.toString(MIN_API_LEVEL_NOT_SPECIFIED)));\r
+    }\r
+\r
+    /**\r
+     * Creates a new sample package from an actual directory path and previously\r
+     * saved properties.\r
+     * <p/>\r
+     * This is used to list local SDK folders in which case there is one archive which\r
+     * URL is the actual samples path location.\r
+     * <p/>\r
+     * By design, this creates a package with one and only one archive.\r
+     *\r
+     * @throws AndroidVersionException if the {@link AndroidVersion} can't be restored\r
+     *                                 from properties.\r
+     */\r
+    static Package create(String archiveOsPath, Properties props) throws AndroidVersionException {\r
+        return new SamplePackage(archiveOsPath, props);\r
+    }\r
+\r
+    private SamplePackage(String archiveOsPath, Properties props) throws AndroidVersionException {\r
+        super(null,                                   //source\r
+              props,                                  //properties\r
+              0,                                      //revision will be taken from props\r
+              null,                                   //license\r
+              null,                                   //description\r
+              null,                                   //descUrl\r
+              Os.ANY,                                 //archiveOs\r
+              Arch.ANY,                               //archiveArch\r
+              archiveOsPath                           //archiveOsPath\r
+              );\r
+\r
+        mVersion = new AndroidVersion(props);\r
+\r
+        mMinApiLevel = Integer.parseInt(\r
+            getProperty(props, PROP_MIN_API_LEVEL, Integer.toString(MIN_API_LEVEL_NOT_SPECIFIED)));\r
+    }\r
+\r
+    /**\r
+     * Save the properties of the current packages in the given {@link Properties} object.\r
+     * These properties will later be given to a constructor that takes a {@link Properties} object.\r
+     */\r
+    @Override\r
+    void saveProperties(Properties props) {\r
+        super.saveProperties(props);\r
+\r
+        mVersion.saveProperties(props);\r
+\r
+        if (getMinApiLevel() != MIN_API_LEVEL_NOT_SPECIFIED) {\r
+            props.setProperty(PROP_MIN_API_LEVEL, Integer.toString(getMinApiLevel()));\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Returns the minimal API level required by this extra package, if > 0,\r
+     * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement.\r
+     */\r
+    public int getMinApiLevel() {\r
+        return mMinApiLevel;\r
+    }\r
+\r
+    /** Returns the matching platform version. */\r
+    public AndroidVersion getVersion() {\r
+        return mVersion;\r
+    }\r
+\r
+    /** Returns a short description for an {@link IDescription}. */\r
+    @Override\r
+    public String getShortDescription() {\r
+        String s = String.format("Samples for SDK API %1$s%2$s, revision %3$d%4$s",\r
+                mVersion.getApiString(),\r
+                mVersion.isPreview() ? " Preview" : "",\r
+                getRevision(),\r
+                isObsolete() ? " (Obsolete)" : "");\r
+        return s;\r
+    }\r
+\r
+    /**\r
+     * Returns a long description for an {@link IDescription}.\r
+     *\r
+     * The long description is whatever the XML contains for the &lt;description&gt; field,\r
+     * or the short description if the former is empty.\r
+     */\r
+    @Override\r
+    public String getLongDescription() {\r
+        String s = getDescription();\r
+        if (s == null || s.length() == 0) {\r
+            s = getShortDescription();\r
+        }\r
+\r
+        if (s.indexOf("revision") == -1) {\r
+            s += String.format("\nRevision %1$d%2$s",\r
+                    getRevision(),\r
+                    isObsolete() ? " (Obsolete)" : "");\r
+        }\r
+\r
+        return s;\r
+    }\r
+\r
+    /**\r
+     * Computes a potential installation folder if an archive of this package were\r
+     * to be installed right away in the given SDK root.\r
+     * <p/>\r
+     * A sample package is typically installed in SDK/samples/android-"version".\r
+     * However if we can find a different directory that already has this sample\r
+     * version installed, we'll use that one.\r
+     *\r
+     * @param osSdkRoot The OS path of the SDK root folder.\r
+     * @param sdkManager An existing SDK manager to list current platforms and addons.\r
+     * @return A new {@link File} corresponding to the directory to use to install this package.\r
+     */\r
+    @Override\r
+    public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) {\r
+\r
+        // The /samples dir at the root of the SDK\r
+        File samplesRoot = new File(osSdkRoot, SdkConstants.FD_SAMPLES);\r
+\r
+        // First find if this sample is already installed. If so, reuse the same directory.\r
+        for (IAndroidTarget target : sdkManager.getTargets()) {\r
+            if (target.isPlatform() &&\r
+                    target.getVersion().equals(mVersion)) {\r
+                String p = target.getPath(IAndroidTarget.SAMPLES);\r
+                File f = new File(p);\r
+                if (f.isDirectory()) {\r
+                    // We *only* use this directory if it's using the "new" location\r
+                    // under SDK/samples. We explicitly do not reuse the "old" location\r
+                    // under SDK/platform/android-N/samples.\r
+                    if (f.getParentFile().equals(samplesRoot)) {\r
+                        return f;\r
+                    }\r
+                }\r
+            }\r
+        }\r
+\r
+        // Otherwise, get a suitable default\r
+        File folder = new File(samplesRoot,\r
+                String.format("android-%s", getVersion().getApiString())); //$NON-NLS-1$\r
+\r
+        for (int n = 1; folder.exists(); n++) {\r
+            // Keep trying till we find an unused directory.\r
+            folder = new File(samplesRoot,\r
+                    String.format("android-%s_%d", getVersion().getApiString(), n)); //$NON-NLS-1$\r
+        }\r
+\r
+        return folder;\r
+    }\r
+\r
+    @Override\r
+    public boolean sameItemAs(Package pkg) {\r
+        if (pkg instanceof SamplePackage) {\r
+            SamplePackage newPkg = (SamplePackage)pkg;\r
+\r
+            // check they are the same platform.\r
+            return newPkg.getVersion().equals(this.getVersion());\r
+        }\r
+\r
+        return false;\r
+    }\r
+\r
+    /**\r
+     * Makes sure the base /samples folder exists before installing.\r
+     *\r
+     * {@inheritDoc}\r
+     */\r
+    @Override\r
+    public boolean preInstallHook(Archive archive,\r
+            ITaskMonitor monitor,\r
+            String osSdkRoot,\r
+            File installFolder) {\r
+\r
+        if (installFolder != null && installFolder.isDirectory()) {\r
+            // Get the hash computed during the last installation\r
+            String storedHash = readContentHash(installFolder);\r
+            if (storedHash != null && storedHash.length() > 0) {\r
+\r
+                // Get the hash of the folder now\r
+                String currentHash = computeContentHash(installFolder);\r
+\r
+                if (!storedHash.equals(currentHash)) {\r
+                    // The hashes differ. The content was modified.\r
+                    // Ask the user if we should still wipe the old samples.\r
+\r
+                    String pkgName = archive.getParentPackage().getShortDescription();\r
+\r
+                    String msg = String.format(\r
+                            "-= Warning ! =-\n" +\r
+                            "You are about to replace the content of the folder:\n " +\r
+                            "  %1$s\n" +\r
+                            "by the new package:\n" +\r
+                            "  %2$s.\n" +\r
+                            "\n" +\r
+                            "However it seems that the content of the existing samples " +\r
+                            "has been modified since it was last installed. Are you sure " +\r
+                            "you want to DELETE the existing samples? This cannot be undone.\n" +\r
+                            "Please select YES to delete the existing sample and replace them " +\r
+                            "by the new ones.\n" +\r
+                            "Please select NO to skip this package. You can always install it later.",\r
+                            installFolder.getAbsolutePath(),\r
+                            pkgName);\r
+\r
+                    // Returns true if we can wipe & replace.\r
+                    return monitor.displayPrompt("SDK Manager: overwrite samples?", msg);\r
+                }\r
+            }\r
+        }\r
+\r
+        // The default is to allow installation\r
+        return super.preInstallHook(archive, monitor, osSdkRoot, installFolder);\r
+    }\r
+\r
+    /**\r
+     * Computes a hash of the installed content (in case of successful install.)\r
+     *\r
+     * {@inheritDoc}\r
+     */\r
+    @Override\r
+    public void postInstallHook(Archive archive, ITaskMonitor monitor, File installFolder) {\r
+        super.postInstallHook(archive, monitor, installFolder);\r
+\r
+        if (installFolder != null) {\r
+            String h = computeContentHash(installFolder);\r
+            saveContentHash(installFolder, h);\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Reads the hash from the properties file, if it exists.\r
+     * Returns null if something goes wrong, e.g. there's no property file or\r
+     * it doesn't contain our hash. Returns an empty string if the hash wasn't\r
+     * correctly computed last time by {@link #saveContentHash(File, String)}.\r
+     */\r
+    private String readContentHash(File folder) {\r
+        Properties props = new Properties();\r
+\r
+        FileInputStream fis = null;\r
+        try {\r
+            File f = new File(folder, SdkConstants.FN_CONTENT_HASH_PROP);\r
+            if (f.isFile()) {\r
+                fis = new FileInputStream(f);\r
+                props.load(fis);\r
+                return props.getProperty("content-hash", null);  //$NON-NLS-1$\r
+            }\r
+        } catch (Exception e) {\r
+            // ignore\r
+        } finally {\r
+            if (fis != null) {\r
+                try {\r
+                    fis.close();\r
+                } catch (IOException e) {\r
+                }\r
+            }\r
+        }\r
+\r
+        return null;\r
+    }\r
+\r
+    /**\r
+     * Saves the hash using a properties file\r
+     */\r
+    private void saveContentHash(File folder, String hash) {\r
+        Properties props = new Properties();\r
+\r
+        props.setProperty("content-hash", hash == null ? "" : hash);  //$NON-NLS-1$ //$NON-NLS-2$\r
+\r
+        FileOutputStream fos = null;\r
+        try {\r
+            File f = new File(folder, SdkConstants.FN_CONTENT_HASH_PROP);\r
+            fos = new FileOutputStream(f);\r
+            props.store( fos, "## Android - hash of this archive.");  //$NON-NLS-1$\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
+\r
+    /**\r
+     * Computes a hash of the files names and sizes installed in the folder\r
+     * using the SHA-1 digest.\r
+     * Returns null if the digest algorithm is not available.\r
+     */\r
+    private String computeContentHash(File installFolder) {\r
+        MessageDigest md = null;\r
+        try {\r
+            // SHA-1 is a standard algorithm.\r
+            // http://java.sun.com/j2se/1.4.2/docs/guide/security/CryptoSpec.html#AppB\r
+            md = MessageDigest.getInstance("SHA-1");    //$NON-NLS-1$\r
+        } catch (NoSuchAlgorithmException e) {\r
+            // We're unlikely to get there unless this JVM is not spec conforming\r
+            // in which case there won't be any hash available.\r
+        }\r
+\r
+        if (md != null) {\r
+            hashDirectoryContent(installFolder, md);\r
+            return getDigestHexString(md);\r
+        }\r
+\r
+        return null;\r
+    }\r
+\r
+    /**\r
+     * Computes a hash of the *content* of this directory. The hash only uses\r
+     * the files names and the file sizes.\r
+     */\r
+    private void hashDirectoryContent(File folder, MessageDigest md) {\r
+        if (folder == null || md == null || !folder.isDirectory()) {\r
+            return;\r
+        }\r
+\r
+        for (File f : folder.listFiles()) {\r
+            if (f.isDirectory()) {\r
+                hashDirectoryContent(f, md);\r
+\r
+            } else {\r
+                String name = f.getName();\r
+\r
+                // Skip the file we use to store the content hash\r
+                if (name == null || SdkConstants.FN_CONTENT_HASH_PROP.equals(name)) {\r
+                    continue;\r
+                }\r
+\r
+                try {\r
+                    md.update(name.getBytes("UTF-8"));   //$NON-NLS-1$\r
+                } catch (UnsupportedEncodingException e) {\r
+                    // There is no valid reason for UTF-8 to be unsupported. Ignore.\r
+                }\r
+                try {\r
+                    long len = f.length();\r
+                    md.update((byte) (len & 0x0FF));\r
+                    md.update((byte) ((len >> 8) & 0x0FF));\r
+                    md.update((byte) ((len >> 16) & 0x0FF));\r
+                    md.update((byte) ((len >> 24) & 0x0FF));\r
+\r
+                } catch (SecurityException e) {\r
+                    // Might happen if file is not readable. Ignore.\r
+                }\r
+            }\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Returns a digest as an hex string.\r
+     */\r
+    private String getDigestHexString(MessageDigest digester) {\r
+        // Create an hex string from the digest\r
+        byte[] digest = digester.digest();\r
+        int 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