• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 &lt;description&gt; 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