1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.sdklib.internal.repository; 18 19 import com.android.sdklib.AndroidVersion; 20 import com.android.sdklib.IAndroidTarget; 21 import com.android.sdklib.SdkConstants; 22 import com.android.sdklib.SdkManager; 23 import com.android.sdklib.AndroidVersion.AndroidVersionException; 24 import com.android.sdklib.internal.repository.Archive.Arch; 25 import com.android.sdklib.internal.repository.Archive.Os; 26 import com.android.sdklib.repository.PkgProps; 27 import com.android.sdklib.repository.SdkRepoConstants; 28 29 import org.w3c.dom.Node; 30 31 import java.io.File; 32 import java.io.FileInputStream; 33 import java.io.FileOutputStream; 34 import java.io.IOException; 35 import java.io.UnsupportedEncodingException; 36 import java.security.MessageDigest; 37 import java.security.NoSuchAlgorithmException; 38 import java.util.Map; 39 import java.util.Properties; 40 41 /** 42 * Represents a sample XML node in an SDK repository. 43 */ 44 public class SamplePackage extends MinToolsPackage 45 implements IPackageVersion, IMinApiLevelDependency { 46 47 /** The matching platform version. */ 48 private final AndroidVersion mVersion; 49 50 /** 51 * The minimal API level required by this extra package, if > 0, 52 * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement. 53 */ 54 private final int mMinApiLevel; 55 56 /** 57 * Creates a new sample package from the attributes and elements of the given XML node. 58 * This constructor should throw an exception if the package cannot be created. 59 * 60 * @param source The {@link SdkSource} where this is loaded from. 61 * @param packageNode The XML element being parsed. 62 * @param nsUri The namespace URI of the originating XML document, to be able to deal with 63 * parameters that vary according to the originating XML schema. 64 * @param licenses The licenses loaded from the XML originating document. 65 */ 66 SamplePackage(SdkSource source, Node packageNode, String nsUri, Map<String,String> licenses) { 67 super(source, packageNode, nsUri, licenses); 68 69 int apiLevel = XmlParserUtils.getXmlInt (packageNode, SdkRepoConstants.NODE_API_LEVEL, 0); 70 String codeName = XmlParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_CODENAME); 71 if (codeName.length() == 0) { 72 codeName = null; 73 } 74 mVersion = new AndroidVersion(apiLevel, codeName); 75 76 mMinApiLevel = XmlParserUtils.getXmlInt(packageNode, SdkRepoConstants.NODE_MIN_API_LEVEL, 77 MIN_API_LEVEL_NOT_SPECIFIED); 78 } 79 80 /** 81 * Creates a new sample package based on an actual {@link IAndroidTarget} (which 82 * must have {@link IAndroidTarget#isPlatform()} true) from the {@link SdkManager}. 83 * <p/> 84 * The target <em>must</em> have an existing sample directory that uses the /samples 85 * root form rather than the old form where the samples dir was located under the 86 * platform dir. 87 * <p/> 88 * This is used to list local SDK folders in which case there is one archive which 89 * URL is the actual samples path location. 90 * <p/> 91 * By design, this creates a package with one and only one archive. 92 */ 93 static Package create(IAndroidTarget target, Properties props) { 94 return new SamplePackage(target, props); 95 } 96 97 private SamplePackage(IAndroidTarget target, Properties props) { 98 super( null, //source 99 props, //properties 100 0, //revision will be taken from props 101 null, //license 102 null, //description 103 null, //descUrl 104 Os.ANY, //archiveOs 105 Arch.ANY, //archiveArch 106 target.getPath(IAndroidTarget.SAMPLES) //archiveOsPath 107 ); 108 109 mVersion = target.getVersion(); 110 111 mMinApiLevel = Integer.parseInt( 112 getProperty(props, 113 PkgProps.SAMPLE_MIN_API_LEVEL, 114 Integer.toString(MIN_API_LEVEL_NOT_SPECIFIED))); 115 } 116 117 /** 118 * Creates a new sample package from an actual directory path and previously 119 * saved properties. 120 * <p/> 121 * This is used to list local SDK folders in which case there is one archive which 122 * URL is the actual samples path location. 123 * <p/> 124 * By design, this creates a package with one and only one archive. 125 * 126 * @throws AndroidVersionException if the {@link AndroidVersion} can't be restored 127 * from properties. 128 */ 129 static Package create(String archiveOsPath, Properties props) throws AndroidVersionException { 130 return new SamplePackage(archiveOsPath, props); 131 } 132 133 private SamplePackage(String archiveOsPath, Properties props) throws AndroidVersionException { 134 super(null, //source 135 props, //properties 136 0, //revision will be taken from props 137 null, //license 138 null, //description 139 null, //descUrl 140 Os.ANY, //archiveOs 141 Arch.ANY, //archiveArch 142 archiveOsPath //archiveOsPath 143 ); 144 145 mVersion = new AndroidVersion(props); 146 147 mMinApiLevel = Integer.parseInt( 148 getProperty(props, 149 PkgProps.SAMPLE_MIN_API_LEVEL, 150 Integer.toString(MIN_API_LEVEL_NOT_SPECIFIED))); 151 } 152 153 /** 154 * Save the properties of the current packages in the given {@link Properties} object. 155 * These properties will later be given to a constructor that takes a {@link Properties} object. 156 */ 157 @Override 158 void saveProperties(Properties props) { 159 super.saveProperties(props); 160 161 mVersion.saveProperties(props); 162 163 if (getMinApiLevel() != MIN_API_LEVEL_NOT_SPECIFIED) { 164 props.setProperty(PkgProps.SAMPLE_MIN_API_LEVEL, Integer.toString(getMinApiLevel())); 165 } 166 } 167 168 /** 169 * Returns the minimal API level required by this extra package, if > 0, 170 * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement. 171 */ 172 public int getMinApiLevel() { 173 return mMinApiLevel; 174 } 175 176 /** Returns the matching platform version. */ 177 public AndroidVersion getVersion() { 178 return mVersion; 179 } 180 181 /** 182 * Returns a string identifier to install this package from the command line. 183 * For samples, we use "sample-N" where N is the API or the preview codename. 184 * <p/> 185 * {@inheritDoc} 186 */ 187 @Override 188 public String installId() { 189 return "sample-" + mVersion.getApiString(); //$NON-NLS-1$ 190 } 191 192 /** 193 * Returns a description of this package that is suitable for a list display. 194 * <p/> 195 * {@inheritDoc} 196 */ 197 @Override 198 public String getListDescription() { 199 String s = String.format("Samples for SDK API %1$s%2$s%3$s", 200 mVersion.getApiString(), 201 mVersion.isPreview() ? " Preview" : "", 202 isObsolete() ? " (Obsolete)" : ""); 203 return s; 204 } 205 206 /** 207 * Returns a short description for an {@link IDescription}. 208 */ 209 @Override 210 public String getShortDescription() { 211 String s = String.format("Samples for SDK API %1$s%2$s, revision %3$d%4$s", 212 mVersion.getApiString(), 213 mVersion.isPreview() ? " Preview" : "", 214 getRevision(), 215 isObsolete() ? " (Obsolete)" : ""); 216 return s; 217 } 218 219 /** 220 * Returns a long description for an {@link IDescription}. 221 * 222 * The long description is whatever the XML contains for the <description> field, 223 * or the short description if the former is empty. 224 */ 225 @Override 226 public String getLongDescription() { 227 String s = getDescription(); 228 if (s == null || s.length() == 0) { 229 s = getShortDescription(); 230 } 231 232 if (s.indexOf("revision") == -1) { 233 s += String.format("\nRevision %1$d%2$s", 234 getRevision(), 235 isObsolete() ? " (Obsolete)" : ""); 236 } 237 238 return s; 239 } 240 241 /** 242 * Computes a potential installation folder if an archive of this package were 243 * to be installed right away in the given SDK root. 244 * <p/> 245 * A sample package is typically installed in SDK/samples/android-"version". 246 * However if we can find a different directory that already has this sample 247 * version installed, we'll use that one. 248 * 249 * @param osSdkRoot The OS path of the SDK root folder. 250 * @param sdkManager An existing SDK manager to list current platforms and addons. 251 * @return A new {@link File} corresponding to the directory to use to install this package. 252 */ 253 @Override 254 public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) { 255 256 // The /samples dir at the root of the SDK 257 File samplesRoot = new File(osSdkRoot, SdkConstants.FD_SAMPLES); 258 259 // First find if this sample is already installed. If so, reuse the same directory. 260 for (IAndroidTarget target : sdkManager.getTargets()) { 261 if (target.isPlatform() && 262 target.getVersion().equals(mVersion)) { 263 String p = target.getPath(IAndroidTarget.SAMPLES); 264 File f = new File(p); 265 if (f.isDirectory()) { 266 // We *only* use this directory if it's using the "new" location 267 // under SDK/samples. We explicitly do not reuse the "old" location 268 // under SDK/platform/android-N/samples. 269 if (f.getParentFile().equals(samplesRoot)) { 270 return f; 271 } 272 } 273 } 274 } 275 276 // Otherwise, get a suitable default 277 File folder = new File(samplesRoot, 278 String.format("android-%s", getVersion().getApiString())); //$NON-NLS-1$ 279 280 for (int n = 1; folder.exists(); n++) { 281 // Keep trying till we find an unused directory. 282 folder = new File(samplesRoot, 283 String.format("android-%s_%d", getVersion().getApiString(), n)); //$NON-NLS-1$ 284 } 285 286 return folder; 287 } 288 289 @Override 290 public boolean sameItemAs(Package pkg) { 291 if (pkg instanceof SamplePackage) { 292 SamplePackage newPkg = (SamplePackage)pkg; 293 294 // check they are the same version. 295 return newPkg.getVersion().equals(this.getVersion()); 296 } 297 298 return false; 299 } 300 301 /** 302 * Makes sure the base /samples folder exists before installing. 303 * 304 * {@inheritDoc} 305 */ 306 @Override 307 public boolean preInstallHook(Archive archive, 308 ITaskMonitor monitor, 309 String osSdkRoot, 310 File installFolder) { 311 312 if (installFolder != null && installFolder.isDirectory()) { 313 // Get the hash computed during the last installation 314 String storedHash = readContentHash(installFolder); 315 if (storedHash != null && storedHash.length() > 0) { 316 317 // Get the hash of the folder now 318 String currentHash = computeContentHash(installFolder); 319 320 if (!storedHash.equals(currentHash)) { 321 // The hashes differ. The content was modified. 322 // Ask the user if we should still wipe the old samples. 323 324 String pkgName = archive.getParentPackage().getShortDescription(); 325 326 String msg = String.format( 327 "-= Warning ! =-\n" + 328 "You are about to replace the content of the folder:\n " + 329 " %1$s\n" + 330 "by the new package:\n" + 331 " %2$s.\n" + 332 "\n" + 333 "However it seems that the content of the existing samples " + 334 "has been modified since it was last installed. Are you sure " + 335 "you want to DELETE the existing samples? This cannot be undone.\n" + 336 "Please select YES to delete the existing sample and replace them " + 337 "by the new ones.\n" + 338 "Please select NO to skip this package. You can always install it later.", 339 installFolder.getAbsolutePath(), 340 pkgName); 341 342 // Returns true if we can wipe & replace. 343 return monitor.displayPrompt("SDK Manager: overwrite samples?", msg); 344 } 345 } 346 } 347 348 // The default is to allow installation 349 return super.preInstallHook(archive, monitor, osSdkRoot, installFolder); 350 } 351 352 /** 353 * Computes a hash of the installed content (in case of successful install.) 354 * 355 * {@inheritDoc} 356 */ 357 @Override 358 public void postInstallHook(Archive archive, ITaskMonitor monitor, File installFolder) { 359 super.postInstallHook(archive, monitor, installFolder); 360 361 if (installFolder != null) { 362 String h = computeContentHash(installFolder); 363 saveContentHash(installFolder, h); 364 } 365 } 366 367 /** 368 * Reads the hash from the properties file, if it exists. 369 * Returns null if something goes wrong, e.g. there's no property file or 370 * it doesn't contain our hash. Returns an empty string if the hash wasn't 371 * correctly computed last time by {@link #saveContentHash(File, String)}. 372 */ 373 private String readContentHash(File folder) { 374 Properties props = new Properties(); 375 376 FileInputStream fis = null; 377 try { 378 File f = new File(folder, SdkConstants.FN_CONTENT_HASH_PROP); 379 if (f.isFile()) { 380 fis = new FileInputStream(f); 381 props.load(fis); 382 return props.getProperty("content-hash", null); //$NON-NLS-1$ 383 } 384 } catch (Exception e) { 385 // ignore 386 } finally { 387 if (fis != null) { 388 try { 389 fis.close(); 390 } catch (IOException e) { 391 } 392 } 393 } 394 395 return null; 396 } 397 398 /** 399 * Saves the hash using a properties file 400 */ 401 private void saveContentHash(File folder, String hash) { 402 Properties props = new Properties(); 403 404 props.setProperty("content-hash", hash == null ? "" : hash); //$NON-NLS-1$ //$NON-NLS-2$ 405 406 FileOutputStream fos = null; 407 try { 408 File f = new File(folder, SdkConstants.FN_CONTENT_HASH_PROP); 409 fos = new FileOutputStream(f); 410 props.store( fos, "## Android - hash of this archive."); //$NON-NLS-1$ 411 } catch (IOException e) { 412 e.printStackTrace(); 413 } finally { 414 if (fos != null) { 415 try { 416 fos.close(); 417 } catch (IOException e) { 418 } 419 } 420 } 421 } 422 423 /** 424 * Computes a hash of the files names and sizes installed in the folder 425 * using the SHA-1 digest. 426 * Returns null if the digest algorithm is not available. 427 */ 428 private String computeContentHash(File installFolder) { 429 MessageDigest md = null; 430 try { 431 // SHA-1 is a standard algorithm. 432 // http://java.sun.com/j2se/1.4.2/docs/guide/security/CryptoSpec.html#AppB 433 md = MessageDigest.getInstance("SHA-1"); //$NON-NLS-1$ 434 } catch (NoSuchAlgorithmException e) { 435 // We're unlikely to get there unless this JVM is not spec conforming 436 // in which case there won't be any hash available. 437 } 438 439 if (md != null) { 440 hashDirectoryContent(installFolder, md); 441 return getDigestHexString(md); 442 } 443 444 return null; 445 } 446 447 /** 448 * Computes a hash of the *content* of this directory. The hash only uses 449 * the files names and the file sizes. 450 */ 451 private void hashDirectoryContent(File folder, MessageDigest md) { 452 if (folder == null || md == null || !folder.isDirectory()) { 453 return; 454 } 455 456 for (File f : folder.listFiles()) { 457 if (f.isDirectory()) { 458 hashDirectoryContent(f, md); 459 460 } else { 461 String name = f.getName(); 462 463 // Skip the file we use to store the content hash 464 if (name == null || SdkConstants.FN_CONTENT_HASH_PROP.equals(name)) { 465 continue; 466 } 467 468 try { 469 md.update(name.getBytes("UTF-8")); //$NON-NLS-1$ 470 } catch (UnsupportedEncodingException e) { 471 // There is no valid reason for UTF-8 to be unsupported. Ignore. 472 } 473 try { 474 long len = f.length(); 475 md.update((byte) (len & 0x0FF)); 476 md.update((byte) ((len >> 8) & 0x0FF)); 477 md.update((byte) ((len >> 16) & 0x0FF)); 478 md.update((byte) ((len >> 24) & 0x0FF)); 479 480 } catch (SecurityException e) { 481 // Might happen if file is not readable. Ignore. 482 } 483 } 484 } 485 } 486 487 /** 488 * Returns a digest as an hex string. 489 */ 490 private String getDigestHexString(MessageDigest digester) { 491 // Create an hex string from the digest 492 byte[] digest = digester.digest(); 493 int n = digest.length; 494 String hex = "0123456789abcdef"; //$NON-NLS-1$ 495 char[] hexDigest = new char[n * 2]; 496 for (int i = 0; i < n; i++) { 497 int b = digest[i] & 0x0FF; 498 hexDigest[i*2 + 0] = hex.charAt(b >>> 4); 499 hexDigest[i*2 + 1] = hex.charAt(b & 0x0f); 500 } 501 502 return new String(hexDigest); 503 } 504 } 505