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