SDK Updater: platform dependency on tools, addon dependency on platform.

SDK BUG 2040986

Change-Id: Ica46d14939bb3a9bf499899a0bf571456d4c6017
diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/AndroidVersion.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/AndroidVersion.java
index ef62f6e..13c3ea1 100644
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/AndroidVersion.java
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/AndroidVersion.java
@@ -40,13 +40,14 @@
 public class AndroidVersion {
 
     private static final String PROP_API_LEVEL = "AndroidVersion.ApiLevel";  //$NON-NLS-1$
-    private static final String PROP_CODENAME = "AndroidVersion.CodeName";  //$NON-NLS-1$
+    private static final String PROP_CODENAME = "AndroidVersion.CodeName";   //$NON-NLS-1$
 
     private final int mApiLevel;
     private final String mCodename;
 
     /**
      * Creates an {@link AndroidVersion} with the given api level and codename.
+     * Codename should be null for a release version, otherwise it's a preview codename.
      */
     public AndroidVersion(int apiLevel, String codename) {
         mApiLevel = apiLevel;
diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonPackage.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonPackage.java
index b28019f..2c9b3fb 100755
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonPackage.java
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonPackage.java
@@ -89,6 +89,8 @@
      * {@link IAndroidTarget#isPlatform()} false) from the {@link SdkManager}.

      * This is used to list local SDK folders in which case there is one archive which

      * URL is the actual target location.

+     * <p/>

+     * By design, this creates a package with one and only one archive.

      */

     AddonPackage(IAndroidTarget target, Properties props) {

         super(  null,                       //source

diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/DocPackage.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/DocPackage.java
index 75879b8..e54b550 100755
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/DocPackage.java
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/DocPackage.java
@@ -56,6 +56,8 @@
      * Manually create a new package with one archive and the given attributes.

      * This is used to create packages from local directories in which case there must be

      * one archive which URL is the actual target location.

+     * <p/>

+     * By design, this creates a package with one and only one archive.

      */

     DocPackage(RepoSource source,

             Properties props,

diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ExtraPackage.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ExtraPackage.java
index 74e1c59..587be1d 100755
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ExtraPackage.java
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ExtraPackage.java
@@ -72,6 +72,8 @@
      * Manually create a new package with one archive and the given attributes or properties.

      * This is used to create packages from local directories in which case there must be

      * one archive which URL is the actual target location.

+     * <p/>

+     * By design, this creates a package with one and only one archive.

      */

     ExtraPackage(RepoSource source,

             Properties props,

diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Package.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Package.java
index 69d526b..657bb14 100755
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Package.java
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Package.java
@@ -96,8 +96,10 @@
      * Manually create a new package with one archive and the given attributes.

      * This is used to create packages from local directories in which case there must be

      * one archive which URL is the actual target location.

-     *

+     * <p/>

      * Properties from props are used first when possible, e.g. if props is non null.

+     * <p/>

+     * By design, this creates a package with one and only one archive.

      */

     public Package(

             RepoSource source,

diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/PlatformPackage.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/PlatformPackage.java
index e95656a..e3fb3f2 100755
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/PlatformPackage.java
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/PlatformPackage.java
@@ -35,8 +35,8 @@
  */

 public class PlatformPackage extends Package {

 

-    private static final String PROP_VERSION       = "Platform.Version";      //$NON-NLS-1$

-    private static final String PROP_MIN_TOOLS_REV = "Platform.MinToolsRev";  //$NON-NLS-1$

+    protected static final String PROP_VERSION       = "Platform.Version";      //$NON-NLS-1$

+    protected static final String PROP_MIN_TOOLS_REV = "Platform.MinToolsRev";  //$NON-NLS-1$

 

     /** The package version, for platform, add-on and doc packages. */

     private final AndroidVersion mVersion;

@@ -80,6 +80,8 @@
      * must have {@link IAndroidTarget#isPlatform()} true) from the {@link SdkManager}.

      * This is used to list local SDK folders in which case there is one archive which

      * URL is the actual target location.

+     * <p/>

+     * By design, this creates a package with one and only one archive.

      */

     PlatformPackage(IAndroidTarget target, Properties props) {

         super(  null,                       //source

diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ToolPackage.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ToolPackage.java
index ee13379..974d8ac 100755
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ToolPackage.java
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ToolPackage.java
@@ -45,6 +45,8 @@
      * Manually create a new package with one archive and the given attributes or properties.

      * This is used to create packages from local directories in which case there must be

      * one archive which URL is the actual target location.

+     * <p/>

+     * By design, this creates a package with one and only one archive.

      */

     ToolPackage(

             RepoSource source,

diff --git a/tools/sdkmanager/libs/sdklib/tests/com/android/sdklib/internal/repository/MockAddonPackage.java b/tools/sdkmanager/libs/sdklib/tests/com/android/sdklib/internal/repository/MockAddonPackage.java
new file mode 100755
index 0000000..4d4199f
--- /dev/null
+++ b/tools/sdkmanager/libs/sdklib/tests/com/android/sdklib/internal/repository/MockAddonPackage.java
@@ -0,0 +1,136 @@
+/*

+ * Copyright (C) 2009 The Android Open Source Project

+ *

+ * Licensed under the Apache License, Version 2.0 (the "License");

+ * you may not use this file except in compliance with the License.

+ * You may obtain a copy of the License at

+ *

+ *      http://www.apache.org/licenses/LICENSE-2.0

+ *

+ * Unless required by applicable law or agreed to in writing, software

+ * distributed under the License is distributed on an "AS IS" BASIS,

+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

+ * See the License for the specific language governing permissions and

+ * limitations under the License.

+ */

+

+package com.android.sdklib.internal.repository;

+

+import com.android.sdklib.AndroidVersion;

+import com.android.sdklib.IAndroidTarget;

+

+/**

+ * A mock {@link AddonPackage} for testing.

+ *

+ * By design, this package contains one and only one archive.

+ */

+public class MockAddonPackage extends AddonPackage {

+

+    /**

+     * Creates a {@link MockAddonTarget} with the requested base platform and addon revision

+     * and then a {@link MockAddonPackage} wrapping it.

+     *

+     * By design, this package contains one and only one archive.

+     */

+    public MockAddonPackage(MockPlatformPackage basePlatform, int revision) {

+        super(new MockAddonTarget(basePlatform.getTarget(), revision), null /*props*/);

+    }

+

+    /**

+     * A mock AddonTarget.

+     * This reimplements the minimum needed from the interface for our limited testing needs.

+     */

+    static class MockAddonTarget implements IAndroidTarget {

+

+        private final IAndroidTarget mParentTarget;

+        private final int mRevision;

+

+        public MockAddonTarget(IAndroidTarget parentTarget, int revision) {

+            mParentTarget = parentTarget;

+            mRevision = revision;

+        }

+

+        public String getClasspathName() {

+            return null;

+        }

+

+        public String getDefaultSkin() {

+            return null;

+        }

+

+        public String getDescription() {

+            return "mock addon target";

+        }

+

+        public String getFullName() {

+            return "mock addon target";

+        }

+

+        public String getLocation() {

+            return "";

+        }

+

+        public String getName() {

+            return "mock addon target";

+        }

+

+        public IOptionalLibrary[] getOptionalLibraries() {

+            return null;

+        }

+

+        public IAndroidTarget getParent() {

+            return mParentTarget;

+        }

+

+        public String getPath(int pathId) {

+            return null;

+        }

+

+        public String[] getPlatformLibraries() {

+            return null;

+        }

+

+        public int getRevision() {

+            return mRevision;

+        }

+

+        public String[] getSkins() {

+            return null;

+        }

+

+        public int getUsbVendorId() {

+            return 0;

+        }

+

+        public String getVendor() {

+            return null;

+        }

+

+        public AndroidVersion getVersion() {

+            return mParentTarget.getVersion();

+        }

+

+        public String getVersionName() {

+            return String.format("mock-addon-%1$d", getVersion().getApiLevel());

+        }

+

+        public String hashString() {

+            return getVersionName();

+        }

+

+        /** Returns false for an addon. */

+        public boolean isPlatform() {

+            return false;

+        }

+

+        public boolean isCompatibleBaseFor(IAndroidTarget target) {

+            throw new UnsupportedOperationException("Implement this as needed for tests");

+        }

+

+        public int compareTo(IAndroidTarget o) {

+            throw new UnsupportedOperationException("Implement this as needed for tests");

+        }

+

+    }

+

+}

diff --git a/tools/sdkmanager/libs/sdklib/tests/com/android/sdklib/internal/repository/MockPlatformPackage.java b/tools/sdkmanager/libs/sdklib/tests/com/android/sdklib/internal/repository/MockPlatformPackage.java
new file mode 100755
index 0000000..b840b82
--- /dev/null
+++ b/tools/sdkmanager/libs/sdklib/tests/com/android/sdklib/internal/repository/MockPlatformPackage.java
@@ -0,0 +1,169 @@
+/*

+ * Copyright (C) 2009 The Android Open Source Project

+ *

+ * Licensed under the Apache License, Version 2.0 (the "License");

+ * you may not use this file except in compliance with the License.

+ * You may obtain a copy of the License at

+ *

+ *      http://www.apache.org/licenses/LICENSE-2.0

+ *

+ * Unless required by applicable law or agreed to in writing, software

+ * distributed under the License is distributed on an "AS IS" BASIS,

+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

+ * See the License for the specific language governing permissions and

+ * limitations under the License.

+ */

+

+package com.android.sdklib.internal.repository;

+

+import com.android.sdklib.AndroidVersion;

+import com.android.sdklib.IAndroidTarget;

+

+import java.util.Properties;

+

+/**

+ * A mock {@link PlatformPackage} for testing.

+ *

+ * By design, this package contains one and only one archive.

+ */

+public class MockPlatformPackage extends PlatformPackage {

+

+    private final IAndroidTarget mTarget;

+

+    /**

+     * Creates a {@link MockPlatformTarget} with the requested API and revision

+     * and then a {@link MockPlatformPackage} wrapping it.

+     *

+     * By design, this package contains one and only one archive.

+     */

+    public MockPlatformPackage(int apiLevel, int revision) {

+        this(new MockPlatformTarget(apiLevel, revision), null /*props*/);

+    }

+

+    /**

+     * Creates a {@link MockPlatformTarget} with the requested API and revision

+     * and then a {@link MockPlatformPackage} wrapping it.

+     *

+     * Also sets the min-tools-rev of the platform.

+     *

+     * By design, this package contains one and only one archive.

+     */

+    public MockPlatformPackage(int apiLevel, int revision, int min_tools_rev) {

+        this(new MockPlatformTarget(apiLevel, revision), createProps(min_tools_rev));

+    }

+

+    /** A little trick to be able to capture the target new after passing it to the super. */

+    private MockPlatformPackage(IAndroidTarget target, Properties props) {

+        super(target, props);

+        mTarget = target;

+    }

+

+    private static Properties createProps(int min_tools_rev) {

+        Properties props = new Properties();

+        props.setProperty(PlatformPackage.PROP_MIN_TOOLS_REV, Integer.toString((min_tools_rev)));

+        return props;

+    }

+

+    public IAndroidTarget getTarget() {

+        return mTarget;

+    }

+

+    /**

+     * A mock PlatformTarget.

+     * This reimplements the minimum needed from the interface for our limited testing needs.

+     */

+    static class MockPlatformTarget implements IAndroidTarget {

+

+        private final int mApiLevel;

+        private final int mRevision;

+

+        public MockPlatformTarget(int apiLevel, int revision) {

+            mApiLevel = apiLevel;

+            mRevision = revision;

+

+        }

+

+        public String getClasspathName() {

+            return null;

+        }

+

+        public String getDefaultSkin() {

+            return null;

+        }

+

+        public String getDescription() {

+            return "mock platform target";

+        }

+

+        public String getFullName() {

+            return "mock platform target";

+        }

+

+        public String getLocation() {

+            return "";

+        }

+

+        public String getName() {

+            return "mock platform target";

+        }

+

+        public IOptionalLibrary[] getOptionalLibraries() {

+            return null;

+        }

+

+        public IAndroidTarget getParent() {

+            return null;

+        }

+

+        public String getPath(int pathId) {

+            return null;

+        }

+

+        public String[] getPlatformLibraries() {

+            return null;

+        }

+

+        public int getRevision() {

+            return mRevision;

+        }

+

+        public String[] getSkins() {

+            return null;

+        }

+

+        public int getUsbVendorId() {

+            return 0;

+        }

+

+        public String getVendor() {

+            return null;

+        }

+

+        public AndroidVersion getVersion() {

+            return new AndroidVersion(mApiLevel, null /*codename*/);

+        }

+

+        public String getVersionName() {

+            return String.format("android-%1$d", mApiLevel);

+        }

+

+        public String hashString() {

+            return getVersionName();

+        }

+

+        /** Returns true for a platform. */

+        public boolean isPlatform() {

+            return true;

+        }

+

+        public boolean isCompatibleBaseFor(IAndroidTarget target) {

+            throw new UnsupportedOperationException("Implement this as needed for tests");

+        }

+

+        public int compareTo(IAndroidTarget o) {

+            throw new UnsupportedOperationException("Implement this as needed for tests");

+        }

+

+    }

+

+}

diff --git a/tools/sdkmanager/libs/sdklib/tests/com/android/sdklib/internal/repository/MockToolPackage.java b/tools/sdkmanager/libs/sdklib/tests/com/android/sdklib/internal/repository/MockToolPackage.java
new file mode 100755
index 0000000..e1ce621
--- /dev/null
+++ b/tools/sdkmanager/libs/sdklib/tests/com/android/sdklib/internal/repository/MockToolPackage.java
@@ -0,0 +1,48 @@
+/*

+ * Copyright (C) 2009 The Android Open Source Project

+ *

+ * Licensed under the Apache License, Version 2.0 (the "License");

+ * you may not use this file except in compliance with the License.

+ * You may obtain a copy of the License at

+ *

+ *      http://www.apache.org/licenses/LICENSE-2.0

+ *

+ * Unless required by applicable law or agreed to in writing, software

+ * distributed under the License is distributed on an "AS IS" BASIS,

+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

+ * See the License for the specific language governing permissions and

+ * limitations under the License.

+ */

+

+package com.android.sdklib.internal.repository;

+

+import com.android.sdklib.internal.repository.Archive.Arch;

+import com.android.sdklib.internal.repository.Archive.Os;

+

+/**

+ * A mock {@link ToolPackage} for testing.

+ *

+ * By design, this package contains one and only one archive.

+ */

+public class MockToolPackage extends ToolPackage {

+

+    /**

+     * Creates a {@link MockToolPackage} with the given revision and hardcoded defaults

+     * for everything else.

+     * <p/>

+     * By design, this creates a package with one and only one archive.

+     */

+    public MockToolPackage(int revision) {

+        super(

+            null, // source,

+            null, // props,

+            revision,

+            null, // license,

+            "desc", // description,

+            "url", // descUrl,

+            Os.getCurrentOs(), // archiveOs,

+            Arch.getCurrentArch(), // archiveArch,

+            "foo" // archiveOsPath

+            );

+    }

+}

diff --git a/tools/sdkmanager/libs/sdkuilib/.classpath b/tools/sdkmanager/libs/sdkuilib/.classpath
index 5a1790f..73592c4 100644
--- a/tools/sdkmanager/libs/sdkuilib/.classpath
+++ b/tools/sdkmanager/libs/sdkuilib/.classpath
@@ -1,9 +1,11 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<classpath>
-	<classpathentry kind="src" path="src"/>
-	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
-	<classpathentry exported="true" kind="con" path="org.eclipse.jdt.USER_LIBRARY/ANDROID_SWT"/>
-	<classpathentry combineaccessrules="false" kind="src" path="/SdkLib"/>
-	<classpathentry combineaccessrules="false" kind="src" path="/AndroidPrefs"/>
-	<classpathentry kind="output" path="bin"/>
-</classpath>
+<?xml version="1.0" encoding="UTF-8"?>

+<classpath>

+	<classpathentry kind="src" path="src"/>

+	<classpathentry kind="src" path="tests"/>

+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>

+	<classpathentry exported="true" kind="con" path="org.eclipse.jdt.USER_LIBRARY/ANDROID_SWT"/>

+	<classpathentry combineaccessrules="false" kind="src" path="/SdkLib"/>

+	<classpathentry combineaccessrules="false" kind="src" path="/AndroidPrefs"/>

+	<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/3"/>

+	<classpathentry kind="output" path="bin"/>

+</classpath>

diff --git a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/ArchiveInfo.java b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/ArchiveInfo.java
new file mode 100755
index 0000000..f603169
--- /dev/null
+++ b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/ArchiveInfo.java
@@ -0,0 +1,139 @@
+/*

+ * Copyright (C) 2009 The Android Open Source Project

+ *

+ * Licensed under the Apache License, Version 2.0 (the "License");

+ * you may not use this file except in compliance with the License.

+ * You may obtain a copy of the License at

+ *

+ *      http://www.apache.org/licenses/LICENSE-2.0

+ *

+ * Unless required by applicable law or agreed to in writing, software

+ * distributed under the License is distributed on an "AS IS" BASIS,

+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

+ * See the License for the specific language governing permissions and

+ * limitations under the License.

+ */

+

+package com.android.sdkuilib.internal.repository;

+

+import com.android.sdklib.internal.repository.Archive;

+

+import java.util.ArrayList;

+import java.util.Collection;

+

+/**

+ * Represents an archive that we want to install.

+ * Note that the installer deals with archives whereas the user mostly sees packages

+ * but as far as we are concerned for installation there's a 1-to-1 mapping.

+ * <p/>

+ * A new archive is always a remote archive that needs to be downloaded and then

+ * installed. It can replace an existing local one. It can also depends on another

+ * (new or local) archive, which means the dependent archive needs to be successfully

+ * installed first. Finally this archive can also be a dependency for another one.

+ *

+ * @see ArchiveInfo#ArchiveInfo(Archive, Archive, ArchiveInfo)

+ */

+class ArchiveInfo {

+

+    private final Archive mNewArchive;

+    private final Archive mReplaced;

+    private final ArchiveInfo mDependsOn;

+    private final ArrayList<ArchiveInfo> mDependencyFor = new ArrayList<ArchiveInfo>();

+    private boolean mAccepted;

+    private boolean mRejected;

+

+    /**

+     *

+     * @param newArchive A "new archive" to be installed. This is always an archive

+     *          that comes from a remote site. This can not be null.

+     * @param replaced An optional local archive that the new one will replace.

+     *          Can be null if this archive does not replace anything.

+     * @param dependsOn An optional new or local dependency, that is an archive that

+     *          <em>this</em> archive depends upon. In other words, we can only install

+     *          this archive if the dependency has been successfully installed. It also

+     *          means we need to install the dependency first.

+     */

+    public ArchiveInfo(Archive newArchive, Archive replaced, ArchiveInfo dependsOn) {

+        mNewArchive = newArchive;

+        mReplaced = replaced;

+        mDependsOn = dependsOn;

+    }

+

+    /**

+     * Returns the "new archive" to be installed.

+     * This is always an archive that comes from a remote site.

+     */

+    public Archive getNewArchive() {

+        return mNewArchive;

+    }

+

+    /**

+     * Returns an optional local archive that the new one will replace.

+     * Can be null if this archive does not replace anything.

+     */

+    public Archive getReplaced() {

+        return mReplaced;

+    }

+

+    /**

+     * Returns an optional new or local dependency, that is an archive that <em>this</em>

+     * archive depends upon. In other words, we can only install this archive if the

+     * dependency has been successfully installed. It also means we need to install the

+     * dependency first.

+     */

+    public ArchiveInfo getDependsOn() {

+        return mDependsOn;

+    }

+

+    /**

+     * Returns true if this new archive is a dependency for <em>another</em> one that we

+     * want to install.

+     */

+    public boolean isDependencyFor() {

+        return mDependencyFor.size() > 0;

+    }

+

+    /**

+     * Set to true if this new archive is a dependency for <em>another</em> one that we

+     * want to install.

+     */

+    public void addDependencyFor(ArchiveInfo dependencyFor) {

+        mDependencyFor.add(dependencyFor);

+    }

+

+    public Collection<ArchiveInfo> getDependenciesFor() {

+        return mDependencyFor;

+    }

+

+    /**

+     * Sets whether this archive was accepted (either manually by the user or

+     * automatically if it doesn't have a license) for installation.

+     */

+    public void setAccepted(boolean accepted) {

+        mAccepted = accepted;

+    }

+

+    /**

+     * Returns whether this archive was accepted (either manually by the user or

+     * automatically if it doesn't have a license) for installation.

+     */

+    public boolean isAccepted() {

+        return mAccepted;

+    }

+

+    /**

+     * Sets whether this archive was rejected manually by the user.

+     * An archive can neither accepted nor rejected.

+     */

+    public void setRejected(boolean rejected) {

+        mRejected = rejected;

+    }

+

+    /**

+     * Returns whether this archive was rejected manually by the user.

+     * An archive can neither accepted nor rejected.

+     */

+    public boolean isRejected() {

+        return mRejected;

+    }

+}

diff --git a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdateChooserDialog.java b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdateChooserDialog.java
index d7d3a90..5b8da83 100755
--- a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdateChooserDialog.java
+++ b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdateChooserDialog.java
@@ -52,11 +52,6 @@
 import org.eclipse.swt.widgets.TableColumn;

 

 import java.util.ArrayList;

-import java.util.Collection;

-import java.util.Comparator;

-import java.util.HashSet;

-import java.util.Map;

-import java.util.TreeMap;

 

 

 /**

@@ -64,21 +59,16 @@
  */

 final class UpdateChooserDialog extends Dialog {

 

-    /**

-     * Min Y location for dialog. Need to deal with the menu bar on mac os.

-     */

-    private final static int MIN_Y = SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN ?

-            20 : 0;

+    /** Min Y location for dialog. Need to deal with the menu bar on mac os. */

+    private final static int MIN_Y =

+        SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN ? 20 : 0;

 

     /** Last dialog size for this session. */

     private static Point sLastSize;

+    private boolean mCancelled = true;

     private boolean mCompleted;

-    private final Map<Archive, Archive> mNewToOldArchiveMap;

     private boolean mLicenseAcceptAll;

     private boolean mInternalLicenseRadioUpdate;

-    private HashSet<Archive> mAccepted = new HashSet<Archive>();

-    private HashSet<Archive> mRejected = new HashSet<Archive>();

-    private ArrayList<Archive> mResult = new ArrayList<Archive>();

 

     // UI fields

     private Shell mDialogShell;

@@ -96,40 +86,56 @@
     private Group mPackageTextGroup;

     private final UpdaterData mUpdaterData;

     private Group mTableGroup;

+    private Label mErrorLabel;

+

+    /**

+     * List of all archives to be installed with dependency information.

+     *

+     * Note: in a lot of cases, we need to find the archive info for a given archive. This

+     * is currently done using a simple linear search, which is fine since we only have a very

+     * limited number of archives to deal with (e.g. < 10 now). We might want to revisit

+     * this later if it becomes an issue. Right now just do the simple thing.

+     *

+     * Typically we could add a map Archive=>ArchiveInfo later.

+     */

+    private final ArrayList<ArchiveInfo> mArchives;

+

 

 

     /**

      * Create the dialog.

      * @param parentShell The shell to use, typically updaterData.getWindowShell()

      * @param updaterData The updater data

-     * @param newToOldUpdates The map [new archive => old archive] of potential updates

+     * @param archives The archives to be installed

      */

     public UpdateChooserDialog(Shell parentShell,

             UpdaterData updaterData,

-            Map<Archive, Archive> newToOldUpdates) {

+            ArrayList<ArchiveInfo> archives) {

         super(parentShell,

               SWT.APPLICATION_MODAL);

         mUpdaterData = updaterData;

-

-        mNewToOldArchiveMap = new TreeMap<Archive, Archive>(new Comparator<Archive>() {

-            public int compare(Archive a1, Archive a2) {

-                // The items are archive but what we show are packages so we'll

-                // sort of packages short descriptions

-                String desc1 = a1.getParentPackage().getShortDescription();

-                String desc2 = a2.getParentPackage().getShortDescription();

-                return desc1.compareTo(desc2);

-            }

-        });

-        mNewToOldArchiveMap.putAll(newToOldUpdates);

+        mArchives = archives;

     }

 

     /**

      * Returns the results, i.e. the list of selected new archives to install.

-     * The list is always non null. It is empty when cancel is selected or when

-     * all potential updates have been refused.

+     * This is similar to the {@link ArchiveInfo} list instance given to the constructor

+     * except only accepted archives are present.

+     *

+     * An empty list is returned if cancel was choosen.

      */

-    public Collection<Archive> getResult() {

-        return mResult;

+    public ArrayList<ArchiveInfo> getResult() {

+        ArrayList<ArchiveInfo> ais = new ArrayList<ArchiveInfo>();

+

+        if (!mCancelled) {

+            for (ArchiveInfo ai : mArchives) {

+                if (ai.isAccepted()) {

+                    ais.add(ai);

+                }

+            }

+        }

+

+        return ais;

     }

 

     /**

@@ -249,7 +255,14 @@
 

         mSashForm.setWeights(new int[] {200, 300});

 

-        // Bottom buttons

+        // Error message area

+

+        mErrorLabel = new Label(mDialogShell, SWT.NONE);

+        mErrorLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 3, 1));

+

+

+        // Bottom buttons area

+

         placeholder = new Label(mDialogShell, SWT.NONE);

         placeholder.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, true, false, 1, 1));

 

@@ -293,24 +306,19 @@
     private void postCreate() {

         setWindowImage();

 

-        // Automatically accept those with an empty license

-        for (Archive a : mNewToOldArchiveMap.keySet()) {

+        // Automatically accept those with an empty license or no license

+        for (ArchiveInfo ai : mArchives) {

+            Archive a = ai.getNewArchive();

+            assert a != null;

 

             String license = a.getParentPackage().getLicense();

-            if (license != null) {

-                license = license.trim();

-                if (license.length() == 0) {

-                    mAccepted.add(a);

-                }

-            } else {

-                mAccepted.add(a);

-            }

+            ai.setAccepted(license == null || license.trim().length() == 0);

         }

 

         // Fill the list with the replacement packages

         mTableViewPackage.setLabelProvider(new NewArchivesLabelProvider());

         mTableViewPackage.setContentProvider(new NewArchivesContentProvider());

-        mTableViewPackage.setInput(mNewToOldArchiveMap);

+        mTableViewPackage.setInput(mArchives);

 

         adjustColumnsWidth();

 

@@ -403,12 +411,10 @@
     }

 

     /**

-     * Callback invoked when the Install button is selected. Fills {@link #mResult} and

-     * completes the dialog.

+     * Callback invoked when the Install button is selected. Completes the dialog.

      */

     private void onInstallSelected() {

-        // get list of accepted items

-        mResult.addAll(mAccepted);

+        mCancelled = false;

         mCompleted = true;

     }

 

@@ -416,6 +422,7 @@
      * Callback invoked when the Cancel button is selected.

      */

     private void onCancelSelected() {

+        mCancelled = true;

         mCompleted = true;

     }

 

@@ -423,49 +430,123 @@
      * Callback invoked when a package item is selected in the list.

      */

     private void onPackageSelected() {

-        Archive a = getSelectedArchive();

-        displayInformation(a);

-        updateLicenceRadios(a);

+        ArchiveInfo ai = getSelectedArchive();

+        displayInformation(ai);

+        displayMissingDependency(ai);

+        updateLicenceRadios(ai);

     }

 

-    /** Returns the currently selected Archive or null. */

-    private Archive getSelectedArchive() {

+    /** Returns the currently selected {@link ArchiveInfo} or null. */

+    private ArchiveInfo getSelectedArchive() {

         ISelection sel = mTableViewPackage.getSelection();

         if (sel instanceof IStructuredSelection) {

             Object elem = ((IStructuredSelection) sel).getFirstElement();

-            if (elem instanceof Archive) {

-                return (Archive) elem;

+            if (elem instanceof ArchiveInfo) {

+                return (ArchiveInfo) elem;

             }

         }

         return null;

     }

 

-    private void displayInformation(Archive a) {

-        if (a == null) {

+    /**

+     * Updates the package description and license text depending on the selected package.

+     */

+    private void displayInformation(ArchiveInfo ai) {

+        if (ai == null) {

             mPackageText.setText("Please select a package.");

             return;

         }

 

-        mPackageText.setText("");                                               //$NON-NLS-1$

+        Archive anew = ai.getNewArchive();

+

+        mPackageText.setText("");                                                //$NON-NLS-1$

 

         addSectionTitle("Package Description\n");

-        addText(a.getParentPackage().getLongDescription(), "\n\n");             //$NON-NLS-1$

+        addText(anew.getParentPackage().getLongDescription(), "\n\n");          //$NON-NLS-1$

 

-        Archive aold = mNewToOldArchiveMap.get(a);

+        Archive aold = ai.getReplaced();

         if (aold != null) {

             addText(String.format("This update will replace revision %1$s with revision %2$s.\n\n",

                     aold.getParentPackage().getRevision(),

-                    a.getParentPackage().getRevision()));

+                    anew.getParentPackage().getRevision()));

         }

 

+        ArchiveInfo adep = ai.getDependsOn();

+        if (adep != null || ai.isDependencyFor()) {

+            addSectionTitle("Dependencies\n");

+

+            if (adep != null) {

+                addText(String.format("This package depends on %1$s.\n\n",

+                        adep.getNewArchive().getParentPackage().getShortDescription()));

+            }

+

+            if (ai.isDependencyFor()) {

+                addText("This package is a dependency for:");

+                for (ArchiveInfo ai2 : ai.getDependenciesFor()) {

+                    addText("\n- " +

+                            ai2.getNewArchive().getParentPackage().getShortDescription());

+                }

+                addText("\n\n");

+            }

+        }

 

         addSectionTitle("Archive Description\n");

-        addText(a.getLongDescription(), "\n\n");                                //$NON-NLS-1$

+        addText(anew.getLongDescription(), "\n\n");                             //$NON-NLS-1$

 

-        String license = a.getParentPackage().getLicense();

+        String license = anew.getParentPackage().getLicense();

         if (license != null) {

             addSectionTitle("License\n");

-            addText(license.trim(), "\n");                                      //$NON-NLS-1$

+            addText(license.trim(), "\n");                                       //$NON-NLS-1$

+        }

+    }

+

+    /**

+     * Computes and display missing dependency.

+     * If there's a selected package, check the dependency for that one.

+     * Otherwise display the first missing dependency.

+     */

+    private void displayMissingDependency(ArchiveInfo ai) {

+        String error = null;

+

+        try {

+            if (ai != null) {

+

+                if (!ai.isAccepted()) {

+                    // Case where this package blocks another one when not accepted

+                    for (ArchiveInfo ai2 : ai.getDependenciesFor()) {

+                        // It only matters if the blocked one is accepted

+                        if (ai2.isAccepted()) {

+                            error = String.format("Package '%1$s' depends on this one.",

+                                    ai2.getNewArchive().getParentPackage().getShortDescription());

+                            return;

+                        }

+                    }

+                } else {

+                    // Case where this package is accepted but blocked by another non-accepted one

+                    ArchiveInfo adep = ai.getDependsOn();

+                    if (adep != null && !adep.isAccepted()) {

+                        error = String.format("This package depends on '%1$s'.",

+                                adep.getNewArchive().getParentPackage().getShortDescription());

+                        return;

+                    }

+                }

+            }

+

+            // If there's no selection, just find the first missing dependency of any accepted

+            // package.

+            for (ArchiveInfo ai2 : mArchives) {

+                if (ai2.isAccepted()) {

+                    ArchiveInfo adep = ai.getDependsOn();

+                    if (adep != null && !adep.isAccepted()) {

+                        error = String.format("Package '%1$s' depends on '%2$s'",

+                                ai2.getNewArchive().getParentPackage().getShortDescription(),

+                                adep.getNewArchive().getParentPackage().getShortDescription());

+                        return;

+                    }

+                }

+            }

+        } finally {

+            mErrorLabel.setText(error == null ? "" : error);        //$NON-NLS-1$

         }

     }

 

@@ -488,25 +569,41 @@
         mPackageText.setStyleRange(sr);

     }

 

-    private void updateLicenceRadios(Archive a) {

+    private void updateLicenceRadios(ArchiveInfo ai) {

         if (mInternalLicenseRadioUpdate) {

             return;

         }

         mInternalLicenseRadioUpdate = true;

 

+        boolean oneAccepted = false;

+

         if (mLicenseAcceptAll) {

             mLicenseRadioAcceptAll.setSelection(true);

+            mLicenseRadioAccept.setEnabled(true);

+            mLicenseRadioReject.setEnabled(true);

             mLicenseRadioAccept.setSelection(false);

             mLicenseRadioReject.setSelection(false);

         } else {

             mLicenseRadioAcceptAll.setSelection(false);

-            mLicenseRadioAccept.setSelection(mAccepted.contains(a));

-            mLicenseRadioReject.setSelection(mRejected.contains(a));

+            oneAccepted = ai != null && ai.isAccepted();

+            mLicenseRadioAccept.setEnabled(ai != null);

+            mLicenseRadioReject.setEnabled(ai != null);

+            mLicenseRadioAccept.setSelection(oneAccepted);

+            mLicenseRadioReject.setSelection(ai != null && ai.isRejected());

         }

 

-        // The install button is enabled if there's at least one

-        // package accepted.

-        mInstallButton.setEnabled(mAccepted.size() > 0);

+        // The install button is enabled if there's at least one package accepted.

+        // If the current one isn't, look for another one.

+        boolean missing = mErrorLabel.getText() != null && mErrorLabel.getText().length() > 0;

+        if (!missing && !oneAccepted) {

+            for(ArchiveInfo ai2 : mArchives) {

+                if (ai2.isAccepted()) {

+                    oneAccepted = true;

+                    break;

+                }

+            }

+        }

+        mInstallButton.setEnabled(!missing && oneAccepted);

 

         mInternalLicenseRadioUpdate = false;

     }

@@ -523,26 +620,28 @@
         }

         mInternalLicenseRadioUpdate = true;

 

-        Archive a = getSelectedArchive();

+        ArchiveInfo ai = getSelectedArchive();

         boolean needUpdate = true;

 

         if (!mLicenseAcceptAll && mLicenseRadioAcceptAll.getSelection()) {

             // Accept all has been switched on. Mark all packages as accepted

             mLicenseAcceptAll = true;

-            mAccepted.addAll(mNewToOldArchiveMap.keySet());

-            mRejected.clear();

+            for(ArchiveInfo ai2 : mArchives) {

+                ai2.setAccepted(true);

+                ai2.setRejected(false);

+            }

 

         } else if (mLicenseRadioAccept.getSelection()) {

             // Accept only this one

             mLicenseAcceptAll = false;

-            mAccepted.add(a);

-            mRejected.remove(a);

+            ai.setAccepted(true);

+            ai.setRejected(false);

 

         } else if (mLicenseRadioReject.getSelection()) {

             // Reject only this one

             mLicenseAcceptAll = false;

-            mAccepted.remove(a);

-            mRejected.add(a);

+            ai.setAccepted(false);

+            ai.setRejected(true);

 

         } else {

             needUpdate = false;

@@ -554,9 +653,10 @@
             if (mLicenseAcceptAll) {

                 mTableViewPackage.refresh();

             } else {

-               mTableViewPackage.refresh(a);

+               mTableViewPackage.refresh(ai);

             }

-            updateLicenceRadios(a);

+            displayMissingDependency(ai);

+            updateLicenceRadios(ai);

         }

     }

 

@@ -564,32 +664,29 @@
      * Callback invoked when a package item is double-clicked in the list.

      */

     private void onPackageDoubleClick() {

-        Archive a = getSelectedArchive();

+        ArchiveInfo ai = getSelectedArchive();

 

-        if (mAccepted.contains(a)) {

-            // toggle from accepted to rejected

-            mAccepted.remove(a);

-            mRejected.add(a);

-        } else {

-            // toggle from rejected or unknown to accepted

-            mAccepted.add(a);

-            mRejected.remove(a);

-        }

+        boolean wasAccepted = ai.isAccepted();

+        ai.setAccepted(!wasAccepted);

+        ai.setRejected(wasAccepted);

 

         // update state

         mLicenseAcceptAll = false;

-        mTableViewPackage.refresh(a);

-        updateLicenceRadios(a);

+        mTableViewPackage.refresh(ai);

+        updateLicenceRadios(ai);

     }

 

     private class NewArchivesLabelProvider extends LabelProvider {

         @Override

         public Image getImage(Object element) {

+            assert element instanceof ArchiveInfo;

+            ArchiveInfo ai = (ArchiveInfo) element;

+

             ImageFactory imgFactory = mUpdaterData.getImageFactory();

             if (imgFactory != null) {

-                if (mAccepted.contains(element)) {

+                if (ai.isAccepted()) {

                     return imgFactory.getImageByName("accept_icon16.png");

-                } else if (mRejected.contains(element)) {

+                } else if (ai.isRejected()) {

                     return imgFactory.getImageByName("reject_icon16.png");

                 }

                 return imgFactory.getImageByName("unknown_icon16.png");

@@ -599,10 +696,16 @@
 

         @Override

         public String getText(Object element) {

-            if (element instanceof Archive) {

-                return ((Archive) element).getParentPackage().getShortDescription();

+            assert element instanceof ArchiveInfo;

+            ArchiveInfo ai = (ArchiveInfo) element;

+

+            String desc = ai.getNewArchive().getParentPackage().getShortDescription();

+

+            if (ai.isDependencyFor()) {

+                desc += " [*]";

             }

-            return super.getText(element);

+

+            return desc;

         }

     }

 

@@ -613,11 +716,11 @@
         }

 

         public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {

-            // Ignore. The input is always mNewArchives

+            // Ignore. The input is always mArchives

         }

 

         public Object[] getElements(Object inputElement) {

-            return mNewToOldArchiveMap.keySet().toArray();

+            return mArchives.toArray();

         }

     }

 

diff --git a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterData.java b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterData.java
index bec00f8..1841421 100755
--- a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterData.java
+++ b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterData.java
@@ -30,7 +30,6 @@
 import com.android.sdklib.internal.repository.RepoSource;

 import com.android.sdklib.internal.repository.RepoSources;

 import com.android.sdklib.internal.repository.ToolPackage;

-import com.android.sdklib.internal.repository.Package.UpdateInfo;

 import com.android.sdkuilib.internal.repository.icons.ImageFactory;

 import com.android.sdkuilib.repository.UpdaterWindow.ISdkListener;

 

@@ -42,8 +41,7 @@
 import java.io.PrintStream;

 import java.util.ArrayList;

 import java.util.Collection;

-import java.util.HashMap;

-import java.util.Map;

+import java.util.HashSet;

 

 /**

  * Data shared between {@link UpdaterWindowImpl} and its pages.

@@ -268,9 +266,9 @@
      * Install the list of given {@link Archive}s. This is invoked by the user selecting some

      * packages in the remote page and then clicking "install selected".

      *

-     * @param archives The archives to install. Incompatible ones will be skipped.

+     * @param result The archives to install. Incompatible ones will be skipped.

      */

-    public void installArchives(final Collection<Archive> archives) {

+    public void installArchives(final ArrayList<ArchiveInfo> result) {

         if (mTaskFactory == null) {

             throw new IllegalArgumentException("Task Factory is null");

         }

@@ -281,14 +279,23 @@
             public void run(ITaskMonitor monitor) {

 

                 final int progressPerArchive = 2 * Archive.NUM_MONITOR_INC;

-                monitor.setProgressMax(archives.size() * progressPerArchive);

+                monitor.setProgressMax(result.size() * progressPerArchive);

                 monitor.setDescription("Preparing to install archives");

 

                 boolean installedAddon = false;

                 boolean installedTools = false;

 

+                // Mark all current local archives as already installed.

+                HashSet<Archive> installedArchives = new HashSet<Archive>();

+                for (Package p : getInstalledPackage()) {

+                    for (Archive a : p.getArchives()) {

+                        installedArchives.add(a);

+                    }

+                }

+

                 int numInstalled = 0;

-                for (Archive archive : archives) {

+                for (ArchiveInfo ai : result) {

+                    Archive archive = ai.getNewArchive();

 

                     int nextProgress = monitor.getProgress() + progressPerArchive;

                     try {

@@ -296,9 +303,24 @@
                             break;

                         }

 

+                        ArchiveInfo adep = ai.getDependsOn();

+                        if (adep != null && !installedArchives.contains(adep)) {

+                            // This archive depends on another one that was not installed.

+                            // Skip it.

+                            monitor.setResult("Skipping '%1$s'; it depends on '%2$s' which was not installed.",

+                                    archive.getParentPackage().getShortDescription(),

+                                    adep.getNewArchive().getParentPackage().getShortDescription());

+                        }

+

                         if (archive.install(mOsSdkRoot, forceHttp, mSdkManager, monitor)) {

+                            // We installed this archive.

+                            installedArchives.add(archive);

                             numInstalled++;

 

+                            // If this package was replacing an existing one, the old one

+                            // is no longer installed.

+                            installedArchives.remove(ai.getReplaced());

+

                             // Check if we successfully installed a tool or add-on package.

                             if (archive.getParentPackage() instanceof AddonPackage) {

                                 installedAddon = true;

@@ -435,28 +457,20 @@
             refreshSources(true);

         }

 

-        final Map<Archive, Archive> updates = findUpdates(selectedArchives);

+        UpdaterLogic ul = new UpdaterLogic();

+        ArrayList<ArchiveInfo> archives = ul.computeUpdates(

+                selectedArchives,

+                getSources(),

+                getLocalSdkParser().getPackages());

 

-        if (selectedArchives != null) {

-            // Not only we want to perform updates but we also want to install the

-            // selected archives. If they do not match an update, list them anyway

-            // except they map themselves to null (no "old" archive)

-            for (Archive a : selectedArchives) {

-                if (!updates.containsKey(a)) {

-                    updates.put(a, null);

-                }

-            }

-        }

-

-        UpdateChooserDialog dialog = new UpdateChooserDialog(getWindowShell(), this, updates);

+        UpdateChooserDialog dialog = new UpdateChooserDialog(getWindowShell(), this, archives);

         dialog.open();

 

-        Collection<Archive> result = dialog.getResult();

+        ArrayList<ArchiveInfo> result = dialog.getResult();

         if (result != null && result.size() > 0) {

             installArchives(result);

         }

     }

-

     /**

      * Refresh all sources. This is invoked either internally (reusing an existing monitor)

      * or as a UI callback on the remote page "Refresh" button (in which case the monitor is

@@ -485,108 +499,4 @@
             }

         });

     }

-

-    /**

-     * Check the local archives vs the remote available packages to find potential updates.

-     * Return a map [remote archive => local archive] of suitable update candidates.

-     * Returns null if there's an unexpected error. Otherwise returns a map that can be

-     * empty but not null.

-     *

-     * @param selectedArchives The list of remote archive to consider for the update.

-     *  This can be null, in which case a list of remote archive is fetched from all

-     *  available sources.

-     */

-    private Map<Archive, Archive> findUpdates(Collection<Archive> selectedArchives) {

-        // Map [remote archive => local archive] of suitable update candidates

-        Map<Archive, Archive> result = new HashMap<Archive, Archive>();

-

-        // First go thru all sources and make a list of all available remote archives

-        // sorted by package class.

-        HashMap<Class<? extends Package>, ArrayList<Archive>> availablePkgs =

-            new HashMap<Class<? extends Package>, ArrayList<Archive>>();

-

-        if (selectedArchives != null) {

-            // Only consider the archives given

-

-            for (Archive a : selectedArchives) {

-                // Only add compatible archives

-                if (a.isCompatible()) {

-                    Class<? extends Package> clazz = a.getParentPackage().getClass();

-

-                    ArrayList<Archive> list = availablePkgs.get(clazz);

-                    if (list == null) {

-                        availablePkgs.put(clazz, list = new ArrayList<Archive>());

-                    }

-

-                    list.add(a);

-                }

-            }

-

-        } else {

-            // Get all the available archives from all loaded sources

-            RepoSource[] remoteSources = getSources().getSources();

-

-            for (RepoSource remoteSrc : remoteSources) {

-                Package[] remotePkgs = remoteSrc.getPackages();

-                if (remotePkgs != null) {

-                    for (Package remotePkg : remotePkgs) {

-                        Class<? extends Package> clazz = remotePkg.getClass();

-

-                        ArrayList<Archive> list = availablePkgs.get(clazz);

-                        if (list == null) {

-                            availablePkgs.put(clazz, list = new ArrayList<Archive>());

-                        }

-

-                        for (Archive a : remotePkg.getArchives()) {

-                            // Only add compatible archives

-                            if (a.isCompatible()) {

-                                list.add(a);

-                            }

-                        }

-                    }

-                }

-            }

-        }

-

-        Package[] localPkgs = getLocalSdkParser().getPackages();

-        if (localPkgs == null) {

-            // This is unexpected. The local sdk parser should have been called first.

-            return null;

-        }

-

-        for (Package localPkg : localPkgs) {

-            // get the available archive list for this package type

-            ArrayList<Archive> list = availablePkgs.get(localPkg.getClass());

-

-            // if this list is empty, we'll never find anything that matches

-            if (list == null || list.size() == 0) {

-                continue;

-            }

-

-            // local packages should have one archive at most

-            Archive[] localArchives = localPkg.getArchives();

-            if (localArchives != null && localArchives.length > 0) {

-                Archive localArchive = localArchives[0];

-                // only consider archives compatible with the current platform

-                if (localArchive != null && localArchive.isCompatible()) {

-

-                    // We checked all this archive stuff because that's what eventually gets

-                    // installed, but the "update" mechanism really works on packages. So now

-                    // the real question: is there a remote package that can update this

-                    // local package?

-

-                    for (Archive availArchive : list) {

-                        UpdateInfo info = localPkg.canBeUpdatedBy(availArchive.getParentPackage());

-                        if (info == UpdateInfo.UPDATE) {

-                            // Found one!

-                            result.put(availArchive, localArchive);

-                            break;

-                        }

-                    }

-                }

-            }

-        }

-

-        return result;

-    }

 }

diff --git a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterLogic.java b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterLogic.java
new file mode 100755
index 0000000..6d9935d
--- /dev/null
+++ b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterLogic.java
@@ -0,0 +1,323 @@
+/*

+ * Copyright (C) 2009 The Android Open Source Project

+ *

+ * Licensed under the Apache License, Version 2.0 (the "License");

+ * you may not use this file except in compliance with the License.

+ * You may obtain a copy of the License at

+ *

+ *      http://www.apache.org/licenses/LICENSE-2.0

+ *

+ * Unless required by applicable law or agreed to in writing, software

+ * distributed under the License is distributed on an "AS IS" BASIS,

+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

+ * See the License for the specific language governing permissions and

+ * limitations under the License.

+ */

+

+package com.android.sdkuilib.internal.repository;

+

+import com.android.sdklib.AndroidVersion;

+import com.android.sdklib.internal.repository.AddonPackage;

+import com.android.sdklib.internal.repository.Archive;

+import com.android.sdklib.internal.repository.Package;

+import com.android.sdklib.internal.repository.PlatformPackage;

+import com.android.sdklib.internal.repository.RepoSource;

+import com.android.sdklib.internal.repository.RepoSources;

+import com.android.sdklib.internal.repository.ToolPackage;

+import com.android.sdklib.internal.repository.Package.UpdateInfo;

+

+import java.util.ArrayList;

+import java.util.Collection;

+

+class UpdaterLogic {

+

+    private RepoSources mSources;

+

+    public ArrayList<ArchiveInfo> computeUpdates(

+            Collection<Archive> selectedArchives,

+            RepoSources sources,

+            Package[] localPkgs) {

+

+        mSources = sources;

+        ArrayList<ArchiveInfo> archives = new ArrayList<ArchiveInfo>();

+        ArrayList<Package> remotePkgs = new ArrayList<Package>();

+

+        if (selectedArchives == null) {

+            selectedArchives = findUpdates(localPkgs, remotePkgs);

+        }

+

+        for (Archive a : selectedArchives) {

+            insertArchive(a, archives, selectedArchives, remotePkgs, localPkgs, false);

+        }

+

+        return archives;

+    }

+

+

+    /**

+     * Find suitable updates to all current local packages.

+     */

+    private Collection<Archive> findUpdates(Package[] localPkgs, ArrayList<Package> remotePkgs) {

+        ArrayList<Archive> updates = new ArrayList<Archive>();

+

+        fetchRemotePackages(remotePkgs);

+

+        for (Package localPkg : localPkgs) {

+            for (Package remotePkg : remotePkgs) {

+                if (localPkg.canBeUpdatedBy(remotePkg) == UpdateInfo.UPDATE) {

+                    // Found a suitable update. Only accept the remote package

+                    // if it provides at least one compatible archive.

+

+                    for (Archive a : remotePkg.getArchives()) {

+                        if (a.isCompatible()) {

+                            updates.add(a);

+                            break;

+                        }

+                    }

+                }

+            }

+        }

+

+        return updates;

+    }

+

+    private ArchiveInfo insertArchive(Archive archive,

+            ArrayList<ArchiveInfo> outArchives,

+            Collection<Archive> selectedArchives,

+            ArrayList<Package> remotePkgs,

+            Package[] localPkgs,

+            boolean automated) {

+        Package p = archive.getParentPackage();

+

+        // Is this an update?

+        Archive updatedArchive = null;

+        for (Package lp : localPkgs) {

+            assert lp.getArchives().length == 1;

+            if (lp.getArchives().length > 0 && lp.canBeUpdatedBy(p) == UpdateInfo.UPDATE) {

+                updatedArchive = lp.getArchives()[0];

+            }

+        }

+

+        // find dependencies

+        ArchiveInfo dep = findDependency(p, outArchives, selectedArchives, remotePkgs, localPkgs);

+

+        ArchiveInfo ai = new ArchiveInfo(

+                archive, //newArchive

+                updatedArchive, //replaced

+                dep //dependsOn

+                );

+

+        outArchives.add(ai);

+        if (dep != null) {

+            dep.addDependencyFor(ai);

+        }

+

+        return ai;

+    }

+

+    private ArchiveInfo findDependency(Package pkg,

+            ArrayList<ArchiveInfo> outArchives,

+            Collection<Archive> selectedArchives,

+            ArrayList<Package> remotePkgs,

+            Package[] localPkgs) {

+

+        // Current dependencies can be:

+        // - addon: *always* depends on platform of same API level

+        // - platform: *might* depends on tools of rev >= min-tools-rev

+

+        if (pkg instanceof AddonPackage) {

+            AddonPackage addon = (AddonPackage) pkg;

+

+            return findAddonDependency(

+                    addon, outArchives, selectedArchives, remotePkgs, localPkgs);

+

+        } else if (pkg instanceof PlatformPackage) {

+            PlatformPackage platform = (PlatformPackage) pkg;

+

+            return findPlatformDependency(

+                    platform, outArchives, selectedArchives, remotePkgs, localPkgs);

+        }

+

+        return null;

+    }

+

+    /**

+     * A platform can have a min-tools-rev, in which case it depends on having a tools package

+     * of the requested revision.

+     * Finds the tools dependency. If found, add it to the list of things to install.

+     * Returns the archive info dependency, if any.

+     */

+    protected ArchiveInfo findPlatformDependency(PlatformPackage platform,

+            ArrayList<ArchiveInfo> outArchives,

+            Collection<Archive> selectedArchives,

+            ArrayList<Package> remotePkgs,

+            Package[] localPkgs) {

+        // This is the requirement to match.

+        int rev = platform.getMinToolsRevision();

+

+        if (rev == PlatformPackage.MIN_TOOLS_REV_NOT_SPECIFIED) {

+            // Well actually there's no requirement.

+            return null;

+        }

+

+        // First look in local packages.

+        for (Package p : localPkgs) {

+            if (p instanceof ToolPackage) {

+                if (((ToolPackage) p).getRevision() >= rev) {

+                    // We found one already installed. We don't report this dependency

+                    // as the UI only cares about resolving "newly added dependencies".

+                    return null;

+                }

+            }

+        }

+

+        // Look in archives already scheduled for install

+        for (ArchiveInfo ai : outArchives) {

+            Package p = ai.getNewArchive().getParentPackage();

+            if (p instanceof PlatformPackage) {

+                if (((ToolPackage) p).getRevision() >= rev) {

+                    // The dependency is already scheduled for install, nothing else to do.

+                    return ai;

+                }

+            }

+        }

+

+        // Otherwise look in the selected archives.

+        for (Archive a : selectedArchives) {

+            Package p = a.getParentPackage();

+            if (p instanceof ToolPackage) {

+                if (((ToolPackage) p).getRevision() >= rev) {

+                    // It's not already in the list of things to install, so add it now

+                    return insertArchive(a, outArchives,

+                            selectedArchives, remotePkgs, localPkgs,

+                            true);

+                }

+            }

+        }

+

+        // Finally nothing matched, so let's look at all available remote packages

+        fetchRemotePackages(remotePkgs);

+        for (Package p : remotePkgs) {

+            if (p instanceof ToolPackage) {

+                if (((ToolPackage) p).getRevision() >= rev) {

+                    // It's not already in the list of things to install, so add the

+                    // first compatible archive we can find.

+                    for (Archive a : p.getArchives()) {

+                        if (a.isCompatible()) {

+                            return insertArchive(a, outArchives,

+                                    selectedArchives, remotePkgs, localPkgs,

+                                    true);

+                        }

+                    }

+                }

+            }

+        }

+

+        // We end up here if nothing matches. We don't have a good tools to match.

+        // Seriously, that can't happens unless we totally screwed our repo manifest.

+        // We'll let this one go through anyway.

+        return null;

+    }

+

+    /**

+     * An addon depends on having a platform with the same API version.

+     * Finds the platform dependency. If found, add it to the list of things to install.

+     * Returns the archive info dependency, if any.

+     */

+    protected ArchiveInfo findAddonDependency(AddonPackage addon,

+            ArrayList<ArchiveInfo> outArchives,

+            Collection<Archive> selectedArchives,

+            ArrayList<Package> remotePkgs,

+            Package[] localPkgs) {

+        // This is the requirement to match.

+        AndroidVersion v = addon.getVersion();

+

+        // Find a platform that would satisfy the requirement.

+

+        // First look in local packages.

+        for (Package p : localPkgs) {

+            if (p instanceof PlatformPackage) {

+                if (v.equals(((PlatformPackage) p).getVersion())) {

+                    // We found one already installed. We don't report this dependency

+                    // as the UI only cares about resolving "newly added dependencies".

+                    return null;

+                }

+            }

+        }

+

+        // Look in archives already scheduled for install

+        for (ArchiveInfo ai : outArchives) {

+            Package p = ai.getNewArchive().getParentPackage();

+            if (p instanceof PlatformPackage) {

+                if (v.equals(((PlatformPackage) p).getVersion())) {

+                    // The dependency is already scheduled for install, nothing else to do.

+                    return ai;

+                }

+            }

+        }

+

+        // Otherwise look in the selected archives.

+        for (Archive a : selectedArchives) {

+            Package p = a.getParentPackage();

+            if (p instanceof PlatformPackage) {

+                if (v.equals(((PlatformPackage) p).getVersion())) {

+                    // It's not already in the list of things to install, so add it now

+                    return insertArchive(a, outArchives,

+                            selectedArchives, remotePkgs, localPkgs,

+                            true);

+                }

+            }

+        }

+

+        // Finally nothing matched, so let's look at all available remote packages

+        fetchRemotePackages(remotePkgs);

+        for (Package p : remotePkgs) {

+            if (p instanceof PlatformPackage) {

+                if (v.equals(((PlatformPackage) p).getVersion())) {

+                    // It's not already in the list of things to install, so add the

+                    // first compatible archive we can find.

+                    for (Archive a : p.getArchives()) {

+                        if (a.isCompatible()) {

+                            return insertArchive(a, outArchives,

+                                    selectedArchives, remotePkgs, localPkgs,

+                                    true);

+                        }

+                    }

+                }

+            }

+        }

+

+        // We end up here if nothing matches. We don't have a good platform to match.

+        // Seriously, that can't happens unless the repository contains a bogus addon

+        // entry that does not match any existing platform API level.

+        // It's conceivable that a 3rd part addon repo might have error, in which case

+        // we'll let this one go through anyway.

+        return null;

+    }

+

+    /** Fetch all remote packages only if really needed. */

+    protected void fetchRemotePackages(ArrayList<Package> remotePkgs) {

+        if (remotePkgs.size() > 0) {

+            return;

+        }

+

+        // Get all the available packages from all loaded sources

+        RepoSource[] remoteSources = mSources.getSources();

+

+        for (RepoSource remoteSrc : remoteSources) {

+            Package[] pkgs = remoteSrc.getPackages();

+            if (pkgs != null) {

+                nextPackage: for (Package pkg : pkgs) {

+                    for (Archive a : pkg.getArchives()) {

+                        // Only add a package if it contains at least one compatible archive

+                        if (a.isCompatible()) {

+                            remotePkgs.add(pkg);

+                            continue nextPackage;

+                        }

+                    }

+                }

+            }

+        }

+    }

+

+}

diff --git a/tools/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/UpdaterLogicTest.java b/tools/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/UpdaterLogicTest.java
new file mode 100755
index 0000000..bb1f52e
--- /dev/null
+++ b/tools/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/UpdaterLogicTest.java
@@ -0,0 +1,101 @@
+/*

+ * Copyright (C) 2009 The Android Open Source Project

+ *

+ * Licensed under the Apache License, Version 2.0 (the "License");

+ * you may not use this file except in compliance with the License.

+ * You may obtain a copy of the License at

+ *

+ *      http://www.apache.org/licenses/LICENSE-2.0

+ *

+ * Unless required by applicable law or agreed to in writing, software

+ * distributed under the License is distributed on an "AS IS" BASIS,

+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

+ * See the License for the specific language governing permissions and

+ * limitations under the License.

+ */

+

+package com.android.sdkuilib.internal.repository;

+

+import com.android.sdklib.internal.repository.Archive;

+import com.android.sdklib.internal.repository.MockAddonPackage;

+import com.android.sdklib.internal.repository.MockPlatformPackage;

+import com.android.sdklib.internal.repository.MockToolPackage;

+import com.android.sdklib.internal.repository.Package;

+

+import java.util.ArrayList;

+import java.util.Arrays;

+

+import junit.framework.TestCase;

+

+public class UpdaterLogicTest extends TestCase {

+

+    private static class MockUpdaterLogic extends UpdaterLogic {

+        private final Package[] mRemotePackages;

+

+        public MockUpdaterLogic(Package[] remotePackages) {

+            mRemotePackages = remotePackages;

+        }

+

+        @Override

+        protected void fetchRemotePackages(ArrayList<Package> remotePkgs) {

+            if (mRemotePackages != null) {

+                remotePkgs.addAll(Arrays.asList(mRemotePackages));

+            }

+        }

+    }

+

+    public void testFindAddonDependency() throws Exception {

+        MockUpdaterLogic mul = new MockUpdaterLogic(null);

+

+        MockPlatformPackage p1 = new MockPlatformPackage(1, 1);

+        MockPlatformPackage p2 = new MockPlatformPackage(2, 1);

+

+        MockAddonPackage a1 = new MockAddonPackage(p1, 1);

+        MockAddonPackage a2 = new MockAddonPackage(p2, 2);

+

+        ArrayList<ArchiveInfo> out = new ArrayList<ArchiveInfo>();

+        ArrayList<Archive> selected = new ArrayList<Archive>();

+        ArrayList<Package> remote = new ArrayList<Package>();

+

+        // a2 depends on p2, which is not in the locals

+        Package[] locals = { p1, a1 };

+        assertNull(mul.findAddonDependency(a2, out, selected, remote, locals));

+        assertEquals(0, out.size());

+

+        // p2 is now selected, and should be scheduled for install in out

+        Archive p2_archive = p2.getArchives()[0];

+        selected.add(p2_archive);

+        ArchiveInfo ai2 = mul.findAddonDependency(a2, out, selected, remote, locals);

+        assertNotNull(ai2);

+        assertSame(p2_archive, ai2.getNewArchive());

+        assertEquals(1, out.size());

+        assertSame(p2_archive, out.get(0).getNewArchive());

+    }

+

+    public void testFindPlatformDependency() throws Exception {

+        MockUpdaterLogic mul = new MockUpdaterLogic(null);

+

+        MockToolPackage t1 = new MockToolPackage(1);

+        MockToolPackage t2 = new MockToolPackage(2);

+

+        MockPlatformPackage p2 = new MockPlatformPackage(2, 1, 2);

+

+        ArrayList<ArchiveInfo> out = new ArrayList<ArchiveInfo>();

+        ArrayList<Archive> selected = new ArrayList<Archive>();

+        ArrayList<Package> remote = new ArrayList<Package>();

+

+        // p2 depends on t2, which is not locally installed

+        Package[] locals = { t1 };

+        assertNull(mul.findPlatformDependency(p2, out, selected, remote, locals));

+        assertEquals(0, out.size());

+

+        // t2 is now selected and can be used as a dependency

+        Archive t2_archive = t2.getArchives()[0];

+        selected.add(t2_archive);

+        ArchiveInfo ai2 = mul.findPlatformDependency(p2, out, selected, remote, locals);

+        assertNotNull(ai2);

+        assertSame(t2_archive, ai2.getNewArchive());

+        assertEquals(1, out.size());

+        assertSame(t2_archive, out.get(0).getNewArchive());

+    }

+}