OSDN Git Service

Merge: Support broken addons in SDK Manager UI.
authorRaphael Moll <ralf@android.com>
Sun, 2 Jan 2011 21:36:40 +0000 (13:36 -0800)
committerRaphael Moll <ralf@android.com>
Sat, 8 Jan 2011 06:53:06 +0000 (22:53 -0800)
Change-Id: I21cbd773a941d5b74676d154c42be959497b2f5f

sdkmanager/libs/sdklib/src/com/android/sdklib/SdkManager.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonPackage.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/BrokenPackage.java [new file with mode: 0755]
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/DocPackage.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ExtraPackage.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/LocalSdkParser.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/PlatformPackage.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/PlatformToolPackage.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SamplePackage.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ToolPackage.java
sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/broken_pkg_16.png [new file with mode: 0755]

index 32c5838..2920674 100644 (file)
@@ -21,6 +21,7 @@ import com.android.prefs.AndroidLocation.AndroidLocationException;
 import com.android.sdklib.AndroidVersion.AndroidVersionException;
 import com.android.sdklib.internal.project.ProjectProperties;
 import com.android.sdklib.io.FileWrapper;
+import com.android.sdklib.util.Pair;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -43,27 +44,27 @@ import java.util.regex.Pattern;
  */
 public final class SdkManager {
 
-    public final static String PROP_VERSION_SDK = "ro.build.version.sdk";
-    public final static String PROP_VERSION_CODENAME = "ro.build.version.codename";
-    public final static String PROP_VERSION_RELEASE = "ro.build.version.release";
+    public final static String PROP_VERSION_SDK = "ro.build.version.sdk";              //$NON-NLS-1$
+    public final static String PROP_VERSION_CODENAME = "ro.build.version.codename";    //$NON-NLS-1$
+    public final static String PROP_VERSION_RELEASE = "ro.build.version.release";      //$NON-NLS-1$
 
-    private final static String ADDON_NAME = "name";
-    private final static String ADDON_VENDOR = "vendor";
-    private final static String ADDON_API = "api";
-    private final static String ADDON_DESCRIPTION = "description";
-    private final static String ADDON_LIBRARIES = "libraries";
-    private final static String ADDON_DEFAULT_SKIN = "skin";
-    private final static String ADDON_USB_VENDOR = "usb-vendor";
-    private final static String ADDON_REVISION = "revision";
-    private final static String ADDON_REVISION_OLD = "version";
+    public final static String ADDON_NAME = "name";                                    //$NON-NLS-1$
+    public final static String ADDON_VENDOR = "vendor";                                //$NON-NLS-1$
+    public final static String ADDON_API = "api";                                      //$NON-NLS-1$
+    public final static String ADDON_DESCRIPTION = "description";                      //$NON-NLS-1$
+    public final static String ADDON_LIBRARIES = "libraries";                          //$NON-NLS-1$
+    public final static String ADDON_DEFAULT_SKIN = "skin";                            //$NON-NLS-1$
+    public final static String ADDON_USB_VENDOR = "usb-vendor";                        //$NON-NLS-1$
+    public final static String ADDON_REVISION = "revision";                            //$NON-NLS-1$
+    public final static String ADDON_REVISION_OLD = "version";                         //$NON-NLS-1$
 
 
     private final static Pattern PATTERN_LIB_DATA = Pattern.compile(
-            "^([a-zA-Z0-9._-]+\\.jar);(.*)$", Pattern.CASE_INSENSITIVE);
+            "^([a-zA-Z0-9._-]+\\.jar);(.*)$", Pattern.CASE_INSENSITIVE);               //$NON-NLS-1$
 
      // usb ids are 16-bit hexadecimal values.
     private final static Pattern PATTERN_USB_IDS = Pattern.compile(
-            "^0x[a-f0-9]{4}$", Pattern.CASE_INSENSITIVE);
+            "^0x[a-f0-9]{4}$", Pattern.CASE_INSENSITIVE);                              //$NON-NLS-1$
 
     /** List of items in the platform to check when parsing it. These paths are relative to the
      * platform root folder. */
@@ -73,39 +74,40 @@ public final class SdkManager {
     };
 
     /** Preference file containing the usb ids for adb */
-    private final static String ADB_INI_FILE = "adb_usb.ini";
+    private final static String ADB_INI_FILE = "adb_usb.ini";                          //$NON-NLS-1$
        //0--------90--------90--------90--------90--------90--------90--------90--------9
     private final static String ADB_INI_HEADER =
-        "# ANDROID 3RD PARTY USB VENDOR ID LIST -- DO NOT EDIT.\n" +
-        "# USE 'android update adb' TO GENERATE.\n" +
-        "# 1 USB VENDOR ID PER LINE.\n";
+        "# ANDROID 3RD PARTY USB VENDOR ID LIST -- DO NOT EDIT.\n" +                   //$NON-NLS-1$
+        "# USE 'android update adb' TO GENERATE.\n" +                                  //$NON-NLS-1$
+        "# 1 USB VENDOR ID PER LINE.\n";                                               //$NON-NLS-1$
 
-    /** the location of the SDK */
-    private final String mSdkLocation;
+    /** The location of the SDK as an OS path */
+    private final String mOsSdkPath;
+    /** Valid targets that have been loaded. */
     private IAndroidTarget[] mTargets;
 
     /**
      * Create a new {@link SdkManager} instance.
      * External users should use {@link #createManager(String, ISdkLog)}.
      *
-     * @param sdkLocation the location of the SDK.
+     * @param osSdkPath the location of the SDK.
      */
