OSDN Git Service

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