OSDN Git Service

42493651b75716bd07fef9951d0b3bdbf6c1d3ca
[android-x86/sdk.git] / sdkmanager / libs / sdklib / src / com / android / sdklib / SdkManager.java
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.prefs.AndroidLocation;
20 import com.android.prefs.AndroidLocation.AndroidLocationException;
21 import com.android.sdklib.AndroidVersion.AndroidVersionException;
22 import com.android.sdklib.internal.project.ProjectProperties;
23
24 import java.io.File;
25 import java.io.FileInputStream;
26 import java.io.FileNotFoundException;
27 import java.io.FileWriter;
28 import java.io.IOException;
29 import java.util.ArrayList;
30 import java.util.Collections;
31 import java.util.HashMap;
32 import java.util.HashSet;
33 import java.util.Map;
34 import java.util.Properties;
35 import java.util.regex.Matcher;
36 import java.util.regex.Pattern;
37
38 /**
39  * The SDK manager parses the SDK folder and gives access to the content.
40  * @see PlatformTarget
41  * @see AddOnTarget
42  */
43 public final class SdkManager {
44
45     public final static String PROP_VERSION_SDK = "ro.build.version.sdk";
46     public final static String PROP_VERSION_CODENAME = "ro.build.version.codename";
47     public final static String PROP_VERSION_RELEASE = "ro.build.version.release";
48
49     private final static String ADDON_NAME = "name";
50     private final static String ADDON_VENDOR = "vendor";
51     private final static String ADDON_API = "api";
52     private final static String ADDON_DESCRIPTION = "description";
53     private final static String ADDON_LIBRARIES = "libraries";
54     private final static String ADDON_DEFAULT_SKIN = "skin";
55     private final static String ADDON_USB_VENDOR = "usb-vendor";
56     private final static String ADDON_REVISION = "revision";
57     private final static String ADDON_REVISION_OLD = "version";
58
59
60     private final static Pattern PATTERN_LIB_DATA = Pattern.compile(
61             "^([a-zA-Z0-9._-]+\\.jar);(.*)$", Pattern.CASE_INSENSITIVE);
62
63      // usb ids are 16-bit hexadecimal values.
64     private final static Pattern PATTERN_USB_IDS = Pattern.compile(
65             "^0x[a-f0-9]{4}$", Pattern.CASE_INSENSITIVE);
66
67     /** List of items in the platform to check when parsing it. These paths are relative to the
68      * platform root folder. */
69     private final static String[] sPlatformContentList = new String[] {
70         SdkConstants.FN_FRAMEWORK_LIBRARY,
71         SdkConstants.FN_FRAMEWORK_AIDL,
72         SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_AAPT,
73         SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_AIDL,
74         SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_DX,
75         SdkConstants.OS_SDK_TOOLS_LIB_FOLDER + SdkConstants.FN_DX_JAR,
76     };
77
78     /** Preference file containing the usb ids for adb */
79     private final static String ADB_INI_FILE = "adb_usb.ini";
80        //0--------90--------90--------90--------90--------90--------90--------90--------9
81     private final static String ADB_INI_HEADER =
82         "# ANDROID 3RD PARTY USB VENDOR ID LIST -- DO NOT EDIT.\n" +
83         "# USE 'android update adb' TO GENERATE.\n" +
84         "# 1 USB VENDOR ID PER LINE.\n";
85
86     /** the location of the SDK */
87     private final String mSdkLocation;
88     private IAndroidTarget[] mTargets;
89
90     /**
91      * Create a new {@link SdkManager} instance.
92      * External users should use {@link #createManager(String, ISdkLog)}.
93      *
94      * @param sdkLocation the location of the SDK.
95      */
96     private SdkManager(String sdkLocation) {
97         mSdkLocation = sdkLocation;
98     }
99
100     /**
101      * Creates an {@link SdkManager} for a given sdk location.
102      * @param sdkLocation the location of the SDK.
103      * @param log the ISdkLog object receiving warning/error from the parsing. Cannot be null.
104      * @return the created {@link SdkManager} or null if the location is not valid.
105      */
106     public static SdkManager createManager(String sdkLocation, ISdkLog log) {
107         try {
108             SdkManager manager = new SdkManager(sdkLocation);
109             ArrayList<IAndroidTarget> list = new ArrayList<IAndroidTarget>();
110             loadPlatforms(sdkLocation, list, log);
111             loadAddOns(sdkLocation, list, log);
112
113             // sort the targets/add-ons
114             Collections.sort(list);
115
116             manager.setTargets(list.toArray(new IAndroidTarget[list.size()]));
117
118             // load the samples, after the targets have been set.
119             manager.loadSamples(log);
120
121             return manager;
122         } catch (IllegalArgumentException e) {
123             log.error(e, "Error parsing the sdk.");
124         }
125
126         return null;
127     }
128
129     /**
130      * Returns the location of the SDK.
131      */
132     public String getLocation() {
133         return mSdkLocation;
134     }
135
136     /**
137      * Returns the targets that are available in the SDK.
138      * <p/>
139      * The array can be empty but not null.
140      */
141     public IAndroidTarget[] getTargets() {
142         return mTargets;
143     }
144
145     /**
146      * Sets the targets that are available in the SDK.
147      * <p/>
148      * The array can be empty but not null.
149      */
150     private void setTargets(IAndroidTarget[] targets) {
151         assert targets != null;
152         mTargets = targets;
153     }
154
155     /**
156      * Returns a target from a hash that was generated by {@link IAndroidTarget#hashString()}.
157      *
158      * @param hash the {@link IAndroidTarget} hash string.
159      * @return The matching {@link IAndroidTarget} or null.
160      */
161     public IAndroidTarget getTargetFromHashString(String hash) {
162         if (hash != null) {
163             for (IAndroidTarget target : mTargets) {
164                 if (hash.equals(target.hashString())) {
165                     return target;
166                 }
167             }
168         }
169
170         return null;
171     }
172
173     /**
174      * Updates adb with the USB devices declared in the SDK add-ons.
175      * @throws AndroidLocationException
176      * @throws IOException
177      */
178     public void updateAdb() throws AndroidLocationException, IOException {
179         FileWriter writer = null;
180         try {
181             // get the android prefs location to know where to write the file.
182             File adbIni = new File(AndroidLocation.getFolder(), ADB_INI_FILE);
183             writer = new FileWriter(adbIni);
184
185             // first, put all the vendor id in an HashSet to remove duplicate.
186             HashSet<Integer> set = new HashSet<Integer>();
187             IAndroidTarget[] targets = getTargets();
188             for (IAndroidTarget target : targets) {
189                 if (target.getUsbVendorId() != IAndroidTarget.NO_USB_ID) {
190                     set.add(target.getUsbVendorId());
191                 }
192             }
193
194             // write file header.
195             writer.write(ADB_INI_HEADER);
196
197             // now write the Id in a text file, one per line.
198             for (Integer i : set) {
199                 writer.write(String.format("0x%04x\n", i));
200             }
201         } finally {
202             if (writer != null) {
203                 writer.close();
204             }
205         }
206     }
207
208     /**
209      * Reloads the content of the SDK.
210      *
211      * @param log the ISdkLog object receiving warning/error from the parsing. Cannot be null.
212      */
213     public void reloadSdk(ISdkLog log) {
214         // get the current target list.
215         ArrayList<IAndroidTarget> list = new ArrayList<IAndroidTarget>();
216         loadPlatforms(mSdkLocation, list, log);
217         loadAddOns(mSdkLocation, list, log);
218
219         // For now replace the old list with the new one.
220         // In the future we may want to keep the current objects, so that ADT doesn't have to deal
221         // with new IAndroidTarget objects when a target didn't actually change.
222
223         // sort the targets/add-ons
224         Collections.sort(list);
225         setTargets(list.toArray(new IAndroidTarget[list.size()]));
226
227         // load the samples, after the targets have been set.
228         loadSamples(log);
229     }
230
231     /**
232      * Loads the Platforms from the SDK.
233      * @param location Location of the SDK
234      * @param list the list to fill with the platforms.
235      * @param log the ISdkLog object receiving warning/error from the parsing. Cannot be null.
236      */
237     private static void loadPlatforms(String location, ArrayList<IAndroidTarget> list,
238             ISdkLog log) {
239         File platformFolder = new File(location, SdkConstants.FD_PLATFORMS);
240         if (platformFolder.isDirectory()) {
241             File[] platforms  = platformFolder.listFiles();
242
243             for (File platform : platforms) {
244                 if (platform.isDirectory()) {
245                     PlatformTarget target = loadPlatform(platform, log);
246                     if (target != null) {
247                         list.add(target);
248                     }
249                 } else {
250                     log.warning("Ignoring platform '%1$s', not a folder.", platform.getName());
251                 }
252             }
253
254             return;
255         }
256
257         String message = null;
258         if (platformFolder.exists() == false) {
259             message = "%s is missing.";
260         } else {
261             message = "%s is not a folder.";
262         }
263
264         throw new IllegalArgumentException(String.format(message,
265                 platformFolder.getAbsolutePath()));
266     }
267
268     /**
269      * Loads a specific Platform at a given location.
270      * @param platform the location of the platform.
271      * @param log the ISdkLog object receiving warning/error from the parsing. Cannot be null.
272      */
273     private static PlatformTarget loadPlatform(File platform, ISdkLog log) {
274         File buildProp = new File(platform, SdkConstants.FN_BUILD_PROP);
275
276         if (buildProp.isFile()) {
277             Map<String, String> map = ProjectProperties.parsePropertyFile(buildProp, log);
278
279             if (map != null) {
280                 // look for some specific values in the map.
281
282                 // version string
283                 String apiName = map.get(PROP_VERSION_RELEASE);
284                 if (apiName == null) {
285                     log.warning(null,
286                             "Ignoring platform '%1$s': %2$s is missing from '%3$s'",
287                             platform.getName(), PROP_VERSION_RELEASE,
288                             SdkConstants.FN_BUILD_PROP);
289                     return null;
290                 }
291
292                 // api level
293                 int apiNumber;
294                 String stringValue = map.get(PROP_VERSION_SDK);
295                 if (stringValue == null) {
296                     log.warning(null,
297                             "Ignoring platform '%1$s': %2$s is missing from '%3$s'",
298                             platform.getName(), PROP_VERSION_SDK,
299                             SdkConstants.FN_BUILD_PROP);
300                     return null;
301                 } else {
302                     try {
303                          apiNumber = Integer.parseInt(stringValue);
304                     } catch (NumberFormatException e) {
305                         // looks like apiNumber does not parse to a number.
306                         // Ignore this platform.
307                         log.warning(null,
308                                 "Ignoring platform '%1$s': %2$s is not a valid number in %3$s.",
309                                 platform.getName(), PROP_VERSION_SDK,
310                                 SdkConstants.FN_BUILD_PROP);
311                         return null;
312                     }
313                 }
314
315                 // codename (optional)
316                 String apiCodename = map.get(PROP_VERSION_CODENAME);
317                 if (apiCodename != null && apiCodename.equals("REL")) {
318                     apiCodename = null; // REL means it's a release version and therefore the
319                                         // codename is irrelevant at this point.
320                 }
321
322                 // platform rev number
323                 int revision = 1;
324                 File sourcePropFile = new File(platform, SdkConstants.FN_SOURCE_PROP);
325                 Map<String, String> sourceProp = ProjectProperties.parsePropertyFile(sourcePropFile,
326                         log);
327                 if (sourceProp != null) {
328                     try {
329                         revision = Integer.parseInt(sourceProp.get("Pkg.Revision"));
330                     } catch (NumberFormatException e) {
331                         // do nothing, we'll keep the default value of 1.
332                     }
333                     map.putAll(sourceProp);
334                 }
335
336                 // Ant properties
337                 File sdkPropFile = new File(platform, SdkConstants.FN_SDK_PROP);
338                 Map<String, String> antProp = ProjectProperties.parsePropertyFile(sdkPropFile, log);
339                 if (antProp != null) {
340                     map.putAll(antProp);
341                 }
342
343                 // api number and name look valid, perform a few more checks
344                 if (checkPlatformContent(platform, log) == false) {
345                     return null;
346                 }
347                 // create the target.
348                 PlatformTarget target = new PlatformTarget(
349                         platform.getAbsolutePath(),
350                         map,
351                         apiNumber,
352                         apiCodename,
353                         apiName,
354                         revision);
355
356                 // need to parse the skins.
357                 String[] skins = parseSkinFolder(target.getPath(IAndroidTarget.SKINS));
358                 target.setSkins(skins);
359
360                 return target;
361             }
362         } else {
363             log.warning(null, "Ignoring platform '%1$s': %2$s is missing.", platform.getName(),
364                     SdkConstants.FN_BUILD_PROP);
365         }
366
367         return null;
368     }
369
370
371     /**
372      * Loads the Add-on from the SDK.
373      * @param location Location of the SDK
374      * @param list the list to fill with the add-ons.
375      * @param log the ISdkLog object receiving warning/error from the parsing. Cannot be null.
376      */
377     private static void loadAddOns(String location, ArrayList<IAndroidTarget> list, ISdkLog log) {
378         File addonFolder = new File(location, SdkConstants.FD_ADDONS);
379         if (addonFolder.isDirectory()) {
380             File[] addons  = addonFolder.listFiles();
381
382             for (File addon : addons) {
383                 // Add-ons have to be folders. Ignore files and no need to warn about them.
384                 if (addon.isDirectory()) {
385                     AddOnTarget target = loadAddon(addon, list, log);
386                     if (target != null) {
387                         list.add(target);
388                     }
389                 }
390             }
391
392             return;
393         }
394
395         String message = null;
396         if (addonFolder.exists() == false) {
397             message = "%s is missing.";
398         } else {
399             message = "%s is not a folder.";
400         }
401
402         throw new IllegalArgumentException(String.format(message,
403                 addonFolder.getAbsolutePath()));
404     }
405
406     /**
407      * Loads a specific Add-on at a given location.
408      * @param addon the location of the addon.
409      * @param targetList The list of Android target that were already loaded from the SDK.
410      * @param log the ISdkLog object receiving warning/error from the parsing. Cannot be null.
411      */
412     private static AddOnTarget loadAddon(File addon, ArrayList<IAndroidTarget> targetList,
413             ISdkLog log) {
414         File addOnManifest = new File(addon, SdkConstants.FN_MANIFEST_INI);
415
416         if (addOnManifest.isFile()) {
417             Map<String, String> propertyMap = ProjectProperties.parsePropertyFile(addOnManifest,
418                     log);
419
420             if (propertyMap != null) {
421                 // look for some specific values in the map.
422                 // we require name, vendor, and api
423                 String name = propertyMap.get(ADDON_NAME);
424                 if (name == null) {
425                     displayAddonManifestWarning(log, addon.getName(), ADDON_NAME);
426                     return null;
427                 }
428
429                 String vendor = propertyMap.get(ADDON_VENDOR);
430                 if (vendor == null) {
431                     displayAddonManifestWarning(log, addon.getName(), ADDON_VENDOR);
432                     return null;
433                 }
434
435                 String api = propertyMap.get(ADDON_API);
436                 PlatformTarget baseTarget = null;
437                 if (api == null) {
438                     displayAddonManifestWarning(log, addon.getName(), ADDON_API);
439                     return null;
440                 } else {
441                     // Look for a platform that has a matching api level or codename.
442                     for (IAndroidTarget target : targetList) {
443                         if (target.isPlatform() && target.getVersion().equals(api)) {
444                             baseTarget = (PlatformTarget)target;
445                             break;
446                         }
447                     }
448
449                     if (baseTarget == null) {
450                         // Ignore this add-on.
451                         log.warning(null,
452                                 "Ignoring add-on '%1$s': Unable to find base platform with API level '%2$s'",
453                                 addon.getName(), api);
454                         return null;
455                     }
456                 }
457
458                 // get the optional description
459                 String description = propertyMap.get(ADDON_DESCRIPTION);
460
461                 // get the add-on revision
462                 int revisionValue = 1;
463                 String revision = propertyMap.get(ADDON_REVISION);
464                 if (revision == null) {
465                     revision = propertyMap.get(ADDON_REVISION_OLD);
466                 }
467                 if (revision != null) {
468                     try {
469                         revisionValue = Integer.parseInt(revision);
470                     } catch (NumberFormatException e) {
471                         // looks like apiNumber does not parse to a number.
472                         // Ignore this add-on.
473                         log.warning(null,
474                                 "Ignoring add-on '%1$s': %2$s is not a valid number in %3$s.",
475                                 addon.getName(), ADDON_REVISION, SdkConstants.FN_BUILD_PROP);
476                         return null;
477                     }
478                 }
479
480                 // get the optional libraries
481                 String librariesValue = propertyMap.get(ADDON_LIBRARIES);
482                 Map<String, String[]> libMap = null;
483
484                 if (librariesValue != null) {
485                     librariesValue = librariesValue.trim();
486                     if (librariesValue.length() > 0) {
487                         // split in the string into the libraries name
488                         String[] libraries = librariesValue.split(";");
489                         if (libraries.length > 0) {
490                             libMap = new HashMap<String, String[]>();
491                             for (String libName : libraries) {
492                                 libName = libName.trim();
493
494                                 // get the library data from the properties
495                                 String libData = propertyMap.get(libName);
496
497                                 if (libData != null) {
498                                     // split the jar file from the description
499                                     Matcher m = PATTERN_LIB_DATA.matcher(libData);
500                                     if (m.matches()) {
501                                         libMap.put(libName, new String[] {
502                                                 m.group(1), m.group(2) });
503                                     } else {
504                                         log.warning(null,
505                                                 "Ignoring library '%1$s', property value has wrong format\n\t%2$s",
506                                                 libName, libData);
507                                     }
508                                 } else {
509                                     log.warning(null,
510                                             "Ignoring library '%1$s', missing property value",
511                                             libName, libData);
512                                 }
513                             }
514                         }
515                     }
516                 }
517
518                 AddOnTarget target = new AddOnTarget(addon.getAbsolutePath(), name, vendor,
519                         revisionValue, description, libMap, baseTarget);
520
521                 // need to parse the skins.
522                 String[] skins = parseSkinFolder(target.getPath(IAndroidTarget.SKINS));
523
524                 // get the default skin, or take it from the base platform if needed.
525                 String defaultSkin = propertyMap.get(ADDON_DEFAULT_SKIN);
526                 if (defaultSkin == null) {
527                     if (skins.length == 1) {
528                         defaultSkin = skins[0];
529                     } else {
530                         defaultSkin = baseTarget.getDefaultSkin();
531                     }
532                 }
533
534                 // get the USB ID (if available)
535                 int usbVendorId = convertId(propertyMap.get(ADDON_USB_VENDOR));
536                 if (usbVendorId != IAndroidTarget.NO_USB_ID) {
537                     target.setUsbVendorId(usbVendorId);
538                 }
539
540                 target.setSkins(skins, defaultSkin);
541
542                 return target;
543             }
544         } else {
545             log.warning(null, "Ignoring add-on '%1$s': %2$s is missing.", addon.getName(),
546                     SdkConstants.FN_MANIFEST_INI);
547         }
548
549         return null;
550     }
551
552     /**
553      * Converts a string representation of an hexadecimal ID into an int.
554      * @param value the string to convert.
555      * @return the int value, or {@link IAndroidTarget#NO_USB_ID} if the convertion failed.
556      */
557     private static int convertId(String value) {
558         if (value != null && value.length() > 0) {
559             if (PATTERN_USB_IDS.matcher(value).matches()) {
560                 String v = value.substring(2);
561                 try {
562                     return Integer.parseInt(v, 16);
563                 } catch (NumberFormatException e) {
564                     // this shouldn't happen since we check the pattern above, but this is safer.
565                     // the method will return 0 below.
566                 }
567             }
568         }
569
570         return IAndroidTarget.NO_USB_ID;
571     }
572
573     /**
574      * Displays a warning in the log about the addon being ignored due to a missing manifest value.
575      *
576      * @param log The logger object. Cannot be null.
577      * @param addonName The addon name, for display.
578      * @param valueName The missing manifest value, for display.
579      */
580     private static void displayAddonManifestWarning(ISdkLog log, String addonName, String valueName) {
581         log.warning(null, "Ignoring add-on '%1$s': '%2$s' is missing from %3$s.",
582                 addonName, valueName, SdkConstants.FN_MANIFEST_INI);
583     }
584
585     /**
586      * Checks the given platform has all the required files, and returns true if they are all
587      * present.
588      * <p/>This checks the presence of the following files: android.jar, framework.aidl, aapt(.exe),
589      * aidl(.exe), dx(.bat), and dx.jar
590      *
591      * @param platform The folder containing the platform.
592      * @param log Logger. Cannot be null.
593      */
594     private static boolean checkPlatformContent(File platform, ISdkLog log) {
595         for (String relativePath : sPlatformContentList) {
596             File f = new File(platform, relativePath);
597             if (!f.exists()) {
598                 log.warning(null,
599                         "Ignoring platform '%1$s': %2$s is missing.",
600                         platform.getName(), relativePath);
601                 return false;
602             }
603         }
604         return true;
605     }
606
607
608
609     /**
610      * Parses the skin folder and builds the skin list.
611      * @param osPath The path of the skin root folder.
612      */
613     private static String[] parseSkinFolder(String osPath) {
614         File skinRootFolder = new File(osPath);
615
616         if (skinRootFolder.isDirectory()) {
617             ArrayList<String> skinList = new ArrayList<String>();
618
619             File[] files = skinRootFolder.listFiles();
620
621             for (File skinFolder : files) {
622                 if (skinFolder.isDirectory()) {
623                     // check for layout file
624                     File layout = new File(skinFolder, SdkConstants.FN_SKIN_LAYOUT);
625
626                     if (layout.isFile()) {
627                         // for now we don't parse the content of the layout and
628                         // simply add the directory to the list.
629                         skinList.add(skinFolder.getName());
630                     }
631                 }
632             }
633
634             return skinList.toArray(new String[skinList.size()]);
635         }
636
637         return new String[0];
638     }
639
640     /**
641      * Loads all samples from the {@link SdkConstants#FD_SAMPLES} directory.
642      *
643      * @param log Logger. Cannot be null.
644      */
645     private void loadSamples(ISdkLog log) {
646         File sampleFolder = new File(mSdkLocation, SdkConstants.FD_SAMPLES);
647         if (sampleFolder.isDirectory()) {
648             File[] platforms  = sampleFolder.listFiles();
649
650             for (File platform : platforms) {
651                 if (platform.isDirectory()) {
652                     // load the source.properties file and get an AndroidVersion object from it.
653                     AndroidVersion version = getSamplesVersion(platform, log);
654
655                     if (version != null) {
656                         // locate the platform matching this version
657                         for (IAndroidTarget target : mTargets) {
658                             if (target.isPlatform() && target.getVersion().equals(version)) {
659                                 ((PlatformTarget)target).setSamplesPath(platform.getAbsolutePath());
660                                 break;
661                             }
662                         }
663                     }
664                 }
665             }
666         }
667     }
668
669     /**
670      * Returns the {@link AndroidVersion} of the sample in the given folder.
671      *
672      * @param folder The sample's folder.
673      * @param log Logger for errors. Cannot be null.
674      * @return An {@link AndroidVersion} or null on error.
675      */
676     private AndroidVersion getSamplesVersion(File folder, ISdkLog log) {
677         File sourceProp = new File(folder, SdkConstants.FN_SOURCE_PROP);
678         try {
679             Properties p = new Properties();
680             p.load(new FileInputStream(sourceProp));
681
682             return new AndroidVersion(p);
683         } catch (FileNotFoundException e) {
684             log.warning("Ignoring sample '%1$s': does not contain %2$s.", //$NON-NLS-1$
685                     folder.getName(), SdkConstants.FN_SOURCE_PROP);
686         } catch (IOException e) {
687             log.warning("Ignoring sample '%1$s': failed reading %2$s.", //$NON-NLS-1$
688                     folder.getName(), SdkConstants.FN_SOURCE_PROP);
689         } catch (AndroidVersionException e) {
690             log.warning("Ignoring sample '%1$s': no android version found in %2$s.", //$NON-NLS-1$
691                     folder.getName(), SdkConstants.FN_SOURCE_PROP);
692         }
693
694         return null;
695     }
696
697 }