• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 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;
18 
19 import com.android.annotations.NonNull;
20 import com.android.annotations.VisibleForTesting;
21 import com.android.annotations.VisibleForTesting.Visibility;
22 import com.android.io.FileWrapper;
23 import com.android.prefs.AndroidLocation;
24 import com.android.prefs.AndroidLocation.AndroidLocationException;
25 import com.android.sdklib.AndroidVersion.AndroidVersionException;
26 import com.android.sdklib.ISystemImage.LocationType;
27 import com.android.sdklib.internal.project.ProjectProperties;
28 import com.android.sdklib.internal.repository.LocalSdkParser;
29 import com.android.sdklib.internal.repository.NullTaskMonitor;
30 import com.android.sdklib.internal.repository.archives.Archive;
31 import com.android.sdklib.internal.repository.packages.ExtraPackage;
32 import com.android.sdklib.internal.repository.packages.Package;
33 import com.android.sdklib.repository.PkgProps;
34 import com.android.util.Pair;
35 
36 import java.io.File;
37 import java.io.FileInputStream;
38 import java.io.FileNotFoundException;
39 import java.io.FileWriter;
40 import java.io.IOException;
41 import java.util.ArrayList;
42 import java.util.Collections;
43 import java.util.HashMap;
44 import java.util.HashSet;
45 import java.util.Map;
46 import java.util.Properties;
47 import java.util.Set;
48 import java.util.TreeSet;
49 import java.util.regex.Matcher;
50 import java.util.regex.Pattern;
51 
52 /**
53  * The SDK manager parses the SDK folder and gives access to the content.
54  * @see PlatformTarget
55  * @see AddOnTarget
56  */
57 public class SdkManager {
58 
59     public final static String PROP_VERSION_SDK = "ro.build.version.sdk";              //$NON-NLS-1$
60     public final static String PROP_VERSION_CODENAME = "ro.build.version.codename";    //$NON-NLS-1$
61     public final static String PROP_VERSION_RELEASE = "ro.build.version.release";      //$NON-NLS-1$
62 
63     public final static String ADDON_NAME = "name";                                    //$NON-NLS-1$
64     public final static String ADDON_VENDOR = "vendor";                                //$NON-NLS-1$
65     public final static String ADDON_API = "api";                                      //$NON-NLS-1$
66     public final static String ADDON_DESCRIPTION = "description";                      //$NON-NLS-1$
67     public final static String ADDON_LIBRARIES = "libraries";                          //$NON-NLS-1$
68     public final static String ADDON_DEFAULT_SKIN = "skin";                            //$NON-NLS-1$
69     public final static String ADDON_USB_VENDOR = "usb-vendor";                        //$NON-NLS-1$
70     public final static String ADDON_REVISION = "revision";                            //$NON-NLS-1$
71     public final static String ADDON_REVISION_OLD = "version";                         //$NON-NLS-1$
72 
73 
74     private final static Pattern PATTERN_LIB_DATA = Pattern.compile(
75             "^([a-zA-Z0-9._-]+\\.jar);(.*)$", Pattern.CASE_INSENSITIVE);               //$NON-NLS-1$
76 
77      // usb ids are 16-bit hexadecimal values.
78     private final static Pattern PATTERN_USB_IDS = Pattern.compile(
79             "^0x[a-f0-9]{4}$", Pattern.CASE_INSENSITIVE);                              //$NON-NLS-1$
80 
81     /** List of items in the platform to check when parsing it. These paths are relative to the
82      * platform root folder. */
83     private final static String[] sPlatformContentList = new String[] {
84         SdkConstants.FN_FRAMEWORK_LIBRARY,
85         SdkConstants.FN_FRAMEWORK_AIDL,
86     };
87 
88     /** Preference file containing the usb ids for adb */
89     private final static String ADB_INI_FILE = "adb_usb.ini";                          //$NON-NLS-1$
90        //0--------90--------90--------90--------90--------90--------90--------90--------9
91     private final static String ADB_INI_HEADER =
92         "# ANDROID 3RD PARTY USB VENDOR ID LIST -- DO NOT EDIT.\n" +                   //$NON-NLS-1$
93         "# USE 'android update adb' TO GENERATE.\n" +                                  //$NON-NLS-1$
94         "# 1 USB VENDOR ID PER LINE.\n";                                               //$NON-NLS-1$
95 
96     /** The location of the SDK as an OS path */
97     private final String mOsSdkPath;
98     /** Valid targets that have been loaded. Can be empty but not null. */
99     private IAndroidTarget[] mTargets = new IAndroidTarget[0];
100 
101     public static class LayoutlibVersion implements Comparable<LayoutlibVersion> {
102         private final int mApi;
103         private final int mRevision;
104 
105         public static final int NOT_SPECIFIED = 0;
106 
LayoutlibVersion(int api, int revision)107         public LayoutlibVersion(int api, int revision) {
108             mApi = api;
109             mRevision = revision;
110         }
111 
getApi()112         public int getApi() {
113             return mApi;
114         }
115 
getRevision()116         public int getRevision() {
117             return mRevision;
118         }
119 
120         @Override
compareTo(LayoutlibVersion rhs)121         public int compareTo(LayoutlibVersion rhs) {
122             boolean useRev = this.mRevision > NOT_SPECIFIED && rhs.mRevision > NOT_SPECIFIED;
123             int lhsValue = (this.mApi << 16) + (useRev ? this.mRevision : 0);
124             int rhsValue = (rhs.mApi  << 16) + (useRev ? rhs.mRevision  : 0);
125             return lhsValue - rhsValue;
126         }
127     }
128 
129     /**
130      * Create a new {@link SdkManager} instance.
131      * External users should use {@link #createManager(String, ISdkLog)}.
132      *
133      * @param osSdkPath the location of the SDK.
134      */
135     @VisibleForTesting(visibility=Visibility.PRIVATE)
SdkManager(String osSdkPath)136     protected SdkManager(String osSdkPath) {
137         mOsSdkPath = osSdkPath;
138     }
139 
140     /**
141      * Creates an {@link SdkManager} for a given sdk location.
142      * @param osSdkPath the location of the SDK.
143      * @param log the ISdkLog object receiving warning/error from the parsing. Cannot be null.
144      * @return the created {@link SdkManager} or null if the location is not valid.
145      */
createManager(String osSdkPath, ISdkLog log)146     public static SdkManager createManager(String osSdkPath, ISdkLog log) {
147         try {
148             SdkManager manager = new SdkManager(osSdkPath);
149             ArrayList<IAndroidTarget> list = new ArrayList<IAndroidTarget>();
150             loadPlatforms(osSdkPath, list, log);
151             loadAddOns(osSdkPath, list, log);
152 
153             // sort the targets/add-ons
154             Collections.sort(list);
155 
156             manager.setTargets(list.toArray(new IAndroidTarget[list.size()]));
157 
158             // Initialize the targets' sample paths, after the targets have been set.
159             manager.initializeSamplePaths(log);
160 
161             return manager;
162         } catch (IllegalArgumentException e) {
163             log.error(e, "Error parsing the sdk.");
164         }
165 
166         return null;
167     }
168 
169     /**
170      * Returns the location of the SDK.
171      */
getLocation()172     public String getLocation() {
173         return mOsSdkPath;
174     }
175 
176     /**
177      * Returns the targets that are available in the SDK.
178      * <p/>
179      * The array can be empty but not null.
180      */
getTargets()181     public IAndroidTarget[] getTargets() {
182         return mTargets;
183     }
184 
185     /**
186      * Sets the targets that are available in the SDK.
187      * <p/>
188      * The array can be empty but not null.
189      */
190     @VisibleForTesting(visibility=Visibility.PRIVATE)
setTargets(IAndroidTarget[] targets)191     protected void setTargets(IAndroidTarget[] targets) {
192         assert targets != null;
193         mTargets = targets;
194     }
195 
196     /**
197      * Returns a target from a hash that was generated by {@link IAndroidTarget#hashString()}.
198      *
199      * @param hash the {@link IAndroidTarget} hash string.
200      * @return The matching {@link IAndroidTarget} or null.
201      */
getTargetFromHashString(String hash)202     public IAndroidTarget getTargetFromHashString(String hash) {
203         if (hash != null) {
204             for (IAndroidTarget target : mTargets) {
205                 if (hash.equals(target.hashString())) {
206                     return target;
207                 }
208             }
209         }
210 
211         return null;
212     }
213 
214     /**
215      * Updates adb with the USB devices declared in the SDK add-ons.
216      * @throws AndroidLocationException
217      * @throws IOException
218      */
updateAdb()219     public void updateAdb() throws AndroidLocationException, IOException {
220         FileWriter writer = null;
221         try {
222             // get the android prefs location to know where to write the file.
223             File adbIni = new File(AndroidLocation.getFolder(), ADB_INI_FILE);
224             writer = new FileWriter(adbIni);
225 
226             // first, put all the vendor id in an HashSet to remove duplicate.
227             HashSet<Integer> set = new HashSet<Integer>();
228             IAndroidTarget[] targets = getTargets();
229             for (IAndroidTarget target : targets) {
230                 if (target.getUsbVendorId() != IAndroidTarget.NO_USB_ID) {
231                     set.add(target.getUsbVendorId());
232                 }
233             }
234 
235             // write file header.
236             writer.write(ADB_INI_HEADER);
237 
238             // now write the Id in a text file, one per line.
239             for (Integer i : set) {
240                 writer.write(String.format("0x%04x\n", i));                            //$NON-NLS-1$
241             }
242         } finally {
243             if (writer != null) {
244                 writer.close();
245             }
246         }
247     }
248 
249     /**
250      * Reloads the content of the SDK.
251      *
252      * @param log the ISdkLog object receiving warning/error from the parsing. Cannot be null.
253      */
reloadSdk(ISdkLog log)254     public void reloadSdk(ISdkLog log) {
255         // get the current target list.
256         ArrayList<IAndroidTarget> list = new ArrayList<IAndroidTarget>();
257         loadPlatforms(mOsSdkPath, list, log);
258         loadAddOns(mOsSdkPath, list, log);
259 
260         // For now replace the old list with the new one.
261         // In the future we may want to keep the current objects, so that ADT doesn't have to deal
262         // with new IAndroidTarget objects when a target didn't actually change.
263 
264         // sort the targets/add-ons
265         Collections.sort(list);
266         setTargets(list.toArray(new IAndroidTarget[list.size()]));
267 
268         // load the samples, after the targets have been set.
269         initializeSamplePaths(log);
270     }
271 
272     /**
273      * Returns the greatest {@link LayoutlibVersion} found amongst all platform
274      * targets currently loaded in the SDK.
275      * <p/>
276      * We only started recording Layoutlib Versions recently in the platform meta data
277      * so it's possible to have an SDK with many platforms loaded but no layoutlib
278      * version defined.
279      *
280      * @return The greatest {@link LayoutlibVersion} or null if none is found.
281      * @deprecated This does NOT solve the right problem and will be changed later.
282      */
283     @Deprecated
getMaxLayoutlibVersion()284     public LayoutlibVersion getMaxLayoutlibVersion() {
285         LayoutlibVersion maxVersion = null;
286 
287         for (IAndroidTarget target : getTargets()) {
288             if (target instanceof PlatformTarget) {
289                 LayoutlibVersion lv = ((PlatformTarget) target).getLayoutlibVersion();
290                 if (lv != null) {
291                     if (maxVersion == null || lv.compareTo(maxVersion) > 0) {
292                         maxVersion = lv;
293                     }
294                 }
295             }
296         }
297 
298         return maxVersion;
299     }
300 
301     /**
302      * Returns a map of the <em>root samples directories</em> located in the SDK/extras packages.
303      * No guarantee is made that the extras' samples directory actually contain any valid samples.
304      * The only guarantee is that the root samples directory actually exists.
305      * The map is { File: Samples root directory => String: Extra package display name. }
306      *
307      * @return A non-null possibly empty map of extra samples directories and their associated
308      *   extra package display name.
309      */
getExtraSamples()310     public @NonNull Map<File, String> getExtraSamples() {
311         LocalSdkParser parser = new LocalSdkParser();
312         Package[] packages = parser.parseSdk(mOsSdkPath,
313                                              this,
314                                              LocalSdkParser.PARSE_EXTRAS,
315                                              new NullTaskMonitor(new NullSdkLog()));
316 
317         Map<File, String> samples = new HashMap<File, String>();
318 
319         for (Package pkg : packages) {
320             if (pkg instanceof ExtraPackage && pkg.isLocal()) {
321                 // isLocal()==true implies there's a single locally-installed archive.
322                 assert pkg.getArchives() != null && pkg.getArchives().length == 1;
323                 Archive a = pkg.getArchives()[0];
324                 assert a != null;
325                 File path = new File(a.getLocalOsPath(), SdkConstants.FD_SAMPLES);
326                 if (path.isDirectory()) {
327                     samples.put(path, pkg.getListDescription());
328                 }
329             }
330         }
331 
332         return samples;
333     }
334 
335 
336     // -------- private methods ----------
337 
338     /**
339      * Loads the Platforms from the SDK.
340      * Creates the "platforms" folder if necessary.
341      *
342      * @param sdkOsPath Location of the SDK
343      * @param list the list to fill with the platforms.
344      * @param log the ISdkLog object receiving warning/error from the parsing. Cannot be null.
345      * @throws RuntimeException when the "platforms" folder is missing and cannot be created.
346      */
loadPlatforms(String sdkOsPath, ArrayList<IAndroidTarget> list, ISdkLog log)347     private static void loadPlatforms(String sdkOsPath, ArrayList<IAndroidTarget> list,
348             ISdkLog log) {
349         File platformFolder = new File(sdkOsPath, SdkConstants.FD_PLATFORMS);
350 
351         if (platformFolder.isDirectory()) {
352             File[] platforms  = platformFolder.listFiles();
353 
354             for (File platform : platforms) {
355                 if (platform.isDirectory()) {
356                     PlatformTarget target = loadPlatform(sdkOsPath, platform, log);
357                     if (target != null) {
358                         list.add(target);
359                     }
360                 } else {
361                     log.warning("Ignoring platform '%1$s', not a folder.", platform.getName());
362                 }
363             }
364 
365             return;
366         }
367 
368         // Try to create it or complain if something else is in the way.
369         if (!platformFolder.exists()) {
370             if (!platformFolder.mkdir()) {
371                 throw new RuntimeException(
372                         String.format("Failed to create %1$s.",
373                                 platformFolder.getAbsolutePath()));
374             }
375         } else {
376             throw new RuntimeException(
377                     String.format("%1$s is not a folder.",
378                             platformFolder.getAbsolutePath()));
379         }
380     }
381 
382     /**
383      * Loads a specific Platform at a given location.
384      * @param sdkOsPath Location of the SDK
385      * @param platformFolder the root folder of the platform.
386      * @param log the ISdkLog object receiving warning/error from the parsing. Cannot be null.
387      */
loadPlatform(String sdkOsPath, File platformFolder, ISdkLog log)388     private static PlatformTarget loadPlatform(String sdkOsPath, File platformFolder,
389             ISdkLog log) {
390         FileWrapper buildProp = new FileWrapper(platformFolder, SdkConstants.FN_BUILD_PROP);
391         FileWrapper sourcePropFile = new FileWrapper(platformFolder, SdkConstants.FN_SOURCE_PROP);
392 
393         if (buildProp.isFile() && sourcePropFile.isFile()) {
394             Map<String, String> platformProp = new HashMap<String, String>();
395 
396             // add all the property files
397             Map<String, String> map = ProjectProperties.parsePropertyFile(buildProp, log);
398             if (map != null) {
399                 platformProp.putAll(map);
400             }
401 
402             map = ProjectProperties.parsePropertyFile(sourcePropFile, log);
403             if (map != null) {
404                 platformProp.putAll(map);
405             }
406 
407             FileWrapper sdkPropFile = new FileWrapper(platformFolder, SdkConstants.FN_SDK_PROP);
408             if (sdkPropFile.isFile()) { // obsolete platforms don't have this.
409                 map = ProjectProperties.parsePropertyFile(sdkPropFile, log);
410                 if (map != null) {
411                     platformProp.putAll(map);
412                 }
413             }
414 
415             // look for some specific values in the map.
416 
417             // api level
418             int apiNumber;
419             String stringValue = platformProp.get(PROP_VERSION_SDK);
420             if (stringValue == null) {
421                 log.warning(
422                         "Ignoring platform '%1$s': %2$s is missing from '%3$s'",
423                         platformFolder.getName(), PROP_VERSION_SDK,
424                         SdkConstants.FN_BUILD_PROP);
425                 return null;
426             } else {
427                 try {
428                      apiNumber = Integer.parseInt(stringValue);
429                 } catch (NumberFormatException e) {
430                     // looks like apiNumber does not parse to a number.
431                     // Ignore this platform.
432                     log.warning(
433                             "Ignoring platform '%1$s': %2$s is not a valid number in %3$s.",
434                             platformFolder.getName(), PROP_VERSION_SDK,
435                             SdkConstants.FN_BUILD_PROP);
436                     return null;
437                 }
438             }
439 
440             // codename (optional)
441             String apiCodename = platformProp.get(PROP_VERSION_CODENAME);
442             if (apiCodename != null && apiCodename.equals("REL")) {
443                 apiCodename = null; // REL means it's a release version and therefore the
444                                     // codename is irrelevant at this point.
445             }
446 
447             // version string
448             String apiName = platformProp.get(PkgProps.PLATFORM_VERSION);
449             if (apiName == null) {
450                 apiName = platformProp.get(PROP_VERSION_RELEASE);
451             }
452             if (apiName == null) {
453                 log.warning(
454                         "Ignoring platform '%1$s': %2$s is missing from '%3$s'",
455                         platformFolder.getName(), PROP_VERSION_RELEASE,
456                         SdkConstants.FN_BUILD_PROP);
457                 return null;
458             }
459 
460             // platform rev number & layoutlib version are extracted from the source.properties
461             // saved by the SDK Manager when installing the package.
462 
463             int revision = 1;
464             LayoutlibVersion layoutlibVersion = null;
465             try {
466                 revision = Integer.parseInt(platformProp.get(PkgProps.PKG_REVISION));
467             } catch (NumberFormatException e) {
468                 // do nothing, we'll keep the default value of 1.
469             }
470 
471             try {
472                 String propApi = platformProp.get(PkgProps.LAYOUTLIB_API);
473                 String propRev = platformProp.get(PkgProps.LAYOUTLIB_REV);
474                 int llApi = propApi == null ? LayoutlibVersion.NOT_SPECIFIED :
475                                               Integer.parseInt(propApi);
476                 int llRev = propRev == null ? LayoutlibVersion.NOT_SPECIFIED :
477                                               Integer.parseInt(propRev);
478                 if (llApi > LayoutlibVersion.NOT_SPECIFIED &&
479                         llRev >= LayoutlibVersion.NOT_SPECIFIED) {
480                     layoutlibVersion = new LayoutlibVersion(llApi, llRev);
481                 }
482             } catch (NumberFormatException e) {
483                 // do nothing, we'll ignore the layoutlib version if it's invalid
484             }
485 
486             // api number and name look valid, perform a few more checks
487             if (checkPlatformContent(platformFolder, log) == false) {
488                 return null;
489             }
490 
491             ISystemImage[] systemImages =
492                 getPlatformSystemImages(sdkOsPath, platformFolder, apiNumber, apiCodename);
493 
494             // create the target.
495             PlatformTarget target = new PlatformTarget(
496                     sdkOsPath,
497                     platformFolder.getAbsolutePath(),
498                     apiNumber,
499                     apiCodename,
500                     apiName,
501                     revision,
502                     layoutlibVersion,
503                     systemImages,
504                     platformProp);
505 
506             // need to parse the skins.
507             String[] skins = parseSkinFolder(target.getPath(IAndroidTarget.SKINS));
508             target.setSkins(skins);
509 
510             return target;
511         } else {
512             log.warning("Ignoring platform '%1$s': %2$s is missing.",   //$NON-NLS-1$
513                     platformFolder.getName(),
514                     SdkConstants.FN_BUILD_PROP);
515         }
516 
517         return null;
518     }
519 
520     /**
521      * Get all the system images supported by an add-on target.
522      * For an add-on, we first look for sub-folders in the addon/images directory.
523      * If none are found but the directory exists and is not empty, assume it's a legacy
524      * arm eabi system image.
525      * <p/>
526      * Note that it's OK for an add-on to have no system-images at all, since it can always
527      * rely on the ones from its base platform.
528      *
529      * @param root Root of the add-on target being loaded.
530      * @return an array of ISystemImage containing all the system images for the target.
531      *              The list can be empty.
532     */
getAddonSystemImages(File root)533     private static ISystemImage[] getAddonSystemImages(File root) {
534         Set<ISystemImage> found = new TreeSet<ISystemImage>();
535 
536         root = new File(root, SdkConstants.OS_IMAGES_FOLDER);
537         File[] files = root.listFiles();
538         boolean hasImgFiles = false;
539 
540         if (files != null) {
541             // Look for sub-directories
542             for (File file : files) {
543                 if (file.isDirectory()) {
544                     found.add(new SystemImage(
545                             file,
546                             LocationType.IN_PLATFORM_SUBFOLDER,
547                             file.getName()));
548                 } else if (!hasImgFiles && file.isFile()) {
549                     if (file.getName().endsWith(".img")) {      //$NON-NLS-1$
550                         hasImgFiles = true;
551                     }
552                 }
553             }
554         }
555 
556         if (found.size() == 0 && hasImgFiles && root.isDirectory()) {
557             // We found no sub-folder system images but it looks like the top directory
558             // has some img files in it. It must be a legacy ARM EABI system image folder.
559             found.add(new SystemImage(
560                     root,
561                     LocationType.IN_PLATFORM_LEGACY,
562                     SdkConstants.ABI_ARMEABI));
563         }
564 
565         return found.toArray(new ISystemImage[found.size()]);
566     }
567 
568     /**
569      * Get all the system images supported by a platform target.
570      * For a platform, we first look in the new sdk/system-images folders then we
571      * look for sub-folders in the platform/images directory and/or the one legacy
572      * folder.
573      * If any given API appears twice or more, the first occurrence wins.
574      *
575      * @param sdkOsPath The path to the SDK.
576      * @param root Root of the platform target being loaded.
577      * @param apiNumber API level of platform being loaded
578      * @param apiCodename Optional codename of platform being loaded
579      * @return an array of ISystemImage containing all the system images for the target.
580      *              The list can be empty.
581     */
getPlatformSystemImages( String sdkOsPath, File root, int apiNumber, String apiCodename)582     private static ISystemImage[] getPlatformSystemImages(
583             String sdkOsPath,
584             File root,
585             int apiNumber,
586             String apiCodename) {
587         Set<ISystemImage> found = new TreeSet<ISystemImage>();
588         Set<String> abiFound = new HashSet<String>();
589 
590         // First look in the SDK/system-image/platform-n/abi folders.
591         // We require/enforce the system image to have a valid properties file.
592         // The actual directory names are irrelevant.
593         // If we find multiple occurrences of the same platform/abi, the first one read wins.
594 
595         AndroidVersion version = new AndroidVersion(apiNumber, apiCodename);
596 
597         File[] firstLevelFiles = new File(sdkOsPath, SdkConstants.FD_SYSTEM_IMAGES).listFiles();
598         if (firstLevelFiles != null) {
599             for (File firstLevel : firstLevelFiles) {
600                 File[] secondLevelFiles = firstLevel.listFiles();
601                 if (secondLevelFiles != null) {
602                     for (File secondLevel : secondLevelFiles) {
603                         try {
604                             File propFile = new File(secondLevel, SdkConstants.FN_SOURCE_PROP);
605                             Properties props = new Properties();
606                             FileInputStream fis = null;
607                             try {
608                                 fis = new FileInputStream(propFile);
609                                 props.load(fis);
610                             } finally {
611                                 if (fis != null) {
612                                     fis.close();
613                                 }
614                             }
615 
616                             AndroidVersion propsVersion = new AndroidVersion(props);
617                             if (!propsVersion.equals(version)) {
618                                 continue;
619                             }
620 
621                             String abi = props.getProperty(PkgProps.SYS_IMG_ABI);
622                             if (abi != null && !abiFound.contains(abi)) {
623                                 found.add(new SystemImage(
624                                         secondLevel,
625                                         LocationType.IN_SYSTEM_IMAGE,
626                                         abi));
627                                 abiFound.add(abi);
628                             }
629                         } catch (Exception ignore) {
630                         }
631                     }
632                 }
633             }
634         }
635 
636         // Then look in either the platform/images/abi or the legacy folder
637         root = new File(root, SdkConstants.OS_IMAGES_FOLDER);
638         File[] files = root.listFiles();
639         boolean useLegacy = true;
640         boolean hasImgFiles = false;
641 
642         if (files != null) {
643             // Look for sub-directories
644             for (File file : files) {
645                 if (file.isDirectory()) {
646                     useLegacy = false;
647                     String abi = file.getName();
648                     if (!abiFound.contains(abi)) {
649                         found.add(new SystemImage(
650                                 file,
651                                 LocationType.IN_PLATFORM_SUBFOLDER,
652                                 abi));
653                         abiFound.add(abi);
654                     }
655                 } else if (!hasImgFiles && file.isFile()) {
656                     if (file.getName().endsWith(".img")) {      //$NON-NLS-1$
657                         hasImgFiles = true;
658                     }
659                 }
660             }
661         }
662 
663         if (useLegacy && hasImgFiles && root.isDirectory() &&
664                 !abiFound.contains(SdkConstants.ABI_ARMEABI)) {
665             // We found no sub-folder system images but it looks like the top directory
666             // has some img files in it. It must be a legacy ARM EABI system image folder.
667             found.add(new SystemImage(
668                     root,
669                     LocationType.IN_PLATFORM_LEGACY,
670                     SdkConstants.ABI_ARMEABI));
671         }
672 
673         return found.toArray(new ISystemImage[found.size()]);
674     }
675 
676     /**
677      * Loads the Add-on from the SDK.
678      * Creates the "add-ons" folder if necessary.
679      *
680      * @param osSdkPath Location of the SDK
681      * @param list the list to fill with the add-ons.
682      * @param log the ISdkLog object receiving warning/error from the parsing. Cannot be null.
683      * @throws RuntimeException when the "add-ons" folder is missing and cannot be created.
684      */
loadAddOns(String osSdkPath, ArrayList<IAndroidTarget> list, ISdkLog log)685     private static void loadAddOns(String osSdkPath, ArrayList<IAndroidTarget> list, ISdkLog log) {
686         File addonFolder = new File(osSdkPath, SdkConstants.FD_ADDONS);
687 
688         if (addonFolder.isDirectory()) {
689             File[] addons  = addonFolder.listFiles();
690 
691             IAndroidTarget[] targetList = list.toArray(new IAndroidTarget[list.size()]);
692 
693             for (File addon : addons) {
694                 // Add-ons have to be folders. Ignore files and no need to warn about them.
695                 if (addon.isDirectory()) {
696                     AddOnTarget target = loadAddon(addon, targetList, log);
697                     if (target != null) {
698                         list.add(target);
699                     }
700                 }
701             }
702 
703             return;
704         }
705 
706         // Try to create it or complain if something else is in the way.
707         if (!addonFolder.exists()) {
708             if (!addonFolder.mkdir()) {
709                 throw new RuntimeException(
710                         String.format("Failed to create %1$s.",
711                                 addonFolder.getAbsolutePath()));
712             }
713         } else {
714             throw new RuntimeException(
715                     String.format("%1$s is not a folder.",
716                             addonFolder.getAbsolutePath()));
717         }
718     }
719 
720     /**
721      * Loads a specific Add-on at a given location.
722      * @param addonDir the location of the add-on directory.
723      * @param targetList The list of Android target that were already loaded from the SDK.
724      * @param log the ISdkLog object receiving warning/error from the parsing. Cannot be null.
725      */
loadAddon(File addonDir, IAndroidTarget[] targetList, ISdkLog log)726     private static AddOnTarget loadAddon(File addonDir,
727             IAndroidTarget[] targetList,
728             ISdkLog log) {
729 
730         // Parse the addon properties to ensure we can load it.
731         Pair<Map<String, String>, String> infos = parseAddonProperties(addonDir, targetList, log);
732 
733         Map<String, String> propertyMap = infos.getFirst();
734         String error = infos.getSecond();
735 
736         if (error != null) {
737             log.warning("Ignoring add-on '%1$s': %2$s", addonDir.getName(), error);
738             return null;
739         }
740 
741         // Since error==null we're not supposed to encounter any issues loading this add-on.
742         try {
743             assert propertyMap != null;
744 
745             String api = propertyMap.get(ADDON_API);
746             String name = propertyMap.get(ADDON_NAME);
747             String vendor = propertyMap.get(ADDON_VENDOR);
748 
749             assert api != null;
750             assert name != null;
751             assert vendor != null;
752 
753             PlatformTarget baseTarget = null;
754 
755             // Look for a platform that has a matching api level or codename.
756             for (IAndroidTarget target : targetList) {
757                 if (target.isPlatform() && target.getVersion().equals(api)) {
758                     baseTarget = (PlatformTarget)target;
759                     break;
760                 }
761             }
762 
763             assert baseTarget != null;
764 
765             // get the optional description
766             String description = propertyMap.get(ADDON_DESCRIPTION);
767 
768             // get the add-on revision
769             int revisionValue = 1;
770             String revision = propertyMap.get(ADDON_REVISION);
771             if (revision == null) {
772                 revision = propertyMap.get(ADDON_REVISION_OLD);
773             }
774             if (revision != null) {
775                 revisionValue = Integer.parseInt(revision);
776             }
777 
778             // get the optional libraries
779             String librariesValue = propertyMap.get(ADDON_LIBRARIES);
780             Map<String, String[]> libMap = null;
781 
782             if (librariesValue != null) {
783                 librariesValue = librariesValue.trim();
784                 if (librariesValue.length() > 0) {
785                     // split in the string into the libraries name
786                     String[] libraries = librariesValue.split(";");                    //$NON-NLS-1$
787                     if (libraries.length > 0) {
788                         libMap = new HashMap<String, String[]>();
789                         for (String libName : libraries) {
790                             libName = libName.trim();
791 
792                             // get the library data from the properties
793                             String libData = propertyMap.get(libName);
794 
795                             if (libData != null) {
796                                 // split the jar file from the description
797                                 Matcher m = PATTERN_LIB_DATA.matcher(libData);
798                                 if (m.matches()) {
799                                     libMap.put(libName, new String[] {
800                                             m.group(1), m.group(2) });
801                                 } else {
802                                     log.warning(
803                                             "Ignoring library '%1$s', property value has wrong format\n\t%2$s",
804                                             libName, libData);
805                                 }
806                             } else {
807                                 log.warning(
808                                         "Ignoring library '%1$s', missing property value",
809                                         libName, libData);
810                             }
811                         }
812                     }
813                 }
814             }
815 
816             // get the abi list.
817             ISystemImage[] systemImages = getAddonSystemImages(addonDir);
818 
819             // check whether the add-on provides its own rendering info/library.
820             boolean hasRenderingLibrary = false;
821             boolean hasRenderingResources = false;
822 
823             File dataFolder = new File(addonDir, SdkConstants.FD_DATA);
824             if (dataFolder.isDirectory()) {
825                 hasRenderingLibrary = new File(dataFolder, SdkConstants.FN_LAYOUTLIB_JAR).isFile();
826                 hasRenderingResources = new File(dataFolder, SdkConstants.FD_RES).isDirectory() &&
827                         new File(dataFolder, SdkConstants.FD_FONTS).isDirectory();
828             }
829 
830             AddOnTarget target = new AddOnTarget(addonDir.getAbsolutePath(), name, vendor,
831                     revisionValue, description, systemImages, libMap,
832                     hasRenderingLibrary, hasRenderingResources,baseTarget);
833 
834             // need to parse the skins.
835             String[] skins = parseSkinFolder(target.getPath(IAndroidTarget.SKINS));
836 
837             // get the default skin, or take it from the base platform if needed.
838             String defaultSkin = propertyMap.get(ADDON_DEFAULT_SKIN);
839             if (defaultSkin == null) {
840                 if (skins.length == 1) {
841                     defaultSkin = skins[0];
842                 } else {
843                     defaultSkin = baseTarget.getDefaultSkin();
844                 }
845             }
846 
847             // get the USB ID (if available)
848             int usbVendorId = convertId(propertyMap.get(ADDON_USB_VENDOR));
849             if (usbVendorId != IAndroidTarget.NO_USB_ID) {
850                 target.setUsbVendorId(usbVendorId);
851             }
852 
853             target.setSkins(skins, defaultSkin);
854 
855             return target;
856         }
857         catch (Exception e) {
858             log.warning("Ignoring add-on '%1$s': error %2$s.",
859                     addonDir.getName(), e.toString());
860         }
861 
862         return null;
863     }
864 
865     /**
866      * Parses the add-on properties and decodes any error that occurs when loading an addon.
867      *
868      * @param addonDir the location of the addon directory.
869      * @param targetList The list of Android target that were already loaded from the SDK.
870      * @param log the ISdkLog object receiving warning/error from the parsing. Cannot be null.
871      * @return A pair with the property map and an error string. Both can be null but not at the
872      *  same time. If a non-null error is present then the property map must be ignored. The error
873      *  should be translatable as it might show up in the SdkManager UI.
874      */
parseAddonProperties( File addonDir, IAndroidTarget[] targetList, ISdkLog log)875     public static Pair<Map<String, String>, String> parseAddonProperties(
876             File addonDir,
877             IAndroidTarget[] targetList,
878             ISdkLog log) {
879         Map<String, String> propertyMap = null;
880         String error = null;
881 
882         FileWrapper addOnManifest = new FileWrapper(addonDir, SdkConstants.FN_MANIFEST_INI);
883 
884         do {
885             if (!addOnManifest.isFile()) {
886                 error = String.format("File not found: %1$s", SdkConstants.FN_MANIFEST_INI);
887                 break;
888             }
889 
890             propertyMap = ProjectProperties.parsePropertyFile(addOnManifest, log);
891             if (propertyMap == null) {
892                 error = String.format("Failed to parse properties from %1$s",
893                         SdkConstants.FN_MANIFEST_INI);
894                 break;
895             }
896 
897             // look for some specific values in the map.
898             // we require name, vendor, and api
899             String name = propertyMap.get(ADDON_NAME);
900             if (name == null) {
901                 error = addonManifestWarning(ADDON_NAME);
902                 break;
903             }
904 
905             String vendor = propertyMap.get(ADDON_VENDOR);
906             if (vendor == null) {
907                 error = addonManifestWarning(ADDON_VENDOR);
908                 break;
909             }
910 
911             String api = propertyMap.get(ADDON_API);
912             PlatformTarget baseTarget = null;
913             if (api == null) {
914                 error = addonManifestWarning(ADDON_API);
915                 break;
916             }
917 
918             // Look for a platform that has a matching api level or codename.
919             for (IAndroidTarget target : targetList) {
920                 if (target.isPlatform() && target.getVersion().equals(api)) {
921                     baseTarget = (PlatformTarget)target;
922                     break;
923                 }
924             }
925 
926             if (baseTarget == null) {
927                 error = String.format("Unable to find base platform with API level '%1$s'", api);
928                 break;
929             }
930 
931             // get the add-on revision
932             String revision = propertyMap.get(ADDON_REVISION);
933             if (revision == null) {
934                 revision = propertyMap.get(ADDON_REVISION_OLD);
935             }
936             if (revision != null) {
937                 try {
938                     Integer.parseInt(revision);
939                 } catch (NumberFormatException e) {
940                     // looks like revision does not parse to a number.
941                     error = String.format("%1$s is not a valid number in %2$s.",
942                                 ADDON_REVISION, SdkConstants.FN_BUILD_PROP);
943                     break;
944                 }
945             }
946 
947         } while(false);
948 
949         return Pair.of(propertyMap, error);
950     }
951 
952     /**
953      * Converts a string representation of an hexadecimal ID into an int.
954      * @param value the string to convert.
955      * @return the int value, or {@link IAndroidTarget#NO_USB_ID} if the convertion failed.
956      */
convertId(String value)957     private static int convertId(String value) {
958         if (value != null && value.length() > 0) {
959             if (PATTERN_USB_IDS.matcher(value).matches()) {
960                 String v = value.substring(2);
961                 try {
962                     return Integer.parseInt(v, 16);
963                 } catch (NumberFormatException e) {
964                     // this shouldn't happen since we check the pattern above, but this is safer.
965                     // the method will return 0 below.
966                 }
967             }
968         }
969 
970         return IAndroidTarget.NO_USB_ID;
971     }
972 
973     /**
974      * Prepares a warning about the addon being ignored due to a missing manifest value.
975      * This string will show up in the SdkManager UI.
976      *
977      * @param valueName The missing manifest value, for display.
978      */
addonManifestWarning(String valueName)979     private static String addonManifestWarning(String valueName) {
980         return String.format("'%1$s' is missing from %2$s.",
981                 valueName, SdkConstants.FN_MANIFEST_INI);
982     }
983 
984     /**
985      * Checks the given platform has all the required files, and returns true if they are all
986      * present.
987      * <p/>This checks the presence of the following files: android.jar, framework.aidl, aapt(.exe),
988      * aidl(.exe), dx(.bat), and dx.jar
989      *
990      * @param platform The folder containing the platform.
991      * @param log Logger. Cannot be null.
992      */
checkPlatformContent(File platform, ISdkLog log)993     private static boolean checkPlatformContent(File platform, ISdkLog log) {
994         for (String relativePath : sPlatformContentList) {
995             File f = new File(platform, relativePath);
996             if (!f.exists()) {
997                 log.warning(
998                         "Ignoring platform '%1$s': %2$s is missing.",                  //$NON-NLS-1$
999                         platform.getName(), relativePath);
1000                 return false;
1001             }
1002         }
1003         return true;
1004     }
1005 
1006 
1007 
1008     /**
1009      * Parses the skin folder and builds the skin list.
1010      * @param osPath The path of the skin root folder.
1011      */
parseSkinFolder(String osPath)1012     private static String[] parseSkinFolder(String osPath) {
1013         File skinRootFolder = new File(osPath);
1014 
1015         if (skinRootFolder.isDirectory()) {
1016             ArrayList<String> skinList = new ArrayList<String>();
1017 
1018             File[] files = skinRootFolder.listFiles();
1019 
1020             for (File skinFolder : files) {
1021                 if (skinFolder.isDirectory()) {
1022                     // check for layout file
1023                     File layout = new File(skinFolder, SdkConstants.FN_SKIN_LAYOUT);
1024 
1025                     if (layout.isFile()) {
1026                         // for now we don't parse the content of the layout and
1027                         // simply add the directory to the list.
1028                         skinList.add(skinFolder.getName());
1029                     }
1030                 }
1031             }
1032 
1033             return skinList.toArray(new String[skinList.size()]);
1034         }
1035 
1036         return new String[0];
1037     }
1038 
1039     /**
1040      * Initialize the sample folders for all known targets (platforms and addons).
1041      * <p/>
1042      * Samples used to be located at SDK/Target/samples. We then changed this to
1043      * have a separate SDK/samples/samples-API directory. This parses either directories
1044      * and sets the targets' sample path accordingly.
1045      *
1046      * @param log Logger. Cannot be null.
1047      */
initializeSamplePaths(ISdkLog log)1048     private void initializeSamplePaths(ISdkLog log) {
1049         File sampleFolder = new File(mOsSdkPath, SdkConstants.FD_SAMPLES);
1050         if (sampleFolder.isDirectory()) {
1051             File[] platforms  = sampleFolder.listFiles();
1052 
1053             for (File platform : platforms) {
1054                 if (platform.isDirectory()) {
1055                     // load the source.properties file and get an AndroidVersion object from it.
1056                     AndroidVersion version = getSamplesVersion(platform, log);
1057 
1058                     if (version != null) {
1059                         // locate the platform matching this version
1060                         for (IAndroidTarget target : mTargets) {
1061                             if (target.isPlatform() && target.getVersion().equals(version)) {
1062                                 ((PlatformTarget)target).setSamplesPath(platform.getAbsolutePath());
1063                                 break;
1064                             }
1065                         }
1066                     }
1067                 }
1068             }
1069         }
1070     }
1071 
1072     /**
1073      * Returns the {@link AndroidVersion} of the sample in the given folder.
1074      *
1075      * @param folder The sample's folder.
1076      * @param log Logger for errors. Cannot be null.
1077      * @return An {@link AndroidVersion} or null on error.
1078      */
getSamplesVersion(File folder, ISdkLog log)1079     private AndroidVersion getSamplesVersion(File folder, ISdkLog log) {
1080         File sourceProp = new File(folder, SdkConstants.FN_SOURCE_PROP);
1081         try {
1082             Properties p = new Properties();
1083             FileInputStream fis = null;
1084             try {
1085                 fis = new FileInputStream(sourceProp);
1086                 p.load(fis);
1087             } finally {
1088                 if (fis != null) {
1089                     fis.close();
1090                 }
1091             }
1092 
1093             return new AndroidVersion(p);
1094         } catch (FileNotFoundException e) {
1095             log.warning("Ignoring sample '%1$s': does not contain %2$s.",              //$NON-NLS-1$
1096                     folder.getName(), SdkConstants.FN_SOURCE_PROP);
1097         } catch (IOException e) {
1098             log.warning("Ignoring sample '%1$s': failed reading %2$s.",                //$NON-NLS-1$
1099                     folder.getName(), SdkConstants.FN_SOURCE_PROP);
1100         } catch (AndroidVersionException e) {
1101             log.warning("Ignoring sample '%1$s': no android version found in %2$s.",   //$NON-NLS-1$
1102                     folder.getName(), SdkConstants.FN_SOURCE_PROP);
1103         }
1104 
1105         return null;
1106     }
1107 
1108 }
1109