--- /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.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 <description> 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