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