-    private SdkManager(String sdkLocation) {
-        mSdkLocation = sdkLocation;
+    private SdkManager(String osSdkPath) {
+        mOsSdkPath = osSdkPath;
     }
 
     /**
      * Creates an {@link SdkManager} for a given sdk location.
-     * @param sdkLocation the location of the SDK.
+     * @param osSdkPath the location of the SDK.
      * @param log the ISdkLog object receiving warning/error from the parsing. Cannot be null.
      * @return the created {@link SdkManager} or null if the location is not valid.
      */
-    public static SdkManager createManager(String sdkLocation, ISdkLog log) {
+    public static SdkManager createManager(String osSdkPath, ISdkLog log) {
         try {
-            SdkManager manager = new SdkManager(sdkLocation);
+            SdkManager manager = new SdkManager(osSdkPath);
             ArrayList<IAndroidTarget> list = new ArrayList<IAndroidTarget>();
-            loadPlatforms(sdkLocation, list, log);
-            loadAddOns(sdkLocation, list, log);
+            loadPlatforms(osSdkPath, list, log);
+            loadAddOns(osSdkPath, list, log);
 
             // sort the targets/add-ons
             Collections.sort(list);
@@ -127,7 +129,7 @@ public final class SdkManager {
      * Returns the location of the SDK.
      */
     public String getLocation() {
-        return mSdkLocation;
+        return mOsSdkPath;
     }
 
     /**
@@ -193,7 +195,7 @@ public final class SdkManager {
 
             // now write the Id in a text file, one per line.
             for (Integer i : set) {
-                writer.write(String.format("0x%04x\n", i));
+                writer.write(String.format("0x%04x\n", i));                            //$NON-NLS-1$
             }
         } finally {
             if (writer != null) {
@@ -210,8 +212,8 @@ public final class SdkManager {
     public void reloadSdk(ISdkLog log) {
         // get the current target list.
         ArrayList<IAndroidTarget> list = new ArrayList<IAndroidTarget>();
-        loadPlatforms(mSdkLocation, list, log);
-        loadAddOns(mSdkLocation, list, log);
+        loadPlatforms(mOsSdkPath, list, log);
+        loadAddOns(mOsSdkPath, list, log);
 
         // For now replace the old list with the new one.
         // In the future we may want to keep the current objects, so that ADT doesn't have to deal
@@ -326,7 +328,7 @@ public final class SdkManager {
                         sourcePropFile, log);
                 if (sourceProp != null) {
                     try {
-                        revision = Integer.parseInt(sourceProp.get("Pkg.Revision"));
+                        revision = Integer.parseInt(sourceProp.get("Pkg.Revision"));   //$NON-NLS-1$
                     } catch (NumberFormatException e) {
                         // do nothing, we'll keep the default value of 1.
                     }
@@ -366,7 +368,8 @@ public final class SdkManager {
                 return target;
             }
         } else {
-            log.warning("Ignoring platform '%1$s': %2$s is missing.", platformFolder.getName(),
+            log.warning("Ignoring platform '%1$s': %2$s is missing.",   //$NON-NLS-1$
+                    platformFolder.getName(),
                     SdkConstants.FN_BUILD_PROP);
         }
 
@@ -376,19 +379,21 @@ public final class SdkManager {
 
     /**
      * Loads the Add-on from the SDK.
-     * @param location Location of the SDK
+     * @param osSdkPath Location of the SDK
      * @param list the list to fill with the add-ons.
      * @param log the ISdkLog object receiving warning/error from the parsing. Cannot be null.
      */
-    private static void loadAddOns(String location, ArrayList<IAndroidTarget> list, ISdkLog log) {
-        File addonFolder = new File(location, SdkConstants.FD_ADDONS);
+    private static void loadAddOns(String osSdkPath, ArrayList<IAndroidTarget> list, ISdkLog log) {
+        File addonFolder = new File(osSdkPath, SdkConstants.FD_ADDONS);
         if (addonFolder.isDirectory()) {
             File[] addons  = addonFolder.listFiles();
 
+            IAndroidTarget[] targetList = list.toArray(new IAndroidTarget[list.size()]);
+
             for (File addon : addons) {
                 // Add-ons have to be folders. Ignore files and no need to warn about them.
                 if (addon.isDirectory()) {
-                    AddOnTarget target = loadAddon(addon, list, log);
+                    AddOnTarget target = loadAddon(addon, targetList, log);
                     if (target != null) {
                         list.add(target);
                     }
@@ -411,151 +416,222 @@ public final class SdkManager {
 
     /**
      * Loads a specific Add-on at a given location.
-     * @param addon the location of the addon.
+     * @param addonDir the location of the add-on directory.
      * @param targetList The list of Android target that were already loaded from the SDK.
      * @param log the ISdkLog object receiving warning/error from the parsing. Cannot be null.
      */
-    private static AddOnTarget loadAddon(File addon, ArrayList<IAndroidTarget> targetList,
+    private static AddOnTarget loadAddon(File addonDir,
+            IAndroidTarget[] targetList,
             ISdkLog log) {
-        FileWrapper addOnManifest = new FileWrapper(addon, SdkConstants.FN_MANIFEST_INI);
 
-        if (addOnManifest.isFile()) {
-            Map<String, String> propertyMap = ProjectProperties.parsePropertyFile(
-                    addOnManifest, log);
+        // Parse the addon properties to ensure we can load it.
+        Pair<Map<String, String>, String> infos = parseAddonProperties(addonDir, targetList, log);
 
-            if (propertyMap != null) {
-                // look for some specific values in the map.
-                // we require name, vendor, and api
-                String name = propertyMap.get(ADDON_NAME);
-                if (name == null) {
-                    displayAddonManifestWarning(log, addon.getName(), ADDON_NAME);
-                    return null;
-                }
+        Map<String, String> propertyMap = infos.getFirst();
+        String error = infos.getSecond();
 
-                String vendor = propertyMap.get(ADDON_VENDOR);
-                if (vendor == null) {
-                    displayAddonManifestWarning(log, addon.getName(), ADDON_VENDOR);
-                    return null;
-                }
+        if (error != null) {
+            log.warning("Ignoring add-on '%1$s': %2$s", addonDir.getName(), error);
+            return null;
+        }
 
-                String api = propertyMap.get(ADDON_API);
-                PlatformTarget baseTarget = null;
-                if (api == null) {
-                    displayAddonManifestWarning(log, addon.getName(), ADDON_API);
-                    return null;
-                } else {
-                    // Look for a platform that has a matching api level or codename.
-                    for (IAndroidTarget target : targetList) {
-                        if (target.isPlatform() && target.getVersion().equals(api)) {
-                            baseTarget = (PlatformTarget)target;
-                            break;
-                        }
-                    }
+        // Since error==null we're not supposed to encounter any issues loading this add-on.
+        try {
+            assert propertyMap != null;
 
-                    if (baseTarget == null) {
-                        // Ignore this add-on.
-                        log.warning(
-                                "Ignoring add-on '%1$s': Unable to find base platform with API level '%2$s'",
-                                addon.getName(), api);
-                        return null;
-                    }
-                }
+            String api = propertyMap.get(ADDON_API);
+            String name = propertyMap.get(ADDON_NAME);
+            String vendor = propertyMap.get(ADDON_VENDOR);
 
-                // get the optional description
-                String description = propertyMap.get(ADDON_DESCRIPTION);
+            assert api != null;
+            assert name != null;
+            assert vendor != null;
 
-                // get the add-on revision
-                int revisionValue = 1;
-                String revision = propertyMap.get(ADDON_REVISION);
-                if (revision == null) {
-                    revision = propertyMap.get(ADDON_REVISION_OLD);
-                }
-                if (revision != null) {
-                    try {
-                        revisionValue = Integer.parseInt(revision);
-                    } catch (NumberFormatException e) {
-                        // looks like apiNumber does not parse to a number.
-                        // Ignore this add-on.
-                        log.warning(
-                                "Ignoring add-on '%1$s': %2$s is not a valid number in %3$s.",
-                                addon.getName(), ADDON_REVISION, SdkConstants.FN_BUILD_PROP);
-                        return null;
-                    }
+            PlatformTarget baseTarget = null;
+
+            // Look for a platform that has a matching api level or codename.
+            for (IAndroidTarget target : targetList) {
+                if (target.isPlatform() && target.getVersion().equals(api)) {
+                    baseTarget = (PlatformTarget)target;
+                    break;
                 }
+            }
+
+            assert baseTarget != null;
+
+            // get the optional description
+            String description = propertyMap.get(ADDON_DESCRIPTION);
+
+            // get the add-on revision
+            int revisionValue = 1;
+            String revision = propertyMap.get(ADDON_REVISION);
+            if (revision == null) {
+                revision = propertyMap.get(ADDON_REVISION_OLD);
+            }
+            if (revision != null) {
+                revisionValue = Integer.parseInt(revision);
+            }
 
-                // get the optional libraries
-                String librariesValue = propertyMap.get(ADDON_LIBRARIES);
-                Map<String, String[]> libMap = null;
-
-                if (librariesValue != null) {
-                    librariesValue = librariesValue.trim();
-                    if (librariesValue.length() > 0) {
-                        // split in the string into the libraries name
-                        String[] libraries = librariesValue.split(";");
-                        if (libraries.length > 0) {
-                            libMap = new HashMap<String, String[]>();
-                            for (String libName : libraries) {
-                                libName = libName.trim();
-
-                                // get the library data from the properties
-                                String libData = propertyMap.get(libName);
-
-                                if (libData != null) {
-                                    // split the jar file from the description
-                                    Matcher m = PATTERN_LIB_DATA.matcher(libData);
-                                    if (m.matches()) {
-                                        libMap.put(libName, new String[] {
-                                                m.group(1), m.group(2) });
-                                    } else {
-                                        log.warning(
-                                                "Ignoring library '%1$s', property value has wrong format\n\t%2$s",
-                                                libName, libData);
-                                    }
+            // get the optional libraries
+            String librariesValue = propertyMap.get(ADDON_LIBRARIES);
+            Map<String, String[]> libMap = null;
+
+            if (librariesValue != null) {
+                librariesValue = librariesValue.trim();
+                if (librariesValue.length() > 0) {
+                    // split in the string into the libraries name
+                    String[] libraries = librariesValue.split(";");                    //$NON-NLS-1$
+                    if (libraries.length > 0) {
+                        libMap = new HashMap<String, String[]>();
+                        for (String libName : libraries) {
+                            libName = libName.trim();
+
+                            // get the library data from the properties
+                            String libData = propertyMap.get(libName);
+
+                            if (libData != null) {
+                                // split the jar file from the description
+                                Matcher m = PATTERN_LIB_DATA.matcher(libData);
+                                if (m.matches()) {
+                                    libMap.put(libName, new String[] {
+                                            m.group(1), m.group(2) });
                                 } else {
                                     log.warning(
-                                            "Ignoring library '%1$s', missing property value",
+                                            "Ignoring library '%1$s', property value has wrong format\n\t%2$s",
                                             libName, libData);
                                 }
+                            } else {
+                                log.warning(
+                                        "Ignoring library '%1$s', missing property value",
+                                        libName, libData);
                             }
                         }
                     }
                 }
+            }
 
-                AddOnTarget target = new AddOnTarget(addon.getAbsolutePath(), name, vendor,
-                        revisionValue, description, libMap, baseTarget);
+            AddOnTarget target = new AddOnTarget(addonDir.getAbsolutePath(), name, vendor,
+                    revisionValue, description, libMap, baseTarget);
 
-                // need to parse the skins.
-                String[] skins = parseSkinFolder(target.getPath(IAndroidTarget.SKINS));
+            // need to parse the skins.
+            String[] skins = parseSkinFolder(target.getPath(IAndroidTarget.SKINS));
 
-                // get the default skin, or take it from the base platform if needed.
-                String defaultSkin = propertyMap.get(ADDON_DEFAULT_SKIN);
-                if (defaultSkin == null) {
-                    if (skins.length == 1) {
-                        defaultSkin = skins[0];
-                    } else {
-                        defaultSkin = baseTarget.getDefaultSkin();
-                    }
+            // get the default skin, or take it from the base platform if needed.
+            String defaultSkin = propertyMap.get(ADDON_DEFAULT_SKIN);
+            if (defaultSkin == null) {
+                if (skins.length == 1) {
+                    defaultSkin = skins[0];
+                } else {
+                    defaultSkin = baseTarget.getDefaultSkin();
                 }
+            }
 
-                // get the USB ID (if available)
-                int usbVendorId = convertId(propertyMap.get(ADDON_USB_VENDOR));
-                if (usbVendorId != IAndroidTarget.NO_USB_ID) {
-                    target.setUsbVendorId(usbVendorId);
-                }
+            // get the USB ID (if available)
+            int usbVendorId = convertId(propertyMap.get(ADDON_USB_VENDOR));
+            if (usbVendorId != IAndroidTarget.NO_USB_ID) {
+                target.setUsbVendorId(usbVendorId);
+            }
 
-                target.setSkins(skins, defaultSkin);
+            target.setSkins(skins, defaultSkin);
 
-                return target;
-            }
-        } else {
-            log.warning("Ignoring add-on '%1$s': %2$s is missing.", addon.getName(),
-                    SdkConstants.FN_MANIFEST_INI);
+            return target;
+        }
+        catch (Exception e) {
+            log.warning("Ignoring add-on '%1$s': error %2$s.",
+                    addonDir.getName(), e.toString());
         }
 
         return null;
     }
 
     /**
+     * Parses the add-on properties and decodes any error that occurs when loading an addon.
+     *
+     * @param addonDir the location of the addon directory.
+     * @param targetList The list of Android target that were already loaded from the SDK.
+     * @param log the ISdkLog object receiving warning/error from the parsing. Cannot be null.
+     * @return A pair with the property map and an error string. Both can be null but not at the
+     *  same time. If a non-null error is present then the property map must be ignored. The error
+     *  should be translatable as it might show up in the SdkManager UI.
+     */
+    public static Pair<Map<String, String>, String> parseAddonProperties(
+            File addonDir,
+            IAndroidTarget[] targetList,
+            ISdkLog log) {
+        Map<String, String> propertyMap = null;
+        String error = null;
+
+        FileWrapper addOnManifest = new FileWrapper(addonDir, SdkConstants.FN_MANIFEST_INI);
+
+        do {
+            if (!addOnManifest.isFile()) {
+                error = String.format("File not found: %1$s", SdkConstants.FN_MANIFEST_INI);
+                break;
+            }
+
+            propertyMap = ProjectProperties.parsePropertyFile(addOnManifest, log);
+            if (propertyMap == null) {
+                error = String.format("Failed to parse properties from %1$s",
+                        SdkConstants.FN_MANIFEST_INI);
+                break;
+            }
+
+            // look for some specific values in the map.
+            // we require name, vendor, and api
+            String name = propertyMap.get(ADDON_NAME);
+            if (name == null) {
+                error = addonManifestWarning(ADDON_NAME);
+                break;
+            }
+
+            String vendor = propertyMap.get(ADDON_VENDOR);
+            if (vendor == null) {
+                error = addonManifestWarning(ADDON_VENDOR);
+                break;
+            }
+
+            String api = propertyMap.get(ADDON_API);
+            PlatformTarget baseTarget = null;
+            if (api == null) {
+                error = addonManifestWarning(ADDON_API);
+                break;
+            }
+
+            // Look for a platform that has a matching api level or codename.
+            for (IAndroidTarget target : targetList) {
+                if (target.isPlatform() && target.getVersion().equals(api)) {
+                    baseTarget = (PlatformTarget)target;
+                    break;
+                }
+            }
+
+            if (baseTarget == null) {
+                error = String.format("Unable to find base platform with API level '%1$s'", api);
+                break;
+            }
+
+            // get the add-on revision
+            String revision = propertyMap.get(ADDON_REVISION);
+            if (revision == null) {
+                revision = propertyMap.get(ADDON_REVISION_OLD);
+            }
+            if (revision != null) {
+                try {
+                    Integer.parseInt(revision);
+                } catch (NumberFormatException e) {
+                    // looks like revision does not parse to a number.
+                    error = String.format("%1$s is not a valid number in %2$s.",
+                                ADDON_REVISION, SdkConstants.FN_BUILD_PROP);
+                    break;
+                }
+            }
+
+        } while(false);
+
+        return Pair.of(propertyMap, error);
+    }
+
+    /**
      * Converts a string representation of an hexadecimal ID into an int.
      * @param value the string to convert.
      * @return the int value, or {@link IAndroidTarget#NO_USB_ID} if the convertion failed.
@@ -577,16 +653,14 @@ public final class SdkManager {
     }
 
     /**
-     * Displays a warning in the log about the addon being ignored due to a missing manifest value.
+     * Prepares a warning about the addon being ignored due to a missing manifest value.
+     * This string will show up in the SdkManager UI.
      *
-     * @param log The logger object. Cannot be null.
-     * @param addonName The addon name, for display.
      * @param valueName The missing manifest value, for display.
      */
-    private static void displayAddonManifestWarning(ISdkLog log, String addonName,
-            String valueName) {
-        log.warning("Ignoring add-on '%1$s': '%2$s' is missing from %3$s.",
-                addonName, valueName, SdkConstants.FN_MANIFEST_INI);
+    private static String addonManifestWarning(String valueName) {
+        return String.format("'%1$s' is missing from %2$s.",
+                valueName, SdkConstants.FN_MANIFEST_INI);
     }
 
     /**
@@ -603,7 +677,7 @@ public final class SdkManager {
             File f = new File(platform, relativePath);
             if (!f.exists()) {
                 log.warning(
-                        "Ignoring platform '%1$s': %2$s is missing.",
+                        "Ignoring platform '%1$s': %2$s is missing.",                  //$NON-NLS-1$
                         platform.getName(), relativePath);
                 return false;
             }
@@ -650,7 +724,7 @@ public final class SdkManager {
      * @param log Logger. Cannot be null.
      */
     private void loadSamples(ISdkLog log) {
-        File sampleFolder = new File(mSdkLocation, SdkConstants.FD_SAMPLES);
+        File sampleFolder = new File(mOsSdkPath, SdkConstants.FD_SAMPLES);
         if (sampleFolder.isDirectory()) {
             File[] platforms  = sampleFolder.listFiles();
 
@@ -688,13 +762,13 @@ public final class SdkManager {
 
             return new AndroidVersion(p);
         } catch (FileNotFoundException e) {
-            log.warning("Ignoring sample '%1$s': does not contain %2$s.", //$NON-NLS-1$
+            log.warning("Ignoring sample '%1$s': does not contain %2$s.",              //$NON-NLS-1$
                     folder.getName(), SdkConstants.FN_SOURCE_PROP);
         } catch (IOException e) {
-            log.warning("Ignoring sample '%1$s': failed reading %2$s.", //$NON-NLS-1$
+            log.warning("Ignoring sample '%1$s': failed reading %2$s.",                //$NON-NLS-1$
                     folder.getName(), SdkConstants.FN_SOURCE_PROP);
         } catch (AndroidVersionException e) {
-            log.warning("Ignoring sample '%1$s': no android version found in %2$s.", //$NON-NLS-1$
+            log.warning("Ignoring sample '%1$s': no android version found in %2$s.",   //$NON-NLS-1$
                     folder.getName(), SdkConstants.FN_SOURCE_PROP);
         }
 
index 2a58a89..8226a60 100755 (executable)
@@ -21,6 +21,8 @@ import com.android.sdklib.IAndroidTarget;
 import com.android.sdklib.SdkConstants;\r
 import com.android.sdklib.SdkManager;\r
 import com.android.sdklib.IAndroidTarget.IOptionalLibrary;\r
+import com.android.sdklib.annotations.VisibleForTesting;\r
+import com.android.sdklib.annotations.VisibleForTesting.Visibility;\r
 import com.android.sdklib.internal.repository.Archive.Arch;\r
 import com.android.sdklib.internal.repository.Archive.Os;\r
 import com.android.sdklib.repository.SdkRepoConstants;\r
@@ -98,7 +100,12 @@ public class AddonPackage extends Package
      * <p/>\r
      * By design, this creates a package with one and only one archive.\r
      */\r
-    AddonPackage(IAndroidTarget target, Properties props) {\r
+    static Package create(IAndroidTarget target, Properties props) {\r
+        return new AddonPackage(target, props);\r
+    }\r
+\r
+    @VisibleForTesting(visibility=Visibility.PRIVATE)\r
+    protected AddonPackage(IAndroidTarget target, Properties props) {\r
         super(  null,                       //source\r
                 props,                      //properties\r
                 target.getRevision(),       //revision\r
@@ -126,6 +133,42 @@ public class AddonPackage extends Package
     }\r
 \r
     /**\r
+     * Creates a broken addon which we know failed to load properly.\r
+     *\r
+     * @param archiveOsPath The absolute OS path of the addon folder.\r
+     * @param props The properties parsed from the addon manifest (not the source.properties).\r
+     * @param error The error indicating why this addon failed to be loaded.\r
+     */\r
+    static Package create(String archiveOsPath, Map<String, String> props, String error) {\r
+        String name     = props.get(SdkManager.ADDON_NAME);\r
+        String vendor   = props.get(SdkManager.ADDON_VENDOR);\r
+        String api      = props.get(SdkManager.ADDON_API);\r
+        String revision = props.get(SdkManager.ADDON_REVISION);\r
+\r
+        String shortDesc = String.format("%1$s by %2$s, Android API %3$s, revision %4$s [*]",\r
+                name,\r
+                vendor,\r
+                api,\r
+                revision);\r
+\r
+        String longDesc = String.format(\r
+                "%1$s\n" +\r
+                "[*] Addon failed to load: %2$s",\r
+                shortDesc,\r
+                error);\r
+\r
+        int minApiLevel = IMinApiLevelDependency.MIN_API_LEVEL_NOT_SPECIFIED;\r
+\r
+        try {\r
+            minApiLevel = Integer.parseInt(api);\r
+        } catch(NumberFormatException e) {\r
+            // ignore\r
+        }\r
+\r
+        return new BrokenPackage(null/*props*/, shortDesc, longDesc, minApiLevel, archiveOsPath);\r
+    }\r
+\r
+    /**\r
      * Save the properties of the current packages in the given {@link Properties} object.\r
      * These properties will later be given to a constructor that takes a {@link Properties} object.\r
      */\r
diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/BrokenPackage.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/BrokenPackage.java
new file mode 100755 (executable)
index 0000000..646172d
--- /dev/null
@@ -0,0 +1,147 @@
+/*\r
+ * Copyright (C) 2010 The Android Open Source Project\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *      http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+\r
+package com.android.sdklib.internal.repository;\r
+\r
+import com.android.sdklib.SdkManager;\r
+import com.android.sdklib.internal.repository.Archive.Arch;\r
+import com.android.sdklib.internal.repository.Archive.Os;\r
+\r
+import java.io.File;\r
+import java.util.Properties;\r
+\r
+/**\r
+ * Represents an SDK repository package that is incomplete.\r
+ * It has a distinct icon and a specific error that is supposed to help the user on how to fix it.\r
+ */\r
+public class BrokenPackage extends Package implements IMinApiLevelDependency {\r
+\r
+    /**\r
+     * The minimal API level required by this extra package, if > 0,\r
+     * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement.\r
+     */\r
+    private final int mMinApiLevel;\r
+\r
+    private final String mShortDescription;\r
+\r
+    /**\r
+     * Creates a new "broken" package that represents a package that we failed to load,\r
+     * for whatever error indicated in <code>error</code>.\r
+     * There is also an <em>optional</em> API level dependency that can be specified.\r
+     * <p/>\r
+     * By design, this creates a package with one and only one archive.\r
+     */\r
+    BrokenPackage(Properties props,\r
+            String shortDescription,\r
+            String longDescription,\r
+            int minApiLevel,\r
+            String archiveOsPath) {\r
+        super(  null,                                   //source\r
+                props,                                  //properties\r
+                0,                                      //revision will be taken from props\r
+                null,                                   //license\r
+                longDescription,                        //description\r
+                null,                                   //descUrl\r
+                Os.ANY,                                 //archiveOs\r
+                Arch.ANY,                               //archiveArch\r
+                archiveOsPath                           //archiveOsPath\r
+                );\r
+        mShortDescription = shortDescription;\r
+        mMinApiLevel = minApiLevel;\r
+    }\r
+\r
+    /**\r
+     * Save the properties of the current packages in the given {@link Properties} object.\r
+     * These properties will later be given to a constructor that takes a {@link Properties} object.\r
+     * <p/>\r
+     * Base implementation override: We don't actually save properties for a broken package.\r
+     */\r
+    @Override\r
+    void saveProperties(Properties props) {\r
+        // Nop. We don't actually save properties for a broken package.\r
+    }\r
+\r
+    /**\r
+     * Returns the minimal API level required by this extra package, if > 0,\r
+     * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement.\r
+     */\r
+    public int getMinApiLevel() {\r
+        return mMinApiLevel;\r
+    }\r
+\r
+    /** Returns a short description for an {@link IDescription}. */\r
+    @Override\r
+    public String getShortDescription() {\r
+        return mShortDescription;\r
+    }\r
+\r
+    /**\r
+     * Returns a long description for an {@link IDescription}.\r
+     *\r
+     * The long description is whatever the XML contains for the &lt;description&gt; field,\r
+     * or the short description if the former is empty.\r
+     */\r
+    @Override\r
+    public String getLongDescription() {\r
+        String s = getDescription();\r
+        if (s == null || s.length() == 0) {\r
+            s = getShortDescription();\r
+        }\r
+        return s;\r
+    }\r
+\r
+    /**\r
+     * We should not be attempting to install a broken package.\r
+     *\r
+     * {@inheritDoc}\r
+     */\r
+    @Override\r
+    public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) {\r
+        // We should not be attempting to install a broken package.\r
+        return null;\r
+    }\r
+\r
+    @Override\r
+    public boolean sameItemAs(Package pkg) {\r
+        if (pkg instanceof BrokenPackage) {\r
+            return mShortDescription.equals(((BrokenPackage) pkg).mShortDescription) &&\r
+                getDescription().equals(pkg.getDescription()) &&\r
+                getMinApiLevel() == ((BrokenPackage) pkg).getMinApiLevel();\r
+        }\r
+\r
+        return false;\r
+    }\r
+\r
+    @Override\r
+    public boolean preInstallHook(Archive archive,\r
+            ITaskMonitor monitor,\r
+            String osSdkRoot,\r
+            File installFolder) {\r
+        // Nothing specific to do.\r
+        return super.preInstallHook(archive, monitor, osSdkRoot, installFolder);\r
+    }\r
+\r
+    /**\r
+     * Computes a hash of the installed content (in case of successful install.)\r
+     *\r
+     * {@inheritDoc}\r
+     */\r
+    @Override\r
+    public void postInstallHook(Archive archive, ITaskMonitor monitor, File installFolder) {\r
+        // Nothing specific to do.\r
+        super.postInstallHook(archive, monitor, installFolder);\r
+    }\r
+}\r
index 5e0a767..8a4c19d 100755 (executable)
@@ -68,7 +68,22 @@ public class DocPackage extends Package implements IPackageVersion {
      * <p/>\r
      * By design, this creates a package with one and only one archive.\r
      */\r
-    DocPackage(SdkSource source,\r
+    static Package create(SdkSource source,\r
+            Properties props,\r
+            int apiLevel,\r
+            String codename,\r
+            int revision,\r
+            String license,\r
+            String description,\r
+            String descUrl,\r
+            Os archiveOs,\r
+            Arch archiveArch,\r
+            String archiveOsPath) {\r
+        return new DocPackage(source, props, apiLevel, codename, revision, license, description,\r
+                descUrl, archiveOs, archiveArch, archiveOsPath);\r
+    }\r
+\r
+    private DocPackage(SdkSource source,\r
             Properties props,\r
             int apiLevel,\r
             String codename,\r
index d06b08d..cac622d 100755 (executable)
@@ -87,7 +87,38 @@ public class ExtraPackage extends MinToolsPackage
      * <p/>\r
      * By design, this creates a package with one and only one archive.\r
      */\r
-    ExtraPackage(SdkSource source,\r
+    static Package create(SdkSource source,\r
+            Properties props,\r
+            String vendor,\r
+            String path,\r
+            int revision,\r
+            String license,\r
+            String description,\r
+            String descUrl,\r
+            Os archiveOs,\r
+            Arch archiveArch,\r
+            String archiveOsPath) {\r
+        ExtraPackage ep = new ExtraPackage(source, props, vendor, path, revision, license,\r
+                description, descUrl, archiveOs, archiveArch, archiveOsPath);\r
+\r
+        if (ep.isPathValid()) {\r
+            return ep;\r
+        } else {\r
+            String shortDesc = ep.getShortDescription() + " [*]";  //$NON-NLS-1$\r
+\r
+            String longDesc = String.format(\r
+                    "Broken Extra Package: %1$s\n" +\r
+                    "[*] Package cannot be used due to error: Invalid install path %2$s",\r
+                    description,\r
+                    ep.getPath());\r
+\r
+            BrokenPackage ba = new BrokenPackage(props, shortDesc, longDesc,\r
+                    IMinApiLevelDependency.MIN_API_LEVEL_NOT_SPECIFIED, archiveOsPath);\r
+            return ba;\r
+        }\r
+    }\r
+\r
+    private ExtraPackage(SdkSource source,\r
             Properties props,\r
             String vendor,\r
             String path,\r
index 915a8e6..e0a6b21 100755 (executable)
@@ -22,6 +22,7 @@ import com.android.sdklib.SdkConstants;
 import com.android.sdklib.SdkManager;\r
 import com.android.sdklib.internal.repository.Archive.Arch;\r
 import com.android.sdklib.internal.repository.Archive.Os;\r
+import com.android.sdklib.util.Pair;\r
 \r
 import java.io.File;\r
 import java.io.FileInputStream;\r
@@ -29,6 +30,7 @@ import java.io.IOException;
 import java.util.ArrayList;\r
 import java.util.Collections;\r
 import java.util.HashSet;\r
+import java.util.Map;\r
 import java.util.Properties;\r
 import java.util.Set;\r
 \r
@@ -102,13 +104,12 @@ public class LocalSdkParser {
 \r
         // for platforms, add-ons and samples, rely on the SdkManager parser\r
         for(IAndroidTarget target : sdkManager.getTargets()) {\r
-\r
             Properties props = parseProperties(new File(target.getLocation(),\r
                     SdkConstants.FN_SOURCE_PROP));\r
 \r
             try {\r
                 if (target.isPlatform()) {\r
-                    pkg = new PlatformPackage(target, props);\r
+                    pkg = PlatformPackage.create(target, props);\r
 \r
                     if (samplesRoot.isDirectory()) {\r
                         // Get the samples dir for a platform if it is located in the new\r
@@ -119,14 +120,14 @@ public class LocalSdkParser {
                             Properties samplesProps = parseProperties(\r
                                     new File(samplesDir, SdkConstants.FN_SOURCE_PROP));\r
                             if (samplesProps != null) {\r
-                                SamplePackage pkg2 = new SamplePackage(target, samplesProps);\r
+                                Package pkg2 = SamplePackage.create(target, samplesProps);\r
                                 packages.add(pkg2);\r
                             }\r
                             visited.add(samplesDir);\r
                         }\r
                     }\r
                 } else {\r
-                    pkg = new AddonPackage(target, props);\r
+                    pkg = AddonPackage.create(target, props);\r
                 }\r
             } catch (Exception e) {\r
                 log.error(e, null);\r
@@ -138,6 +139,7 @@ public class LocalSdkParser {
             }\r
         }\r
 \r
+        scanMissingAddons(sdkManager, visited, packages, log);\r
         scanMissingSamples(osSdkRoot, visited, packages, log);\r
         scanExtras(osSdkRoot, visited, packages, log);\r
 \r
@@ -167,7 +169,7 @@ public class LocalSdkParser {
                 Properties props = parseProperties(new File(dir, SdkConstants.FN_SOURCE_PROP));\r
                 if (props != null) {\r
                     try {\r
-                        ExtraPackage pkg = new ExtraPackage(\r
+                        Package pkg = ExtraPackage.create(\r
                                 null,                       //source\r
                                 props,                      //properties\r
                                 null,                       //vendor\r
@@ -181,11 +183,8 @@ public class LocalSdkParser {
                                 dir.getPath()               //archiveOsPath\r
                                 );\r
 \r
-                        // We only accept this as an extra package if it has a valid local path.\r
-                        if (pkg.isPathValid()) {\r
-                            packages.add(pkg);\r
-                            visited.add(dir);\r
-                        }\r
+                        packages.add(pkg);\r
+                        visited.add(dir);\r
                     } catch (Exception e) {\r
                         log.error(e, null);\r
                     }\r
@@ -217,7 +216,7 @@ public class LocalSdkParser {
                 Properties props = parseProperties(new File(dir, SdkConstants.FN_SOURCE_PROP));\r
                 if (props != null) {\r
                     try {\r
-                        SamplePackage pkg = new SamplePackage(dir.getAbsolutePath(), props);\r
+                        Package pkg = SamplePackage.create(dir.getAbsolutePath(), props);\r
                         packages.add(pkg);\r
                         visited.add(dir);\r
                     } catch (Exception e) {\r
@@ -229,6 +228,42 @@ public class LocalSdkParser {
     }\r
 \r
     /**\r
+     * The sdk manager only lists valid addons. However here we also want to find "broken"\r
+     * addons, i.e. addons that failed to load for some reason.\r
+     * <p/>\r
+     * Find any other sub-directories under the /add-ons root that hasn't been visited yet\r
+     * and assume they contain broken addons.\r
+     */\r
+    private void scanMissingAddons(SdkManager sdkManager,\r
+            HashSet<File> visited,\r
+            ArrayList<Package> packages,\r
+            ISdkLog log) {\r
+        File addons = new File(new File(sdkManager.getLocation()), SdkConstants.FD_ADDONS);\r
+\r
+        if (!addons.isDirectory()) {\r
+            // It makes listFiles() return null so let's avoid it.\r
+            return;\r
+        }\r
+\r
+        for (File dir : addons.listFiles()) {\r
+            if (dir.isDirectory() && !visited.contains(dir)) {\r
+                Pair<Map<String, String>, String> infos =\r
+                    SdkManager.parseAddonProperties(dir, sdkManager.getTargets(), log);\r
+\r
+                Map<String, String> props = infos.getFirst();\r
+                String error = infos.getSecond();\r
+                try {\r
+                    Package pkg = AddonPackage.create(dir.getAbsolutePath(), props, error);\r
+                    packages.add(pkg);\r
+                    visited.add(dir);\r
+                } catch (Exception e) {\r
+                    log.error(e, null);\r
+                }\r
+            }\r
+        }\r
+    }\r
+\r
+    /**\r
      * Try to find a tools package at the given location.\r
      * Returns null if not found.\r
      */\r
@@ -252,7 +287,7 @@ public class LocalSdkParser {
 \r
         // Create our package. use the properties if we found any.\r
         try {\r
-            ToolPackage pkg = new ToolPackage(\r
+            Package pkg = ToolPackage.create(\r
                     null,                       //source\r
                     props,                      //properties\r
                     0,                          //revision\r
@@ -289,20 +324,9 @@ public class LocalSdkParser {
             return null;\r
         }\r
 \r
-        Set<String> names = new HashSet<String>();\r
-        for (File file : platformToolsFolder.listFiles()) {\r
-            names.add(file.getName());\r
-        }\r
-        if (!names.contains(SdkConstants.FN_ADB) ||\r
-                !names.contains(SdkConstants.FN_AAPT) ||\r
-                !names.contains(SdkConstants.FN_AIDL) ||\r
-                !names.contains(SdkConstants.FN_DX)) {\r
-            return null;\r
-        }\r
-\r
         // Create our package. use the properties if we found any.\r
         try {\r
-            PlatformToolPackage pkg = new PlatformToolPackage(\r
+            Package pkg = PlatformToolPackage.create(\r
                     null,                           //source\r
                     props,                          //properties\r
                     0,                              //revision\r
@@ -333,7 +357,7 @@ public class LocalSdkParser {
         // We don't actually check the content of the file.\r
         if (new File(docFolder, "index.html").isFile()) {\r
             try {\r
-                DocPackage pkg = new DocPackage(\r
+                Package pkg = DocPackage.create(\r
                         null,                       //source\r
                         props,                      //properties\r
                         0,                          //apiLevel\r
index 1e5e391..b8a8623 100755 (executable)
@@ -20,6 +20,8 @@ import com.android.sdklib.AndroidVersion;
 import com.android.sdklib.IAndroidTarget;\r
 import com.android.sdklib.SdkConstants;\r
 import com.android.sdklib.SdkManager;\r
+import com.android.sdklib.annotations.VisibleForTesting;\r
+import com.android.sdklib.annotations.VisibleForTesting.Visibility;\r
 import com.android.sdklib.internal.repository.Archive.Arch;\r
 import com.android.sdklib.internal.repository.Archive.Os;\r
 import com.android.sdklib.repository.SdkRepoConstants;\r
@@ -73,7 +75,12 @@ public class PlatformPackage extends MinToolsPackage implements IPackageVersion
      * <p/>\r
      * By design, this creates a package with one and only one archive.\r
      */\r
-    PlatformPackage(IAndroidTarget target, Properties props) {\r
+    static Package create(IAndroidTarget target, Properties props) {\r
+        return new PlatformPackage(target, props);\r
+    }\r
+\r
+    @VisibleForTesting(visibility=Visibility.PRIVATE)\r
+    protected PlatformPackage(IAndroidTarget target, Properties props) {\r
         super(  null,                       //source\r
                 props,                      //properties\r
                 target.getRevision(),       //revision\r
index 717948e..7b0494f 100755 (executable)
@@ -18,14 +18,18 @@ package com.android.sdklib.internal.repository;
 
 import com.android.sdklib.SdkConstants;
 import com.android.sdklib.SdkManager;
+import com.android.sdklib.annotations.VisibleForTesting;
+import com.android.sdklib.annotations.VisibleForTesting.Visibility;
 import com.android.sdklib.internal.repository.Archive.Arch;
 import com.android.sdklib.internal.repository.Archive.Os;
 
 import org.w3c.dom.Node;
 
 import java.io.File;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Properties;
+import java.util.Set;
 
 /**
  * Represents a platform-tool XML node in an SDK repository.
@@ -54,7 +58,7 @@ public class PlatformToolPackage extends Package {
      * <p/>
      * By design, this creates a package with one and only one archive.
      */
-    PlatformToolPackage(
+    static Package create(
             SdkSource source,
             Properties props,
             int revision,
@@ -64,6 +68,68 @@ public class PlatformToolPackage extends Package {
             Os archiveOs,
             Arch archiveArch,
             String archiveOsPath) {
+
+        PlatformToolPackage ptp = new PlatformToolPackage(source, props, revision, license,
+                description, descUrl, archiveOs, archiveArch, archiveOsPath);
+
+        File platformToolsFolder = new File(archiveOsPath);
+        String error = null;
+        if (!platformToolsFolder.isDirectory()) {
+            error = "platform-tools folder is missing";
+        } else {
+            File[] files = platformToolsFolder.listFiles();
+            if (files == null || files.length == 0) {
+                error = "platform-tools folder is empty";
+            } else {
+                Set<String> names = new HashSet<String>();
+                for (File file : files) {
+                    names.add(file.getName());
+                }
+                for (String name : new String[] { SdkConstants.FN_ADB,
+                                                  SdkConstants.FN_AAPT,
+                                                  SdkConstants.FN_AIDL,
+                                                  SdkConstants.FN_DX } ) {
+                    if (!names.contains(name)) {
+                        if (error == null) {
+                            error = "platform-tools folder is missing ";
+                        } else {
+                            error += ", ";
+                        }
+                        error += name;
+                    }
+                }
+            }
+        }
+
+        if (error != null) {
+            String shortDesc = ptp.getShortDescription() + " [*]";  //$NON-NLS-1$
+
+            String longDesc = String.format(
+                    "Broken Platform-Tools Package: %1$s\n" +
+                    "[*] Package cannot be used due to error: %2$s",
+                    description,
+                    error);
+
+            BrokenPackage ba = new BrokenPackage(props, shortDesc, longDesc,
+                    IMinApiLevelDependency.MIN_API_LEVEL_NOT_SPECIFIED, archiveOsPath);
+            return ba;
+        }
+
+
+        return ptp;
+    }
+
+    @VisibleForTesting(visibility=Visibility.PRIVATE)
+    protected PlatformToolPackage(
+                SdkSource source,
+                Properties props,
+                int revision,
+                String license,
+                String description,
+                String descUrl,
+                Os archiveOs,
+                Arch archiveArch,
+                String archiveOsPath) {
         super(source,
                 props,
                 revision,
index 920a7e0..b6976e9 100755 (executable)
@@ -91,7 +91,11 @@ public class SamplePackage extends MinToolsPackage
      * <p/>\r
      * By design, this creates a package with one and only one archive.\r
      */\r
-    SamplePackage(IAndroidTarget target, Properties props) {\r
+    static Package create(IAndroidTarget target, Properties props) {\r
+        return new SamplePackage(target, props);\r
+    }\r
+\r
+    private SamplePackage(IAndroidTarget target, Properties props) {\r
         super(  null,                                   //source\r
                 props,                                  //properties\r
                 0,                                      //revision will be taken from props\r
@@ -121,7 +125,11 @@ public class SamplePackage extends MinToolsPackage
      * @throws AndroidVersionException if the {@link AndroidVersion} can't be restored\r
      *                                 from properties.\r
      */\r
-    SamplePackage(String archiveOsPath, Properties props) throws AndroidVersionException {\r
+    static Package create(String archiveOsPath, Properties props) throws AndroidVersionException {\r
+        return new SamplePackage(archiveOsPath, props);\r
+    }\r
+\r
+    private SamplePackage(String archiveOsPath, Properties props) throws AndroidVersionException {\r
         super(null,                                   //source\r
               props,                                  //properties\r
               0,                                      //revision will be taken from props\r
index c76de30..73ddc24 100755 (executable)
@@ -18,6 +18,8 @@ package com.android.sdklib.internal.repository;
 \r
 import com.android.sdklib.SdkConstants;\r
 import com.android.sdklib.SdkManager;\r
+import com.android.sdklib.annotations.VisibleForTesting;\r
+import com.android.sdklib.annotations.VisibleForTesting.Visibility;\r
 import com.android.sdklib.internal.repository.Archive.Arch;\r
 import com.android.sdklib.internal.repository.Archive.Os;\r
 import com.android.sdklib.repository.SdkRepoConstants;\r
@@ -97,7 +99,7 @@ public class ToolPackage extends Package implements IMinPlatformToolsDependency
      * <p/>\r
      * By design, this creates a package with one and only one archive.\r
      */\r
-    ToolPackage(\r
+    static Package create(\r
             SdkSource source,\r
             Properties props,\r
             int revision,\r
@@ -107,6 +109,21 @@ public class ToolPackage extends Package implements IMinPlatformToolsDependency
             Os archiveOs,\r
             Arch archiveArch,\r
             String archiveOsPath) {\r
+        return new ToolPackage(source, props, revision, license, description,\r
+                descUrl, archiveOs, archiveArch, archiveOsPath);\r
+    }\r
+\r
+    @VisibleForTesting(visibility=Visibility.PRIVATE)\r
+    protected ToolPackage(\r
+                SdkSource source,\r
+                Properties props,\r
+                int revision,\r
+                String license,\r
+                String description,\r
+                String descUrl,\r
+                Os archiveOs,\r
+                Arch archiveArch,\r
+                String archiveOsPath) {\r
         super(source,\r
                 props,\r
                 revision,\r
diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/broken_pkg_16.png b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/broken_pkg_16.png
new file mode 100755 (executable)
index 0000000..6daa67b
Binary files /dev/null and b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/broken_pkg_16.png differ