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.annotations.Nullable; 20 import com.android.annotations.VisibleForTesting; 21 import com.android.annotations.VisibleForTesting.Visibility; 22 import com.android.sdklib.NullSdkLog; 23 import com.android.sdklib.SdkConstants; 24 import com.android.sdklib.SdkManager; 25 import com.android.sdklib.internal.repository.IDescription; 26 import com.android.sdklib.internal.repository.LocalSdkParser; 27 import com.android.sdklib.internal.repository.NullTaskMonitor; 28 import com.android.sdklib.internal.repository.XmlParserUtils; 29 import com.android.sdklib.internal.repository.archives.Archive; 30 import com.android.sdklib.internal.repository.archives.Archive.Arch; 31 import com.android.sdklib.internal.repository.archives.Archive.Os; 32 import com.android.sdklib.internal.repository.sources.SdkSource; 33 import com.android.sdklib.repository.PkgProps; 34 import com.android.sdklib.repository.RepoConstants; 35 36 import org.w3c.dom.Node; 37 38 import java.io.File; 39 import java.util.ArrayList; 40 import java.util.Arrays; 41 import java.util.Map; 42 import java.util.Properties; 43 import java.util.regex.Pattern; 44 45 /** 46 * Represents a extra XML node in an SDK repository. 47 */ 48 public class ExtraPackage extends MinToolsPackage 49 implements IMinApiLevelDependency { 50 51 /** 52 * The extra display name. Used in the UI to represent the package. It can be anything. 53 */ 54 private final String mDisplayName; 55 56 /** 57 * The vendor id name. It is a simple alphanumeric string [a-zA-Z0-9_-]. 58 */ 59 private final String mVendorId; 60 61 /** 62 * The vendor display name. Used in the UI to represent the vendor. It can be anything. 63 */ 64 private final String mVendorDisplay; 65 66 /** 67 * The sub-folder name. It must be a non-empty single-segment path. 68 */ 69 private final String mPath; 70 71 /** 72 * The optional old_paths, if any. If present, this is a list of old "path" values that 73 * we'd like to migrate to the current "path" name for this extra. 74 */ 75 private final String mOldPaths; 76 77 /** 78 * The minimal API level required by this extra package, if > 0, 79 * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement. 80 */ 81 private final int mMinApiLevel; 82 83 /** 84 * The project-files listed by this extra package. 85 * The array can be empty but not null. 86 */ 87 private final String[] mProjectFiles; 88 89 /** 90 * Creates a new tool package from the attributes and elements of the given XML node. 91 * This constructor should throw an exception if the package cannot be created. 92 * 93 * @param source The {@link SdkSource} where this is loaded from. 94 * @param packageNode The XML element being parsed. 95 * @param nsUri The namespace URI of the originating XML document, to be able to deal with 96 * parameters that vary according to the originating XML schema. 97 * @param licenses The licenses loaded from the XML originating document. 98 */ ExtraPackage( SdkSource source, Node packageNode, String nsUri, Map<String,String> licenses)99 public ExtraPackage( 100 SdkSource source, 101 Node packageNode, 102 String nsUri, 103 Map<String,String> licenses) { 104 super(source, packageNode, nsUri, licenses); 105 106 mPath = XmlParserUtils.getXmlString(packageNode, RepoConstants.NODE_PATH); 107 108 // Read name-display, vendor-display and vendor-id, introduced in addon-4.xsd. 109 // These are not optional, they are mandatory in addon-4 but we still treat them 110 // as optional so that we can fallback on using <vendor> which was the only one 111 // defined in addon-3.xsd. 112 String name = XmlParserUtils.getXmlString(packageNode, RepoConstants.NODE_NAME_DISPLAY); 113 String vname = XmlParserUtils.getXmlString(packageNode, RepoConstants.NODE_VENDOR_DISPLAY); 114 String vid = XmlParserUtils.getXmlString(packageNode, RepoConstants.NODE_VENDOR_ID); 115 116 if (vid.length() == 0) { 117 // If vid is missing, use the old <vendor> attribute. 118 // Note that in a valid XML, vendor-id cannot be an empty string. 119 // The only reason vid can be empty is when <vendor-id> is missing, which 120 // happens in an addon-3 schema, in which case the old <vendor> needs to be used. 121 String vendor = XmlParserUtils.getXmlString(packageNode, RepoConstants.NODE_VENDOR); 122 vid = sanitizeLegacyVendor(vendor); 123 if (vname.length() == 0) { 124 vname = vendor; 125 } 126 } 127 if (vname.length() == 0) { 128 // The vendor-display name can be empty, in which case we use the vendor-id. 129 vname = vid; 130 } 131 mVendorDisplay = vname.trim(); 132 mVendorId = vid.trim(); 133 134 if (name.length() == 0) { 135 // If name is missing, use the <path> attribute as done in an addon-3 schema. 136 name = getPrettyName(); 137 } 138 mDisplayName = name.trim(); 139 140 mMinApiLevel = XmlParserUtils.getXmlInt( 141 packageNode, RepoConstants.NODE_MIN_API_LEVEL, MIN_API_LEVEL_NOT_SPECIFIED); 142 143 mProjectFiles = parseProjectFiles( 144 XmlParserUtils.getFirstChild(packageNode, RepoConstants.NODE_PROJECT_FILES)); 145 146 mOldPaths = XmlParserUtils.getXmlString(packageNode, RepoConstants.NODE_OLD_PATHS); 147 } 148 parseProjectFiles(Node projectFilesNode)149 private String[] parseProjectFiles(Node projectFilesNode) { 150 ArrayList<String> paths = new ArrayList<String>(); 151 152 if (projectFilesNode != null) { 153 String nsUri = projectFilesNode.getNamespaceURI(); 154 for(Node child = projectFilesNode.getFirstChild(); 155 child != null; 156 child = child.getNextSibling()) { 157 158 if (child.getNodeType() == Node.ELEMENT_NODE && 159 nsUri.equals(child.getNamespaceURI()) && 160 RepoConstants.NODE_PATH.equals(child.getLocalName())) { 161 String path = child.getTextContent(); 162 if (path != null) { 163 path = path.trim(); 164 if (path.length() > 0) { 165 paths.add(path); 166 } 167 } 168 } 169 } 170 } 171 172 return paths.toArray(new String[paths.size()]); 173 } 174 175 /** 176 * Manually create a new package with one archive and the given attributes or properties. 177 * This is used to create packages from local directories in which case there must be 178 * one archive which URL is the actual target location. 179 * <p/> 180 * By design, this creates a package with one and only one archive. 181 */ create(SdkSource source, Properties props, String vendor, String path, int revision, String license, String description, String descUrl, Os archiveOs, Arch archiveArch, String archiveOsPath)182 public static Package create(SdkSource source, 183 Properties props, 184 String vendor, 185 String path, 186 int revision, 187 String license, 188 String description, 189 String descUrl, 190 Os archiveOs, 191 Arch archiveArch, 192 String archiveOsPath) { 193 ExtraPackage ep = new ExtraPackage(source, props, vendor, path, revision, license, 194 description, descUrl, archiveOs, archiveArch, archiveOsPath); 195 return ep; 196 } 197 198 /** 199 * Constructor used to create a mock {@link ExtraPackage}. 200 * Most of the attributes here are optional. 201 * When not defined, they will be extracted from the {@code props} properties. 202 */ 203 @VisibleForTesting(visibility=Visibility.PRIVATE) ExtraPackage(SdkSource source, Properties props, String vendorId, String path, int revision, String license, String description, String descUrl, Os archiveOs, Arch archiveArch, String archiveOsPath)204 protected ExtraPackage(SdkSource source, 205 Properties props, 206 String vendorId, 207 String path, 208 int revision, 209 String license, 210 String description, 211 String descUrl, 212 Os archiveOs, 213 Arch archiveArch, 214 String archiveOsPath) { 215 super(source, 216 props, 217 revision, 218 license, 219 description, 220 descUrl, 221 archiveOs, 222 archiveArch, 223 archiveOsPath); 224 225 // The path argument comes before whatever could be in the properties 226 mPath = path != null ? path : getProperty(props, PkgProps.EXTRA_PATH, path); 227 228 String name = getProperty(props, PkgProps.EXTRA_NAME_DISPLAY, ""); //$NON-NLS-1$ 229 String vname = getProperty(props, PkgProps.EXTRA_VENDOR_DISPLAY, ""); //$NON-NLS-1$ 230 String vid = vendorId != null ? vendorId : 231 getProperty(props, PkgProps.EXTRA_VENDOR_ID, ""); //$NON-NLS-1$ 232 233 if (vid.length() == 0) { 234 // If vid is missing, use the old <vendor> attribute. 235 // <vendor> did not exist prior to schema repo-v3 and tools r8. 236 String vendor = getProperty(props, PkgProps.EXTRA_VENDOR, ""); //$NON-NLS-1$ 237 vid = sanitizeLegacyVendor(vendor); 238 if (vname.length() == 0) { 239 vname = vendor; 240 } 241 } 242 if (vname.length() == 0) { 243 // The vendor-display name can be empty, in which case we use the vendor-id. 244 vname = vid; 245 } 246 mVendorDisplay = vname.trim(); 247 mVendorId = vid.trim(); 248 249 if (name.length() == 0) { 250 // If name is missing, use the <path> attribute as done in an addon-3 schema. 251 name = getPrettyName(); 252 } 253 mDisplayName = name.trim(); 254 255 mOldPaths = getProperty(props, PkgProps.EXTRA_OLD_PATHS, null); 256 257 mMinApiLevel = Integer.parseInt( 258 getProperty(props, 259 PkgProps.EXTRA_MIN_API_LEVEL, 260 Integer.toString(MIN_API_LEVEL_NOT_SPECIFIED))); 261 262 String projectFiles = getProperty(props, PkgProps.EXTRA_PROJECT_FILES, null); 263 ArrayList<String> filePaths = new ArrayList<String>(); 264 if (projectFiles != null && projectFiles.length() > 0) { 265 for (String filePath : projectFiles.split(Pattern.quote(File.pathSeparator))) { 266 filePath = filePath.trim(); 267 if (filePath.length() > 0) { 268 filePaths.add(filePath); 269 } 270 } 271 } 272 mProjectFiles = filePaths.toArray(new String[filePaths.size()]); 273 } 274 275 /** 276 * Save the properties of the current packages in the given {@link Properties} object. 277 * These properties will later be give the constructor that takes a {@link Properties} object. 278 */ 279 @Override saveProperties(Properties props)280 public void saveProperties(Properties props) { 281 super.saveProperties(props); 282 283 props.setProperty(PkgProps.EXTRA_PATH, mPath); 284 props.setProperty(PkgProps.EXTRA_NAME_DISPLAY, mDisplayName); 285 props.setProperty(PkgProps.EXTRA_VENDOR_DISPLAY, mVendorDisplay); 286 props.setProperty(PkgProps.EXTRA_VENDOR_ID, mVendorId); 287 288 if (getMinApiLevel() != MIN_API_LEVEL_NOT_SPECIFIED) { 289 props.setProperty(PkgProps.EXTRA_MIN_API_LEVEL, Integer.toString(getMinApiLevel())); 290 } 291 292 if (mProjectFiles.length > 0) { 293 StringBuilder sb = new StringBuilder(); 294 for (int i = 0; i < mProjectFiles.length; i++) { 295 if (i > 0) { 296 sb.append(File.pathSeparatorChar); 297 } 298 sb.append(mProjectFiles[i]); 299 } 300 props.setProperty(PkgProps.EXTRA_PROJECT_FILES, sb.toString()); 301 } 302 303 if (mOldPaths != null && mOldPaths.length() > 0) { 304 props.setProperty(PkgProps.EXTRA_OLD_PATHS, mOldPaths); 305 } 306 } 307 308 /** 309 * Returns the minimal API level required by this extra package, if > 0, 310 * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement. 311 */ 312 @Override getMinApiLevel()313 public int getMinApiLevel() { 314 return mMinApiLevel; 315 } 316 317 /** 318 * The project-files listed by this extra package. 319 * The array can be empty but not null. 320 * <p/> 321 * IMPORTANT: directory separators are NOT translated and may not match 322 * the {@link File#separatorChar} of the current platform. It's up to the 323 * user to adequately interpret the paths. 324 * Similarly, no guarantee is made on the validity of the paths. 325 * Users are expected to apply all usual sanity checks such as removing 326 * "./" and "../" and making sure these paths don't reference files outside 327 * of the installed archive. 328 * 329 * @since sdk-repository-4.xsd or sdk-addon-2.xsd 330 */ getProjectFiles()331 public String[] getProjectFiles() { 332 return mProjectFiles; 333 } 334 335 /** 336 * Returns the old_paths, a list of obsolete path names for the extra package. 337 * <p/> 338 * These can be used by the installer to migrate an extra package using one of the 339 * old paths into the new path. 340 * <p/> 341 * These can also be used to recognize "old" renamed packages as the same as 342 * the current one. 343 * 344 * @return A list of old paths. Can be empty but not null. 345 */ getOldPaths()346 public String[] getOldPaths() { 347 if (mOldPaths == null || mOldPaths.length() == 0) { 348 return new String[0]; 349 } 350 return mOldPaths.split(";"); //$NON-NLS-1$ 351 } 352 353 /** 354 * Returns the sanitized path folder name. It is a single-segment path. 355 * <p/> 356 * The package is installed in SDK/extras/vendor_name/path_name. 357 */ getPath()358 public String getPath() { 359 // The XSD specifies the XML vendor and path should only contain [a-zA-Z0-9]+ 360 // and cannot be empty. Let's be defensive and enforce that anyway since things 361 // like "____" are still valid values that we don't want to allow. 362 363 // Sanitize the path 364 String path = mPath.replaceAll("[^a-zA-Z0-9-]+", "_"); //$NON-NLS-1$ 365 if (path.length() == 0 || path.equals("_")) { //$NON-NLS-1$ 366 int h = path.hashCode(); 367 path = String.format("extra%08x", h); //$NON-NLS-1$ 368 } 369 370 return path; 371 } 372 373 /** 374 * Returns the vendor id. 375 */ getVendorId()376 public String getVendorId() { 377 return mVendorId; 378 } 379 getVendorDisplay()380 public String getVendorDisplay() { 381 return mVendorDisplay; 382 } 383 getDisplayName()384 public String getDisplayName() { 385 return mDisplayName; 386 } 387 388 /** Transforms the legacy vendor name into a usable vendor id. */ sanitizeLegacyVendor(String vendorDisplay)389 private String sanitizeLegacyVendor(String vendorDisplay) { 390 // The XSD specifies the XML vendor and path should only contain [a-zA-Z0-9]+ 391 // and cannot be empty. Let's be defensive and enforce that anyway since things 392 // like "____" are still valid values that we don't want to allow. 393 394 if (vendorDisplay != null && vendorDisplay.length() > 0) { 395 String vendor = vendorDisplay.trim(); 396 // Sanitize the vendor 397 vendor = vendor.replaceAll("[^a-zA-Z0-9-]+", "_"); //$NON-NLS-1$ 398 if (vendor.equals("_")) { //$NON-NLS-1$ 399 int h = vendor.hashCode(); 400 vendor = String.format("vendor%08x", h); //$NON-NLS-1$ 401 } 402 403 return vendor; 404 } 405 406 return ""; //$NON-NLS-1$ 407 408 } 409 410 /** 411 * Used to produce a suitable name-display based on the current {@link #mPath} 412 * and {@link #mVendorDisplay} in addon-3 schemas. 413 */ getPrettyName()414 private String getPrettyName() { 415 String name = mPath; 416 417 // In the past, we used to save the extras in a folder vendor-path, 418 // and that "vendor" would end up in the path when we reload the extra from 419 // disk. Detect this and compensate. 420 if (mVendorDisplay != null && mVendorDisplay.length() > 0) { 421 if (name.startsWith(mVendorDisplay + "-")) { //$NON-NLS-1$ 422 name = name.substring(mVendorDisplay.length() + 1); 423 } 424 } 425 426 // Uniformize all spaces in the name 427 if (name != null) { 428 name = name.replaceAll("[ _\t\f-]+", " ").trim(); //$NON-NLS-1$ //$NON-NLS-2$ 429 } 430 if (name == null || name.length() == 0) { 431 name = "Unknown Extra"; 432 } 433 434 if (mVendorDisplay != null && mVendorDisplay.length() > 0) { 435 name = mVendorDisplay + " " + name; //$NON-NLS-1$ 436 name = name.replaceAll("[ _\t\f-]+", " ").trim(); //$NON-NLS-1$ //$NON-NLS-2$ 437 } 438 439 // Look at all lower case characters in range [1..n-1] and replace them by an upper 440 // case if they are preceded by a space. Also upper cases the first character of the 441 // string. 442 boolean changed = false; 443 char[] chars = name.toCharArray(); 444 for (int n = chars.length - 1, i = 0; i < n; i++) { 445 if (Character.isLowerCase(chars[i]) && (i == 0 || chars[i - 1] == ' ')) { 446 chars[i] = Character.toUpperCase(chars[i]); 447 changed = true; 448 } 449 } 450 if (changed) { 451 name = new String(chars); 452 } 453 454 // Special case: reformat a few typical acronyms. 455 name = name.replaceAll(" Usb ", " USB "); //$NON-NLS-1$ 456 name = name.replaceAll(" Api ", " API "); //$NON-NLS-1$ 457 458 return name; 459 } 460 461 /** 462 * Returns a string identifier to install this package from the command line. 463 * For extras, we use "extra-vendor-path". 464 * <p/> 465 * {@inheritDoc} 466 */ 467 @Override installId()468 public String installId() { 469 return String.format("extra-%1$s-%2$s", //$NON-NLS-1$ 470 getVendorId(), 471 getPath()); 472 } 473 474 /** 475 * Returns a description of this package that is suitable for a list display. 476 * <p/> 477 * {@inheritDoc} 478 */ 479 @Override getListDescription()480 public String getListDescription() { 481 String s = String.format("%1$s%2$s", 482 getDisplayName(), 483 isObsolete() ? " (Obsolete)" : ""); //$NON-NLS-2$ 484 485 return s; 486 } 487 488 /** 489 * Returns a short description for an {@link IDescription}. 490 */ 491 @Override getShortDescription()492 public String getShortDescription() { 493 String s = String.format("%1$s, revision %2$d%3$s", 494 getDisplayName(), 495 getRevision(), 496 isObsolete() ? " (Obsolete)" : ""); //$NON-NLS-2$ 497 498 return s; 499 } 500 501 /** 502 * Returns a long description for an {@link IDescription}. 503 * 504 * The long description is whatever the XML contains for the <description> field, 505 * or the short description if the former is empty. 506 */ 507 @Override getLongDescription()508 public String getLongDescription() { 509 String s = String.format("%1$s, revision %2$d%3$s\nBy %4$s", 510 getDisplayName(), 511 getRevision(), 512 isObsolete() ? " (Obsolete)" : "", //$NON-NLS-2$ 513 getVendorDisplay()); 514 515 String d = getDescription(); 516 if (d != null && d.length() > 0) { 517 s += '\n' + d; 518 } 519 520 if (getMinToolsRevision() != MIN_TOOLS_REV_NOT_SPECIFIED) { 521 s += String.format("\nRequires tools revision %1$d", getMinToolsRevision()); 522 } 523 524 if (getMinApiLevel() != MIN_API_LEVEL_NOT_SPECIFIED) { 525 s += String.format("\nRequires SDK Platform Android API %1$s", getMinApiLevel()); 526 } 527 528 File localPath = getLocalArchivePath(); 529 if (localPath != null) { 530 // For a local archive, also put the install path in the long description. 531 // This should help users locate the extra on their drive. 532 s += String.format("\nLocation: %1$s", localPath.getAbsolutePath()); 533 } else { 534 // For a non-installed archive, indicate where it would be installed. 535 s += String.format("\nInstall path: %1$s", 536 getInstallSubFolder(null/*sdk root*/).getPath()); 537 } 538 539 return s; 540 } 541 542 /** 543 * Computes a potential installation folder if an archive of this package were 544 * to be installed right away in the given SDK root. 545 * <p/> 546 * A "tool" package should always be located in SDK/tools. 547 * 548 * @param osSdkRoot The OS path of the SDK root folder. Must NOT be null. 549 * @param sdkManager An existing SDK manager to list current platforms and addons. 550 * Not used in this implementation. 551 * @return A new {@link File} corresponding to the directory to use to install this package. 552 */ 553 @Override getInstallFolder(String osSdkRoot, SdkManager sdkManager)554 public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) { 555 556 // First find if this extra is already installed. If so, reuse the same directory. 557 LocalSdkParser localParser = new LocalSdkParser(); 558 Package[] pkgs = localParser.parseSdk( 559 osSdkRoot, 560 sdkManager, 561 LocalSdkParser.PARSE_EXTRAS, 562 new NullTaskMonitor(new NullSdkLog())); 563 564 for (Package pkg : pkgs) { 565 if (sameItemAs(pkg) && pkg instanceof ExtraPackage) { 566 File localPath = ((ExtraPackage) pkg).getLocalArchivePath(); 567 if (localPath != null) { 568 return localPath; 569 } 570 } 571 } 572 573 return getInstallSubFolder(osSdkRoot); 574 } 575 576 /** 577 * Computes the "sub-folder" install path, relative to the given SDK root. 578 * For an extra package, this is generally ".../extra/vendor-id/path". 579 * 580 * @param osSdkRoot The OS path of the SDK root folder if known. 581 * This CAN be null, in which case the path will start at /extra. 582 * @return Either /extra/vendor/path or sdk-root/extra/vendor-id/path. 583 */ getInstallSubFolder(@ullable String osSdkRoot)584 private File getInstallSubFolder(@Nullable String osSdkRoot) { 585 // The /extras dir at the root of the SDK 586 File path = new File(osSdkRoot, SdkConstants.FD_EXTRAS); 587 588 String vendor = getVendorId(); 589 if (vendor != null && vendor.length() > 0) { 590 path = new File(path, vendor); 591 } 592 593 String name = getPath(); 594 if (name != null && name.length() > 0) { 595 path = new File(path, name); 596 } 597 598 return path; 599 } 600 601 @Override sameItemAs(Package pkg)602 public boolean sameItemAs(Package pkg) { 603 // Extra packages are similar if they have the same path and vendor 604 if (pkg instanceof ExtraPackage) { 605 ExtraPackage ep = (ExtraPackage) pkg; 606 607 String[] epOldPaths = ep.getOldPaths(); 608 int lenEpOldPaths = epOldPaths.length; 609 for (int indexEp = -1; indexEp < lenEpOldPaths; indexEp++) { 610 if (sameVendorAndPath( 611 mVendorId, mPath, 612 ep.mVendorId, indexEp < 0 ? ep.mPath : epOldPaths[indexEp])) { 613 return true; 614 } 615 } 616 617 String[] thisOldPaths = getOldPaths(); 618 int lenThisOldPaths = thisOldPaths.length; 619 for (int indexThis = -1; indexThis < lenThisOldPaths; indexThis++) { 620 if (sameVendorAndPath( 621 mVendorId, indexThis < 0 ? mPath : thisOldPaths[indexThis], 622 ep.mVendorId, ep.mPath)) { 623 return true; 624 } 625 } 626 } 627 628 return false; 629 } 630 sameVendorAndPath( String thisVendor, String thisPath, String otherVendor, String otherPath)631 private static boolean sameVendorAndPath( 632 String thisVendor, String thisPath, 633 String otherVendor, String otherPath) { 634 // To be backward compatible, we need to support the old vendor-path form 635 // in either the current or the remote package. 636 // 637 // The vendor test below needs to account for an old installed package 638 // (e.g. with an install path of vendor-name) that has then been updated 639 // in-place and thus when reloaded contains the vendor name in both the 640 // path and the vendor attributes. 641 if (otherPath != null && thisPath != null && thisVendor != null) { 642 if (otherPath.equals(thisVendor + '-' + thisPath) && 643 (otherVendor == null || 644 otherVendor.length() == 0 || 645 otherVendor.equals(thisVendor))) { 646 return true; 647 } 648 } 649 if (thisPath != null && otherPath != null && otherVendor != null) { 650 if (thisPath.equals(otherVendor + '-' + otherPath) && 651 (thisVendor == null || 652 thisVendor.length() == 0 || 653 thisVendor.equals(otherVendor))) { 654 return true; 655 } 656 } 657 658 659 if (thisPath != null && thisPath.equals(otherPath)) { 660 if ((thisVendor == null && otherVendor == null) || 661 (thisVendor != null && thisVendor.equals(otherVendor))) { 662 return true; 663 } 664 } 665 666 return false; 667 } 668 669 /** 670 * For extra packages, we want to add vendor|path to the sorting key 671 * <em>before<em/> the revision number. 672 * <p/> 673 * {@inheritDoc} 674 */ 675 @Override comparisonKey()676 protected String comparisonKey() { 677 String s = super.comparisonKey(); 678 int pos = s.indexOf("|r:"); //$NON-NLS-1$ 679 assert pos > 0; 680 s = s.substring(0, pos) + 681 "|ve:" + getVendorId() + //$NON-NLS-1$ 682 "|pa:" + getPath() + //$NON-NLS-1$ 683 s.substring(pos); 684 return s; 685 } 686 687 // --- 688 689 /** 690 * If this package is installed, returns the install path of the archive if valid. 691 * Returns null if not installed or if the path does not exist. 692 */ getLocalArchivePath()693 private File getLocalArchivePath() { 694 Archive[] archives = getArchives(); 695 if (archives.length == 1 && archives[0].isLocal()) { 696 File path = new File(archives[0].getLocalOsPath()); 697 if (path.isDirectory()) { 698 return path; 699 } 700 } 701 702 return null; 703 } 704 705 @Override hashCode()706 public int hashCode() { 707 final int prime = 31; 708 int result = super.hashCode(); 709 result = prime * result + mMinApiLevel; 710 result = prime * result + ((mPath == null) ? 0 : mPath.hashCode()); 711 result = prime * result + Arrays.hashCode(mProjectFiles); 712 result = prime * result + ((mVendorDisplay == null) ? 0 : mVendorDisplay.hashCode()); 713 return result; 714 } 715 716 @Override equals(Object obj)717 public boolean equals(Object obj) { 718 if (this == obj) { 719 return true; 720 } 721 if (!super.equals(obj)) { 722 return false; 723 } 724 if (!(obj instanceof ExtraPackage)) { 725 return false; 726 } 727 ExtraPackage other = (ExtraPackage) obj; 728 if (mMinApiLevel != other.mMinApiLevel) { 729 return false; 730 } 731 if (mPath == null) { 732 if (other.mPath != null) { 733 return false; 734 } 735 } else if (!mPath.equals(other.mPath)) { 736 return false; 737 } 738 if (!Arrays.equals(mProjectFiles, other.mProjectFiles)) { 739 return false; 740 } 741 if (mVendorDisplay == null) { 742 if (other.mVendorDisplay != null) { 743 return false; 744 } 745 } else if (!mVendorDisplay.equals(other.mVendorDisplay)) { 746 return false; 747 } 748 return true; 749 } 750 } 751