OSDN Git Service

SDK Updater: platform dependency on tools, addon dependency on platform.
authorRaphael <raphael@google.com>
Wed, 7 Oct 2009 05:42:13 +0000 (22:42 -0700)
committerRaphael <raphael@google.com>
Thu, 8 Oct 2009 18:55:48 +0000 (11:55 -0700)
SDK BUG 2040986

Change-Id: Ica46d14939bb3a9bf499899a0bf571456d4c6017

16 files changed:
sdkmanager/libs/sdklib/src/com/android/sdklib/AndroidVersion.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonPackage.java
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/Package.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/PlatformPackage.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ToolPackage.java
sdkmanager/libs/sdklib/tests/com/android/sdklib/internal/repository/MockAddonPackage.java [new file with mode: 0755]
sdkmanager/libs/sdklib/tests/com/android/sdklib/internal/repository/MockPlatformPackage.java [new file with mode: 0755]
sdkmanager/libs/sdklib/tests/com/android/sdklib/internal/repository/MockToolPackage.java [new file with mode: 0755]
sdkmanager/libs/sdkuilib/.classpath
sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/ArchiveInfo.java [new file with mode: 0755]
sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdateChooserDialog.java
sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterData.java
sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterLogic.java [new file with mode: 0755]
sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/UpdaterLogicTest.java [new file with mode: 0755]

index ef62f6e..13c3ea1 100644 (file)
@@ -40,13 +40,14 @@ import java.util.Properties;
 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;
index b28019f..2c9b3fb 100755 (executable)
@@ -89,6 +89,8 @@ public class AddonPackage extends Package {
      * {@link IAndroidTarget#isPlatform()} false) from the {@link SdkManager}.\r
      * This is used to list local SDK folders in which case there is one archive which\r
      * URL is the actual target location.\r
+     * <p/>\r
+     * By design, this creates a package with one and only one archive.\r
      */\r
     AddonPackage(IAndroidTarget target, Properties props) {\r
         super(  null,                       //source\r
index 75879b8..e54b550 100755 (executable)
@@ -56,6 +56,8 @@ public class DocPackage extends Package {
      * Manually create a new package with one archive and the given attributes.\r
      * This is used to create packages from local directories in which case there must be\r
      * one archive which URL is the actual target location.\r
+     * <p/>\r
+     * By design, this creates a package with one and only one archive.\r
      */\r
     DocPackage(RepoSource source,\r
             Properties props,\r
index 74e1c59..587be1d 100755 (executable)
@@ -72,6 +72,8 @@ public class ExtraPackage extends Package {
      * Manually create a new package with one archive and the given attributes or properties.\r
      * This is used to create packages from local directories in which case there must be\r
      * one archive which URL is the actual target location.\r
+     * <p/>\r
+     * By design, this creates a package with one and only one archive.\r
      */\r
     ExtraPackage(RepoSource source,\r
             Properties props,\r
index 69d526b..657bb14 100755 (executable)
@@ -96,8 +96,10 @@ public abstract class Package implements IDescription {
      * Manually create a new package with one archive and the given attributes.\r
      * This is used to create packages from local directories in which case there must be\r
      * one archive which URL is the actual target location.\r
-     *\r
+     * <p/>\r
      * Properties from props are used first when possible, e.g. if props is non null.\r
+     * <p/>\r
+     * By design, this creates a package with one and only one archive.\r
      */\r
     public Package(\r
             RepoSource source,\r
index e95656a..e3fb3f2 100755 (executable)
@@ -35,8 +35,8 @@ import java.util.Properties;
  */\r
 public class PlatformPackage extends Package {\r
 \r
-    private static final String PROP_VERSION       = "Platform.Version";      //$NON-NLS-1$\r
-    private static final String PROP_MIN_TOOLS_REV = "Platform.MinToolsRev";  //$NON-NLS-1$\r
+    protected static final String PROP_VERSION       = "Platform.Version";      //$NON-NLS-1$\r
+    protected static final String PROP_MIN_TOOLS_REV = "Platform.MinToolsRev";  //$NON-NLS-1$\r
 \r
     /** The package version, for platform, add-on and doc packages. */\r
     private final AndroidVersion mVersion;\r
@@ -80,6 +80,8 @@ public class PlatformPackage extends Package {
      * must have {@link IAndroidTarget#isPlatform()} true) from the {@link SdkManager}.\r
      * This is used to list local SDK folders in which case there is one archive which\r
      * URL is the actual target location.\r
+     * <p/>\r
+     * By design, this creates a package with one and only one archive.\r
      */\r
     PlatformPackage(IAndroidTarget target, Properties props) {\r
         super(  null,                       //source\r
index ee13379..974d8ac 100755 (executable)
@@ -45,6 +45,8 @@ public class ToolPackage extends Package {
      * Manually create a new package with one archive and the given attributes or properties.\r
      * This is used to create packages from local directories in which case there must be\r
      * one archive which URL is the actual target location.\r
+     * <p/>\r
+     * By design, this creates a package with one and only one archive.\r
      */\r
     ToolPackage(\r
             RepoSource source,\r
diff --git a/sdkmanager/libs/sdklib/tests/com/android/sdklib/internal/repository/MockAddonPackage.java b/sdkmanager/libs/sdklib/tests/com/android/sdklib/internal/repository/MockAddonPackage.java
new file mode 100755 (executable)
index 0000000..4d4199f
--- /dev/null
@@ -0,0 +1,136 @@
+/*\r
+ * Copyright (C) 2009 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.AndroidVersion;\r
+import com.android.sdklib.IAndroidTarget;\r
+\r
+/**\r
+ * A mock {@link AddonPackage} for testing.\r
+ *\r
+ * By design, this package contains one and only one archive.\r
+ */\r
+public class MockAddonPackage extends AddonPackage {\r
+\r
+    /**\r
+     * Creates a {@link MockAddonTarget} with the requested base platform and addon revision\r
+     * and then a {@link MockAddonPackage} wrapping it.\r
+     *\r
+     * By design, this package contains one and only one archive.\r
+     */\r
+    public MockAddonPackage(MockPlatformPackage basePlatform, int revision) {\r
+        super(new MockAddonTarget(basePlatform.getTarget(), revision), null /*props*/);\r
+    }\r
+\r
+    /**\r
+     * A mock AddonTarget.\r
+     * This reimplements the minimum needed from the interface for our limited testing needs.\r
+     */\r
+    static class MockAddonTarget implements IAndroidTarget {\r
+\r
+        private final IAndroidTarget mParentTarget;\r
+        private final int mRevision;\r
+\r
+        public MockAddonTarget(IAndroidTarget parentTarget, int revision) {\r
+            mParentTarget = parentTarget;\r
+            mRevision = revision;\r
+        }\r
+\r
+        public String getClasspathName() {\r
+            return null;\r
+        }\r
+\r
+        public String getDefaultSkin() {\r
+            return null;\r
+        }\r
+\r
+        public String getDescription() {\r
+            return "mock addon target";\r
+        }\r
+\r
+        public String getFullName() {\r
+            return "mock addon target";\r
+        }\r
+\r
+        public String getLocation() {\r
+            return "";\r
+        }\r
+\r
+        public String getName() {\r
+            return "mock addon target";\r
+        }\r
+\r
+        public IOptionalLibrary[] getOptionalLibraries() {\r
+            return null;\r
+        }\r
+\r
+        public IAndroidTarget getParent() {\r
+            return mParentTarget;\r
+        }\r
+\r
+        public String getPath(int pathId) {\r
+            return null;\r
+        }\r
+\r
+        public String[] getPlatformLibraries() {\r
+            return null;\r
+        }\r
+\r
+        public int getRevision() {\r
+            return mRevision;\r
+        }\r
+\r
+        public String[] getSkins() {\r
+            return null;\r
+        }\r
+\r
+        public int getUsbVendorId() {\r
+            return 0;\r
+        }\r
+\r
+        public String getVendor() {\r
+            return null;\r
+        }\r
+\r
+        public AndroidVersion getVersion() {\r
+            return mParentTarget.getVersion();\r
+        }\r
+\r
+        public String getVersionName() {\r
+            return String.format("mock-addon-%1$d", getVersion().getApiLevel());\r
+        }\r
+\r
+        public String hashString() {\r
+            return getVersionName();\r
+        }\r
+\r
+        /** Returns false for an addon. */\r
+        public boolean isPlatform() {\r
+            return false;\r
+        }\r
+\r
+        public boolean isCompatibleBaseFor(IAndroidTarget target) {\r
+            throw new UnsupportedOperationException("Implement this as needed for tests");\r
+        }\r
+\r
+        public int compareTo(IAndroidTarget o) {\r
+            throw new UnsupportedOperationException("Implement this as needed for tests");\r
+        }\r
+\r
+    }\r
+\r
+}\r
diff --git a/sdkmanager/libs/sdklib/tests/com/android/sdklib/internal/repository/MockPlatformPackage.java b/sdkmanager/libs/sdklib/tests/com/android/sdklib/internal/repository/MockPlatformPackage.java
new file mode 100755 (executable)
index 0000000..b840b82
--- /dev/null
@@ -0,0 +1,169 @@
+/*\r
+ * Copyright (C) 2009 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.AndroidVersion;\r
+import com.android.sdklib.IAndroidTarget;\r
+\r
+import java.util.Properties;\r
+\r
+/**\r
+ * A mock {@link PlatformPackage} for testing.\r
+ *\r
+ * By design, this package contains one and only one archive.\r
+ */\r
+public class MockPlatformPackage extends PlatformPackage {\r
+\r
+    private final IAndroidTarget mTarget;\r
+\r
+    /**\r
+     * Creates a {@link MockPlatformTarget} with the requested API and revision\r
+     * and then a {@link MockPlatformPackage} wrapping it.\r
+     *\r
+     * By design, this package contains one and only one archive.\r
+     */\r
+    public MockPlatformPackage(int apiLevel, int revision) {\r
+        this(new MockPlatformTarget(apiLevel, revision), null /*props*/);\r
+    }\r
+\r
+    /**\r
+     * Creates a {@link MockPlatformTarget} with the requested API and revision\r
+     * and then a {@link MockPlatformPackage} wrapping it.\r
+     *\r
+     * Also sets the min-tools-rev of the platform.\r
+     *\r
+     * By design, this package contains one and only one archive.\r
+     */\r
+    public MockPlatformPackage(int apiLevel, int revision, int min_tools_rev) {\r
+        this(new MockPlatformTarget(apiLevel, revision), createProps(min_tools_rev));\r
+    }\r
+\r
+    /** A little trick to be able to capture the target new after passing it to the super. */\r
+    private MockPlatformPackage(IAndroidTarget target, Properties props) {\r
+        super(target, props);\r
+        mTarget = target;\r
+    }\r
+\r
+    private static Properties createProps(int min_tools_rev) {\r
+        Properties props = new Properties();\r
+        props.setProperty(PlatformPackage.PROP_MIN_TOOLS_REV, Integer.toString((min_tools_rev)));\r
+        return props;\r
+    }\r
+\r
+    public IAndroidTarget getTarget() {\r
+        return mTarget;\r
+    }\r
+\r
+    /**\r
+     * A mock PlatformTarget.\r
+     * This reimplements the minimum needed from the interface for our limited testing needs.\r
+     */\r
+    static class MockPlatformTarget implements IAndroidTarget {\r
+\r
+        private final int mApiLevel;\r
+        private final int mRevision;\r
+\r
+        public MockPlatformTarget(int apiLevel, int revision) {\r
+            mApiLevel = apiLevel;\r
+            mRevision = revision;\r
+\r
+        }\r
+\r
+        public String getClasspathName() {\r
+            return null;\r
+        }\r
+\r
+        public String getDefaultSkin() {\r
+            return null;\r
+        }\r
+\r
+        public String getDescription() {\r
+            return "mock platform target";\r
+        }\r
+\r
+        public String getFullName() {\r
+            return "mock platform target";\r
+        }\r
+\r
+        public String getLocation() {\r
+            return "";\r
+        }\r
+\r
+        public String getName() {\r
+            return "mock platform target";\r
+        }\r
+\r
+        public IOptionalLibrary[] getOptionalLibraries() {\r
+            return null;\r
+        }\r
+\r
+        public IAndroidTarget getParent() {\r
+            return null;\r
+        }\r
+\r
+        public String getPath(int pathId) {\r
+            return null;\r
+        }\r
+\r
+        public String[] getPlatformLibraries() {\r
+            return null;\r
+        }\r
+\r
+        public int getRevision() {\r
+            return mRevision;\r
+        }\r
+\r
+        public String[] getSkins() {\r
+            return null;\r
+        }\r
+\r
+        public int getUsbVendorId() {\r
+            return 0;\r
+        }\r
+\r
+        public String getVendor() {\r
+            return null;\r
+        }\r
+\r
+        public AndroidVersion getVersion() {\r
+            return new AndroidVersion(mApiLevel, null /*codename*/);\r
+        }\r
+\r
+        public String getVersionName() {\r
+            return String.format("android-%1$d", mApiLevel);\r
+        }\r
+\r
+        public String hashString() {\r
+            return getVersionName();\r
+        }\r
+\r
+        /** Returns true for a platform. */\r
+        public boolean isPlatform() {\r
+            return true;\r
+        }\r
+\r
+        public boolean isCompatibleBaseFor(IAndroidTarget target) {\r
+            throw new UnsupportedOperationException("Implement this as needed for tests");\r
+        }\r
+\r
+        public int compareTo(IAndroidTarget o) {\r
+            throw new UnsupportedOperationException("Implement this as needed for tests");\r
+        }\r
+\r
+    }\r
+\r
+}\r
diff --git a/sdkmanager/libs/sdklib/tests/com/android/sdklib/internal/repository/MockToolPackage.java b/sdkmanager/libs/sdklib/tests/com/android/sdklib/internal/repository/MockToolPackage.java
new file mode 100755 (executable)
index 0000000..e1ce621
--- /dev/null
@@ -0,0 +1,48 @@
+/*\r
+ * Copyright (C) 2009 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.internal.repository.Archive.Arch;\r
+import com.android.sdklib.internal.repository.Archive.Os;\r
+\r
+/**\r
+ * A mock {@link ToolPackage} for testing.\r
+ *\r
+ * By design, this package contains one and only one archive.\r
+ */\r
+public class MockToolPackage extends ToolPackage {\r
+\r
+    /**\r
+     * Creates a {@link MockToolPackage} with the given revision and hardcoded defaults\r
+     * for everything else.\r
+     * <p/>\r
+     * By design, this creates a package with one and only one archive.\r
+     */\r
+    public MockToolPackage(int revision) {\r
+        super(\r
+            null, // source,\r
+            null, // props,\r
+            revision,\r
+            null, // license,\r
+            "desc", // description,\r
+            "url", // descUrl,\r
+            Os.getCurrentOs(), // archiveOs,\r
+            Arch.getCurrentArch(), // archiveArch,\r
+            "foo" // archiveOsPath\r
+            );\r
+    }\r
+}\r
index 5a1790f..73592c4 100644 (file)
@@ -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"?>\r
+<classpath>\r
+       <classpathentry kind="src" path="src"/>\r
+       <classpathentry kind="src" path="tests"/>\r
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>\r
+       <classpathentry exported="true" kind="con" path="org.eclipse.jdt.USER_LIBRARY/ANDROID_SWT"/>\r
+       <classpathentry combineaccessrules="false" kind="src" path="/SdkLib"/>\r
+       <classpathentry combineaccessrules="false" kind="src" path="/AndroidPrefs"/>\r
+       <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/3"/>\r
+       <classpathentry kind="output" path="bin"/>\r
+</classpath>\r
diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/ArchiveInfo.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/ArchiveInfo.java
new file mode 100755 (executable)
index 0000000..f603169
--- /dev/null
@@ -0,0 +1,139 @@
+/*\r
+ * Copyright (C) 2009 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.sdkuilib.internal.repository;\r
+\r
+import com.android.sdklib.internal.repository.Archive;\r
+\r
+import java.util.ArrayList;\r
+import java.util.Collection;\r
+\r
+/**\r
+ * Represents an archive that we want to install.\r
+ * Note that the installer deals with archives whereas the user mostly sees packages\r
+ * but as far as we are concerned for installation there's a 1-to-1 mapping.\r
+ * <p/>\r
+ * A new archive is always a remote archive that needs to be downloaded and then\r
+ * installed. It can replace an existing local one. It can also depends on another\r
+ * (new or local) archive, which means the dependent archive needs to be successfully\r
+ * installed first. Finally this archive can also be a dependency for another one.\r
+ *\r
+ * @see ArchiveInfo#ArchiveInfo(Archive, Archive, ArchiveInfo)\r
+ */\r
+class ArchiveInfo {\r
+\r
+    private final Archive mNewArchive;\r
+    private final Archive mReplaced;\r
+    private final ArchiveInfo mDependsOn;\r
+    private final ArrayList<ArchiveInfo> mDependencyFor = new ArrayList<ArchiveInfo>();\r
+    private boolean mAccepted;\r
+    private boolean mRejected;\r
+\r
+    /**\r
+     *\r
+     * @param newArchive A "new archive" to be installed. This is always an archive\r
+     *          that comes from a remote site. This can not be null.\r
+     * @param replaced An optional local archive that the new one will replace.\r
+     *          Can be null if this archive does not replace anything.\r
+     * @param dependsOn An optional new or local dependency, that is an archive that\r
+     *          <em>this</em> archive depends upon. In other words, we can only install\r
+     *          this archive if the dependency has been successfully installed. It also\r
+     *          means we need to install the dependency first.\r
+     */\r
+    public ArchiveInfo(Archive newArchive, Archive replaced, ArchiveInfo dependsOn) {\r
+        mNewArchive = newArchive;\r
+        mReplaced = replaced;\r
+        mDependsOn = dependsOn;\r
+    }\r
+\r
+    /**\r
+     * Returns the "new archive" to be installed.\r
+     * This is always an archive that comes from a remote site.\r
+     */\r
+    public Archive getNewArchive() {\r
+        return mNewArchive;\r
+    }\r
+\r
+    /**\r
+     * Returns an optional local archive that the new one will replace.\r
+     * Can be null if this archive does not replace anything.\r
+     */\r
+    public Archive getReplaced() {\r
+        return mReplaced;\r
+    }\r
+\r
+    /**\r
+     * Returns an optional new or local dependency, that is an archive that <em>this</em>\r
+     * archive depends upon. In other words, we can only install this archive if the\r
+     * dependency has been successfully installed. It also means we need to install the\r
+     * dependency first.\r
+     */\r
+    public ArchiveInfo getDependsOn() {\r
+        return mDependsOn;\r
+    }\r
+\r
+    /**\r
+     * Returns true if this new archive is a dependency for <em>another</em> one that we\r
+     * want to install.\r
+     */\r
+    public boolean isDependencyFor() {\r
+        return mDependencyFor.size() > 0;\r
+    }\r
+\r
+    /**\r
+     * Set to true if this new archive is a dependency for <em>another</em> one that we\r
+     * want to install.\r
+     */\r
+    public void addDependencyFor(ArchiveInfo dependencyFor) {\r
+        mDependencyFor.add(dependencyFor);\r
+    }\r
+\r
+    public Collection<ArchiveInfo> getDependenciesFor() {\r
+        return mDependencyFor;\r
+    }\r
+\r
+    /**\r
+     * Sets whether this archive was accepted (either manually by the user or\r
+     * automatically if it doesn't have a license) for installation.\r
+     */\r
+    public void setAccepted(boolean accepted) {\r
+        mAccepted = accepted;\r
+    }\r
+\r
+    /**\r
+     * Returns whether this archive was accepted (either manually by the user or\r
+     * automatically if it doesn't have a license) for installation.\r
+     */\r
+    public boolean isAccepted() {\r
+        return mAccepted;\r
+    }\r
+\r
+    /**\r
+     * Sets whether this archive was rejected manually by the user.\r
+     * An archive can neither accepted nor rejected.\r
+     */\r
+    public void setRejected(boolean rejected) {\r
+        mRejected = rejected;\r
+    }\r
+\r
+    /**\r
+     * Returns whether this archive was rejected manually by the user.\r
+     * An archive can neither accepted nor rejected.\r
+     */\r
+    public boolean isRejected() {\r
+        return mRejected;\r
+    }\r
+}\r
index d7d3a90..5b8da83 100755 (executable)
@@ -52,11 +52,6 @@ import org.eclipse.swt.widgets.Table;
 import org.eclipse.swt.widgets.TableColumn;\r
 \r
 import java.util.ArrayList;\r
-import java.util.Collection;\r
-import java.util.Comparator;\r
-import java.util.HashSet;\r
-import java.util.Map;\r
-import java.util.TreeMap;\r
 \r
 \r
 /**\r
@@ -64,21 +59,16 @@ import java.util.TreeMap;
  */\r
 final class UpdateChooserDialog extends Dialog {\r
 \r
-    /**\r
-     * Min Y location for dialog. Need to deal with the menu bar on mac os.\r
-     */\r
-    private final static int MIN_Y = SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN ?\r
-            20 : 0;\r
+    /** Min Y location for dialog. Need to deal with the menu bar on mac os. */\r
+    private final static int MIN_Y =\r
+        SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN ? 20 : 0;\r
 \r
     /** Last dialog size for this session. */\r
     private static Point sLastSize;\r
+    private boolean mCancelled = true;\r
     private boolean mCompleted;\r
-    private final Map<Archive, Archive> mNewToOldArchiveMap;\r
     private boolean mLicenseAcceptAll;\r
     private boolean mInternalLicenseRadioUpdate;\r
-    private HashSet<Archive> mAccepted = new HashSet<Archive>();\r
-    private HashSet<Archive> mRejected = new HashSet<Archive>();\r
-    private ArrayList<Archive> mResult = new ArrayList<Archive>();\r
 \r
     // UI fields\r
     private Shell mDialogShell;\r
@@ -96,40 +86,56 @@ final class UpdateChooserDialog extends Dialog {
     private Group mPackageTextGroup;\r
     private final UpdaterData mUpdaterData;\r
     private Group mTableGroup;\r
+    private Label mErrorLabel;\r
+\r
+    /**\r
+     * List of all archives to be installed with dependency information.\r
+     *\r
+     * Note: in a lot of cases, we need to find the archive info for a given archive. This\r
+     * is currently done using a simple linear search, which is fine since we only have a very\r
+     * limited number of archives to deal with (e.g. < 10 now). We might want to revisit\r
+     * this later if it becomes an issue. Right now just do the simple thing.\r
+     *\r
+     * Typically we could add a map Archive=>ArchiveInfo later.\r
+     */\r
+    private final ArrayList<ArchiveInfo> mArchives;\r
+\r
 \r
 \r
     /**\r
      * Create the dialog.\r
      * @param parentShell The shell to use, typically updaterData.getWindowShell()\r
      * @param updaterData The updater data\r
-     * @param newToOldUpdates The map [new archive => old archive] of potential updates\r
+     * @param archives The archives to be installed\r
      */\r
     public UpdateChooserDialog(Shell parentShell,\r
             UpdaterData updaterData,\r
-            Map<Archive, Archive> newToOldUpdates) {\r
+            ArrayList<ArchiveInfo> archives) {\r
         super(parentShell,\r
               SWT.APPLICATION_MODAL);\r
         mUpdaterData = updaterData;\r
-\r
-        mNewToOldArchiveMap = new TreeMap<Archive, Archive>(new Comparator<Archive>() {\r
-            public int compare(Archive a1, Archive a2) {\r
-                // The items are archive but what we show are packages so we'll\r
-                // sort of packages short descriptions\r
-                String desc1 = a1.getParentPackage().getShortDescription();\r
-                String desc2 = a2.getParentPackage().getShortDescription();\r
-                return desc1.compareTo(desc2);\r
-            }\r
-        });\r
-        mNewToOldArchiveMap.putAll(newToOldUpdates);\r
+        mArchives = archives;\r
     }\r
 \r
     /**\r
      * Returns the results, i.e. the list of selected new archives to install.\r
-     * The list is always non null. It is empty when cancel is selected or when\r
-     * all potential updates have been refused.\r
+     * This is similar to the {@link ArchiveInfo} list instance given to the constructor\r
+     * except only accepted archives are present.\r
+     *\r
+     * An empty list is returned if cancel was choosen.\r
      */\r
-    public Collection<Archive> getResult() {\r
-        return mResult;\r
+    public ArrayList<ArchiveInfo> getResult() {\r
+        ArrayList<ArchiveInfo> ais = new ArrayList<ArchiveInfo>();\r
+\r
+        if (!mCancelled) {\r
+            for (ArchiveInfo ai : mArchives) {\r
+                if (ai.isAccepted()) {\r
+                    ais.add(ai);\r
+                }\r
+            }\r
+        }\r
+\r
+        return ais;\r
     }\r
 \r
     /**\r
@@ -249,7 +255,14 @@ final class UpdateChooserDialog extends Dialog {
 \r
         mSashForm.setWeights(new int[] {200, 300});\r
 \r
-        // Bottom buttons\r
+        // Error message area\r
+\r
+        mErrorLabel = new Label(mDialogShell, SWT.NONE);\r
+        mErrorLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 3, 1));\r
+\r
+\r
+        // Bottom buttons area\r
+\r
         placeholder = new Label(mDialogShell, SWT.NONE);\r
         placeholder.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, true, false, 1, 1));\r
 \r
@@ -293,24 +306,19 @@ final class UpdateChooserDialog extends Dialog {
     private void postCreate() {\r
         setWindowImage();\r
 \r
-        // Automatically accept those with an empty license\r
-        for (Archive a : mNewToOldArchiveMap.keySet()) {\r
+        // Automatically accept those with an empty license or no license\r
+        for (ArchiveInfo ai : mArchives) {\r
+            Archive a = ai.getNewArchive();\r
+            assert a != null;\r
 \r
             String license = a.getParentPackage().getLicense();\r
-            if (license != null) {\r
-                license = license.trim();\r
-                if (license.length() == 0) {\r
-                    mAccepted.add(a);\r
-                }\r
-            } else {\r
-                mAccepted.add(a);\r
-            }\r
+            ai.setAccepted(license == null || license.trim().length() == 0);\r
         }\r
 \r
         // Fill the list with the replacement packages\r
         mTableViewPackage.setLabelProvider(new NewArchivesLabelProvider());\r
         mTableViewPackage.setContentProvider(new NewArchivesContentProvider());\r
-        mTableViewPackage.setInput(mNewToOldArchiveMap);\r
+        mTableViewPackage.setInput(mArchives);\r
 \r
         adjustColumnsWidth();\r
 \r
@@ -403,12 +411,10 @@ final class UpdateChooserDialog extends Dialog {
     }\r
 \r
     /**\r
-     * Callback invoked when the Install button is selected. Fills {@link #mResult} and\r
-     * completes the dialog.\r
+     * Callback invoked when the Install button is selected. Completes the dialog.\r
      */\r
     private void onInstallSelected() {\r
-        // get list of accepted items\r
-        mResult.addAll(mAccepted);\r
+        mCancelled = false;\r
         mCompleted = true;\r
     }\r
 \r
@@ -416,6 +422,7 @@ final class UpdateChooserDialog extends Dialog {
      * Callback invoked when the Cancel button is selected.\r
      */\r
     private void onCancelSelected() {\r
+        mCancelled = true;\r
         mCompleted = true;\r
     }\r
 \r
@@ -423,49 +430,123 @@ final class UpdateChooserDialog extends Dialog {
      * Callback invoked when a package item is selected in the list.\r
      */\r
     private void onPackageSelected() {\r
-        Archive a = getSelectedArchive();\r
-        displayInformation(a);\r
-        updateLicenceRadios(a);\r
+        ArchiveInfo ai = getSelectedArchive();\r
+        displayInformation(ai);\r
+        displayMissingDependency(ai);\r
+        updateLicenceRadios(ai);\r
     }\r
 \r
-    /** Returns the currently selected Archive or null. */\r
-    private Archive getSelectedArchive() {\r
+    /** Returns the currently selected {@link ArchiveInfo} or null. */\r
+    private ArchiveInfo getSelectedArchive() {\r
         ISelection sel = mTableViewPackage.getSelection();\r
         if (sel instanceof IStructuredSelection) {\r
             Object elem = ((IStructuredSelection) sel).getFirstElement();\r
-            if (elem instanceof Archive) {\r
-                return (Archive) elem;\r
+            if (elem instanceof ArchiveInfo) {\r
+                return (ArchiveInfo) elem;\r
             }\r
         }\r
         return null;\r
     }\r
 \r
-    private void displayInformation(Archive a) {\r
-        if (a == null) {\r
+    /**\r
+     * Updates the package description and license text depending on the selected package.\r
+     */\r
+    private void displayInformation(ArchiveInfo ai) {\r
+        if (ai == null) {\r
             mPackageText.setText("Please select a package.");\r
             return;\r
         }\r
 \r
-        mPackageText.setText("");                                               //$NON-NLS-1$\r
+        Archive anew = ai.getNewArchive();\r
+\r
+        mPackageText.setText("");                                                //$NON-NLS-1$\r
 \r
         addSectionTitle("Package Description\n");\r
-        addText(a.getParentPackage().getLongDescription(), "\n\n");             //$NON-NLS-1$\r
+        addText(anew.getParentPackage().getLongDescription(), "\n\n");          //$NON-NLS-1$\r
 \r
-        Archive aold = mNewToOldArchiveMap.get(a);\r
+        Archive aold = ai.getReplaced();\r
         if (aold != null) {\r
             addText(String.format("This update will replace revision %1$s with revision %2$s.\n\n",\r
                     aold.getParentPackage().getRevision(),\r
-                    a.getParentPackage().getRevision()));\r
+                    anew.getParentPackage().getRevision()));\r
         }\r
 \r
+        ArchiveInfo adep = ai.getDependsOn();\r
+        if (adep != null || ai.isDependencyFor()) {\r
+            addSectionTitle("Dependencies\n");\r
+\r
+            if (adep != null) {\r
+                addText(String.format("This package depends on %1$s.\n\n",\r
+                        adep.getNewArchive().getParentPackage().getShortDescription()));\r
+            }\r
+\r
+            if (ai.isDependencyFor()) {\r
+                addText("This package is a dependency for:");\r
+                for (ArchiveInfo ai2 : ai.getDependenciesFor()) {\r
+                    addText("\n- " +\r
+                            ai2.getNewArchive().getParentPackage().getShortDescription());\r
+                }\r
+                addText("\n\n");\r
+            }\r
+        }\r
 \r
         addSectionTitle("Archive Description\n");\r
-        addText(a.getLongDescription(), "\n\n");                                //$NON-NLS-1$\r
+        addText(anew.getLongDescription(), "\n\n");                             //$NON-NLS-1$\r
 \r
-        String license = a.getParentPackage().getLicense();\r
+        String license = anew.getParentPackage().getLicense();\r
         if (license != null) {\r
             addSectionTitle("License\n");\r
-            addText(license.trim(), "\n");                                      //$NON-NLS-1$\r
+            addText(license.trim(), "\n");                                       //$NON-NLS-1$\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Computes and display missing dependency.\r
+     * If there's a selected package, check the dependency for that one.\r
+     * Otherwise display the first missing dependency.\r
+     */\r
+    private void displayMissingDependency(ArchiveInfo ai) {\r
+        String error = null;\r
+\r
+        try {\r
+            if (ai != null) {\r
+\r
+                if (!ai.isAccepted()) {\r
+                    // Case where this package blocks another one when not accepted\r
+                    for (ArchiveInfo ai2 : ai.getDependenciesFor()) {\r
+                        // It only matters if the blocked one is accepted\r
+                        if (ai2.isAccepted()) {\r
+                            error = String.format("Package '%1$s' depends on this one.",\r
+                                    ai2.getNewArchive().getParentPackage().getShortDescription());\r
+                            return;\r
+                        }\r
+                    }\r
+                } else {\r
+                    // Case where this package is accepted but blocked by another non-accepted one\r
+                    ArchiveInfo adep = ai.getDependsOn();\r
+                    if (adep != null && !adep.isAccepted()) {\r
+                        error = String.format("This package depends on '%1$s'.",\r
+                                adep.getNewArchive().getParentPackage().getShortDescription());\r
+                        return;\r
+                    }\r
+                }\r
+            }\r
+\r
+            // If there's no selection, just find the first missing dependency of any accepted\r
+            // package.\r
+            for (ArchiveInfo ai2 : mArchives) {\r
+                if (ai2.isAccepted()) {\r
+                    ArchiveInfo adep = ai.getDependsOn();\r
+                    if (adep != null && !adep.isAccepted()) {\r
+                        error = String.format("Package '%1$s' depends on '%2$s'",\r
+                                ai2.getNewArchive().getParentPackage().getShortDescription(),\r
+                                adep.getNewArchive().getParentPackage().getShortDescription());\r
+                        return;\r
+                    }\r
+                }\r
+            }\r
+        } finally {\r
+            mErrorLabel.setText(error == null ? "" : error);        //$NON-NLS-1$\r
         }\r
     }\r
 \r
@@ -488,25 +569,41 @@ final class UpdateChooserDialog extends Dialog {
         mPackageText.setStyleRange(sr);\r
     }\r
 \r
-    private void updateLicenceRadios(Archive a) {\r
+    private void updateLicenceRadios(ArchiveInfo ai) {\r
         if (mInternalLicenseRadioUpdate) {\r
             return;\r
         }\r
         mInternalLicenseRadioUpdate = true;\r
 \r
+        boolean oneAccepted = false;\r
+\r
         if (mLicenseAcceptAll) {\r
             mLicenseRadioAcceptAll.setSelection(true);\r
+            mLicenseRadioAccept.setEnabled(true);\r
+            mLicenseRadioReject.setEnabled(true);\r
             mLicenseRadioAccept.setSelection(false);\r
             mLicenseRadioReject.setSelection(false);\r
         } else {\r
             mLicenseRadioAcceptAll.setSelection(false);\r
-            mLicenseRadioAccept.setSelection(mAccepted.contains(a));\r
-            mLicenseRadioReject.setSelection(mRejected.contains(a));\r
+            oneAccepted = ai != null && ai.isAccepted();\r
+            mLicenseRadioAccept.setEnabled(ai != null);\r
+            mLicenseRadioReject.setEnabled(ai != null);\r
+            mLicenseRadioAccept.setSelection(oneAccepted);\r
+            mLicenseRadioReject.setSelection(ai != null && ai.isRejected());\r
         }\r
 \r
-        // The install button is enabled if there's at least one\r
-        // package accepted.\r
-        mInstallButton.setEnabled(mAccepted.size() > 0);\r
+        // The install button is enabled if there's at least one package accepted.\r
+        // If the current one isn't, look for another one.\r
+        boolean missing = mErrorLabel.getText() != null && mErrorLabel.getText().length() > 0;\r
+        if (!missing && !oneAccepted) {\r
+            for(ArchiveInfo ai2 : mArchives) {\r
+                if (ai2.isAccepted()) {\r
+                    oneAccepted = true;\r
+                    break;\r
+                }\r
+            }\r
+        }\r
+        mInstallButton.setEnabled(!missing && oneAccepted);\r
 \r
         mInternalLicenseRadioUpdate = false;\r
     }\r
@@ -523,26 +620,28 @@ final class UpdateChooserDialog extends Dialog {
         }\r
         mInternalLicenseRadioUpdate = true;\r
 \r
-        Archive a = getSelectedArchive();\r
+        ArchiveInfo ai = getSelectedArchive();\r
         boolean needUpdate = true;\r
 \r
         if (!mLicenseAcceptAll && mLicenseRadioAcceptAll.getSelection()) {\r
             // Accept all has been switched on. Mark all packages as accepted\r
             mLicenseAcceptAll = true;\r
-            mAccepted.addAll(mNewToOldArchiveMap.keySet());\r
-            mRejected.clear();\r
+            for(ArchiveInfo ai2 : mArchives) {\r
+                ai2.setAccepted(true);\r
+                ai2.setRejected(false);\r
+            }\r
 \r
         } else if (mLicenseRadioAccept.getSelection()) {\r
             // Accept only this one\r
             mLicenseAcceptAll = false;\r
-            mAccepted.add(a);\r
-            mRejected.remove(a);\r
+            ai.setAccepted(true);\r
+            ai.setRejected(false);\r
 \r
         } else if (mLicenseRadioReject.getSelection()) {\r
             // Reject only this one\r
             mLicenseAcceptAll = false;\r
-            mAccepted.remove(a);\r
-            mRejected.add(a);\r
+            ai.setAccepted(false);\r
+            ai.setRejected(true);\r
 \r
         } else {\r
             needUpdate = false;\r
@@ -554,9 +653,10 @@ final class UpdateChooserDialog extends Dialog {
             if (mLicenseAcceptAll) {\r
                 mTableViewPackage.refresh();\r
             } else {\r
-               mTableViewPackage.refresh(a);\r
+               mTableViewPackage.refresh(ai);\r
             }\r
-            updateLicenceRadios(a);\r
+            displayMissingDependency(ai);\r
+            updateLicenceRadios(ai);\r
         }\r
     }\r
 \r
@@ -564,32 +664,29 @@ final class UpdateChooserDialog extends Dialog {
      * Callback invoked when a package item is double-clicked in the list.\r
      */\r
     private void onPackageDoubleClick() {\r
-        Archive a = getSelectedArchive();\r
+        ArchiveInfo ai = getSelectedArchive();\r
 \r
-        if (mAccepted.contains(a)) {\r
-            // toggle from accepted to rejected\r
-            mAccepted.remove(a);\r
-            mRejected.add(a);\r
-        } else {\r
-            // toggle from rejected or unknown to accepted\r
-            mAccepted.add(a);\r
-            mRejected.remove(a);\r
-        }\r
+        boolean wasAccepted = ai.isAccepted();\r
+        ai.setAccepted(!wasAccepted);\r
+        ai.setRejected(wasAccepted);\r
 \r
         // update state\r
         mLicenseAcceptAll = false;\r
-        mTableViewPackage.refresh(a);\r
-        updateLicenceRadios(a);\r
+        mTableViewPackage.refresh(ai);\r
+        updateLicenceRadios(ai);\r
     }\r
 \r
     private class NewArchivesLabelProvider extends LabelProvider {\r
         @Override\r
         public Image getImage(Object element) {\r
+            assert element instanceof ArchiveInfo;\r
+            ArchiveInfo ai = (ArchiveInfo) element;\r
+\r
             ImageFactory imgFactory = mUpdaterData.getImageFactory();\r
             if (imgFactory != null) {\r
-                if (mAccepted.contains(element)) {\r
+                if (ai.isAccepted()) {\r
                     return imgFactory.getImageByName("accept_icon16.png");\r
-                } else if (mRejected.contains(element)) {\r
+                } else if (ai.isRejected()) {\r
                     return imgFactory.getImageByName("reject_icon16.png");\r
                 }\r
                 return imgFactory.getImageByName("unknown_icon16.png");\r
@@ -599,10 +696,16 @@ final class UpdateChooserDialog extends Dialog {
 \r
         @Override\r
         public String getText(Object element) {\r
-            if (element instanceof Archive) {\r
-                return ((Archive) element).getParentPackage().getShortDescription();\r
+            assert element instanceof ArchiveInfo;\r
+            ArchiveInfo ai = (ArchiveInfo) element;\r
+\r
+            String desc = ai.getNewArchive().getParentPackage().getShortDescription();\r
+\r
+            if (ai.isDependencyFor()) {\r
+                desc += " [*]";\r
             }\r
-            return super.getText(element);\r
+\r
+            return desc;\r
         }\r
     }\r
 \r
@@ -613,11 +716,11 @@ final class UpdateChooserDialog extends Dialog {
         }\r
 \r
         public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {\r
-            // Ignore. The input is always mNewArchives\r
+            // Ignore. The input is always mArchives\r
         }\r
 \r
         public Object[] getElements(Object inputElement) {\r
-            return mNewToOldArchiveMap.keySet().toArray();\r
+            return mArchives.toArray();\r
         }\r
     }\r
 \r
index bec00f8..1841421 100755 (executable)
@@ -30,7 +30,6 @@ import com.android.sdklib.internal.repository.Package;
 import com.android.sdklib.internal.repository.RepoSource;\r
 import com.android.sdklib.internal.repository.RepoSources;\r
 import com.android.sdklib.internal.repository.ToolPackage;\r
-import com.android.sdklib.internal.repository.Package.UpdateInfo;\r
 import com.android.sdkuilib.internal.repository.icons.ImageFactory;\r
 import com.android.sdkuilib.repository.UpdaterWindow.ISdkListener;\r
 \r
@@ -42,8 +41,7 @@ import java.io.ByteArrayOutputStream;
 import java.io.PrintStream;\r
 import java.util.ArrayList;\r
 import java.util.Collection;\r
-import java.util.HashMap;\r
-import java.util.Map;\r
+import java.util.HashSet;\r
 \r
 /**\r
  * Data shared between {@link UpdaterWindowImpl} and its pages.\r
@@ -268,9 +266,9 @@ class UpdaterData {
      * Install the list of given {@link Archive}s. This is invoked by the user selecting some\r
      * packages in the remote page and then clicking "install selected".\r
      *\r
-     * @param archives The archives to install. Incompatible ones will be skipped.\r
+     * @param result The archives to install. Incompatible ones will be skipped.\r
      */\r
-    public void installArchives(final Collection<Archive> archives) {\r
+    public void installArchives(final ArrayList<ArchiveInfo> result) {\r
         if (mTaskFactory == null) {\r
             throw new IllegalArgumentException("Task Factory is null");\r
         }\r
@@ -281,14 +279,23 @@ class UpdaterData {
             public void run(ITaskMonitor monitor) {\r
 \r
                 final int progressPerArchive = 2 * Archive.NUM_MONITOR_INC;\r
-                monitor.setProgressMax(archives.size() * progressPerArchive);\r
+                monitor.setProgressMax(result.size() * progressPerArchive);\r
                 monitor.setDescription("Preparing to install archives");\r
 \r
                 boolean installedAddon = false;\r
                 boolean installedTools = false;\r
 \r
+                // Mark all current local archives as already installed.\r
+                HashSet<Archive> installedArchives = new HashSet<Archive>();\r
+                for (Package p : getInstalledPackage()) {\r
+                    for (Archive a : p.getArchives()) {\r
+                        installedArchives.add(a);\r
+                    }\r
+                }\r
+\r
                 int numInstalled = 0;\r
-                for (Archive archive : archives) {\r
+                for (ArchiveInfo ai : result) {\r
+                    Archive archive = ai.getNewArchive();\r
 \r
                     int nextProgress = monitor.getProgress() + progressPerArchive;\r
                     try {\r
@@ -296,9 +303,24 @@ class UpdaterData {
                             break;\r
                         }\r
 \r
+                        ArchiveInfo adep = ai.getDependsOn();\r
+                        if (adep != null && !installedArchives.contains(adep)) {\r
+                            // This archive depends on another one that was not installed.\r
+                            // Skip it.\r
+                            monitor.setResult("Skipping '%1$s'; it depends on '%2$s' which was not installed.",\r
+                                    archive.getParentPackage().getShortDescription(),\r
+                                    adep.getNewArchive().getParentPackage().getShortDescription());\r
+                        }\r
+\r
                         if (archive.install(mOsSdkRoot, forceHttp, mSdkManager, monitor)) {\r
+                            // We installed this archive.\r
+                            installedArchives.add(archive);\r
                             numInstalled++;\r
 \r
+                            // If this package was replacing an existing one, the old one\r
+                            // is no longer installed.\r
+                            installedArchives.remove(ai.getReplaced());\r
+\r
                             // Check if we successfully installed a tool or add-on package.\r
                             if (archive.getParentPackage() instanceof AddonPackage) {\r
                                 installedAddon = true;\r
@@ -435,28 +457,20 @@ class UpdaterData {
             refreshSources(true);\r
         }\r
 \r
-        final Map<Archive, Archive> updates = findUpdates(selectedArchives);\r
-\r
-        if (selectedArchives != null) {\r
-            // Not only we want to perform updates but we also want to install the\r
-            // selected archives. If they do not match an update, list them anyway\r
-            // except they map themselves to null (no "old" archive)\r
-            for (Archive a : selectedArchives) {\r
-                if (!updates.containsKey(a)) {\r
-                    updates.put(a, null);\r
-                }\r
-            }\r
-        }\r
+        UpdaterLogic ul = new UpdaterLogic();\r
+        ArrayList<ArchiveInfo> archives = ul.computeUpdates(\r
+                selectedArchives,\r
+                getSources(),\r
+                getLocalSdkParser().getPackages());\r
 \r
-        UpdateChooserDialog dialog = new UpdateChooserDialog(getWindowShell(), this, updates);\r
+        UpdateChooserDialog dialog = new UpdateChooserDialog(getWindowShell(), this, archives);\r
         dialog.open();\r
 \r
-        Collection<Archive> result = dialog.getResult();\r
+        ArrayList<ArchiveInfo> result = dialog.getResult();\r
         if (result != null && result.size() > 0) {\r
             installArchives(result);\r
         }\r
     }\r
-\r
     /**\r
      * Refresh all sources. This is invoked either internally (reusing an existing monitor)\r
      * or as a UI callback on the remote page "Refresh" button (in which case the monitor is\r
@@ -485,108 +499,4 @@ class UpdaterData {
             }\r
         });\r
     }\r
-\r
-    /**\r
-     * Check the local archives vs the remote available packages to find potential updates.\r
-     * Return a map [remote archive => local archive] of suitable update candidates.\r
-     * Returns null if there's an unexpected error. Otherwise returns a map that can be\r
-     * empty but not null.\r
-     *\r
-     * @param selectedArchives The list of remote archive to consider for the update.\r
-     *  This can be null, in which case a list of remote archive is fetched from all\r
-     *  available sources.\r
-     */\r
-    private Map<Archive, Archive> findUpdates(Collection<Archive> selectedArchives) {\r
-        // Map [remote archive => local archive] of suitable update candidates\r
-        Map<Archive, Archive> result = new HashMap<Archive, Archive>();\r
-\r
-        // First go thru all sources and make a list of all available remote archives\r
-        // sorted by package class.\r
-        HashMap<Class<? extends Package>, ArrayList<Archive>> availablePkgs =\r
-            new HashMap<Class<? extends Package>, ArrayList<Archive>>();\r
-\r
-        if (selectedArchives != null) {\r
-            // Only consider the archives given\r
-\r
-            for (Archive a : selectedArchives) {\r
-                // Only add compatible archives\r
-                if (a.isCompatible()) {\r
-                    Class<? extends Package> clazz = a.getParentPackage().getClass();\r
-\r
-                    ArrayList<Archive> list = availablePkgs.get(clazz);\r
-                    if (list == null) {\r
-                        availablePkgs.put(clazz, list = new ArrayList<Archive>());\r
-                    }\r
-\r
-                    list.add(a);\r
-                }\r
-            }\r
-\r
-        } else {\r
-            // Get all the available archives from all loaded sources\r
-            RepoSource[] remoteSources = getSources().getSources();\r
-\r
-            for (RepoSource remoteSrc : remoteSources) {\r
-                Package[] remotePkgs = remoteSrc.getPackages();\r
-                if (remotePkgs != null) {\r
-                    for (Package remotePkg : remotePkgs) {\r
-                        Class<? extends Package> clazz = remotePkg.getClass();\r
-\r
-                        ArrayList<Archive> list = availablePkgs.get(clazz);\r
-                        if (list == null) {\r
-                            availablePkgs.put(clazz, list = new ArrayList<Archive>());\r
-                        }\r
-\r
-                        for (Archive a : remotePkg.getArchives()) {\r
-                            // Only add compatible archives\r
-                            if (a.isCompatible()) {\r
-                                list.add(a);\r
-                            }\r
-                        }\r
-                    }\r
-                }\r
-            }\r
-        }\r
-\r
-        Package[] localPkgs = getLocalSdkParser().getPackages();\r
-        if (localPkgs == null) {\r
-            // This is unexpected. The local sdk parser should have been called first.\r
-            return null;\r
-        }\r
-\r
-        for (Package localPkg : localPkgs) {\r
-            // get the available archive list for this package type\r
-            ArrayList<Archive> list = availablePkgs.get(localPkg.getClass());\r
-\r
-            // if this list is empty, we'll never find anything that matches\r
-            if (list == null || list.size() == 0) {\r
-                continue;\r
-            }\r
-\r
-            // local packages should have one archive at most\r
-            Archive[] localArchives = localPkg.getArchives();\r
-            if (localArchives != null && localArchives.length > 0) {\r
-                Archive localArchive = localArchives[0];\r
-                // only consider archives compatible with the current platform\r
-                if (localArchive != null && localArchive.isCompatible()) {\r
-\r
-                    // We checked all this archive stuff because that's what eventually gets\r
-                    // installed, but the "update" mechanism really works on packages. So now\r
-                    // the real question: is there a remote package that can update this\r
-                    // local package?\r
-\r
-                    for (Archive availArchive : list) {\r
-                        UpdateInfo info = localPkg.canBeUpdatedBy(availArchive.getParentPackage());\r
-                        if (info == UpdateInfo.UPDATE) {\r
-                            // Found one!\r
-                            result.put(availArchive, localArchive);\r
-                            break;\r
-                        }\r
-                    }\r
-                }\r
-            }\r
-        }\r
-\r
-        return result;\r
-    }\r
 }\r
diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterLogic.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterLogic.java
new file mode 100755 (executable)
index 0000000..6d9935d
--- /dev/null
@@ -0,0 +1,323 @@
+/*\r
+ * Copyright (C) 2009 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.sdkuilib.internal.repository;\r
+\r
+import com.android.sdklib.AndroidVersion;\r
+import com.android.sdklib.internal.repository.AddonPackage;\r
+import com.android.sdklib.internal.repository.Archive;\r
+import com.android.sdklib.internal.repository.Package;\r
+import com.android.sdklib.internal.repository.PlatformPackage;\r
+import com.android.sdklib.internal.repository.RepoSource;\r
+import com.android.sdklib.internal.repository.RepoSources;\r
+import com.android.sdklib.internal.repository.ToolPackage;\r
+import com.android.sdklib.internal.repository.Package.UpdateInfo;\r
+\r
+import java.util.ArrayList;\r
+import java.util.Collection;\r
+\r
+class UpdaterLogic {\r
+\r
+    private RepoSources mSources;\r
+\r
+    public ArrayList<ArchiveInfo> computeUpdates(\r
+            Collection<Archive> selectedArchives,\r
+            RepoSources sources,\r
+            Package[] localPkgs) {\r
+\r
+        mSources = sources;\r
+        ArrayList<ArchiveInfo> archives = new ArrayList<ArchiveInfo>();\r
+        ArrayList<Package> remotePkgs = new ArrayList<Package>();\r
+\r
+        if (selectedArchives == null) {\r
+            selectedArchives = findUpdates(localPkgs, remotePkgs);\r
+        }\r
+\r
+        for (Archive a : selectedArchives) {\r
+            insertArchive(a, archives, selectedArchives, remotePkgs, localPkgs, false);\r
+        }\r
+\r
+        return archives;\r
+    }\r
+\r
+\r
+    /**\r
+     * Find suitable updates to all current local packages.\r
+     */\r
+    private Collection<Archive> findUpdates(Package[] localPkgs, ArrayList<Package> remotePkgs) {\r
+        ArrayList<Archive> updates = new ArrayList<Archive>();\r
+\r
+        fetchRemotePackages(remotePkgs);\r
+\r
+        for (Package localPkg : localPkgs) {\r
+            for (Package remotePkg : remotePkgs) {\r
+                if (localPkg.canBeUpdatedBy(remotePkg) == UpdateInfo.UPDATE) {\r
+                    // Found a suitable update. Only accept the remote package\r
+                    // if it provides at least one compatible archive.\r
+\r
+                    for (Archive a : remotePkg.getArchives()) {\r
+                        if (a.isCompatible()) {\r
+                            updates.add(a);\r
+                            break;\r
+                        }\r
+                    }\r
+                }\r
+            }\r
+        }\r
+\r
+        return updates;\r
+    }\r
+\r
+    private ArchiveInfo insertArchive(Archive archive,\r
+            ArrayList<ArchiveInfo> outArchives,\r
+            Collection<Archive> selectedArchives,\r
+            ArrayList<Package> remotePkgs,\r
+            Package[] localPkgs,\r
+            boolean automated) {\r
+        Package p = archive.getParentPackage();\r
+\r
+        // Is this an update?\r
+        Archive updatedArchive = null;\r
+        for (Package lp : localPkgs) {\r
+            assert lp.getArchives().length == 1;\r
+            if (lp.getArchives().length > 0 && lp.canBeUpdatedBy(p) == UpdateInfo.UPDATE) {\r
+                updatedArchive = lp.getArchives()[0];\r
+            }\r
+        }\r
+\r
+        // find dependencies\r
+        ArchiveInfo dep = findDependency(p, outArchives, selectedArchives, remotePkgs, localPkgs);\r
+\r
+        ArchiveInfo ai = new ArchiveInfo(\r
+                archive, //newArchive\r
+                updatedArchive, //replaced\r
+                dep //dependsOn\r
+                );\r
+\r
+        outArchives.add(ai);\r
+        if (dep != null) {\r
+            dep.addDependencyFor(ai);\r
+        }\r
+\r
+        return ai;\r
+    }\r
+\r
+    private ArchiveInfo findDependency(Package pkg,\r
+            ArrayList<ArchiveInfo> outArchives,\r
+            Collection<Archive> selectedArchives,\r
+            ArrayList<Package> remotePkgs,\r
+            Package[] localPkgs) {\r
+\r
+        // Current dependencies can be:\r
+        // - addon: *always* depends on platform of same API level\r
+        // - platform: *might* depends on tools of rev >= min-tools-rev\r
+\r
+        if (pkg instanceof AddonPackage) {\r
+            AddonPackage addon = (AddonPackage) pkg;\r
+\r
+            return findAddonDependency(\r
+                    addon, outArchives, selectedArchives, remotePkgs, localPkgs);\r
+\r
+        } else if (pkg instanceof PlatformPackage) {\r
+            PlatformPackage platform = (PlatformPackage) pkg;\r
+\r
+            return findPlatformDependency(\r
+                    platform, outArchives, selectedArchives, remotePkgs, localPkgs);\r
+        }\r
+\r
+        return null;\r
+    }\r
+\r
+    /**\r
+     * A platform can have a min-tools-rev, in which case it depends on having a tools package\r
+     * of the requested revision.\r
+     * Finds the tools dependency. If found, add it to the list of things to install.\r
+     * Returns the archive info dependency, if any.\r
+     */\r
+    protected ArchiveInfo findPlatformDependency(PlatformPackage platform,\r
+            ArrayList<ArchiveInfo> outArchives,\r
+            Collection<Archive> selectedArchives,\r
+            ArrayList<Package> remotePkgs,\r
+            Package[] localPkgs) {\r
+        // This is the requirement to match.\r
+        int rev = platform.getMinToolsRevision();\r
+\r
+        if (rev == PlatformPackage.MIN_TOOLS_REV_NOT_SPECIFIED) {\r
+            // Well actually there's no requirement.\r
+            return null;\r
+        }\r
+\r
+        // First look in local packages.\r
+        for (Package p : localPkgs) {\r
+            if (p instanceof ToolPackage) {\r
+                if (((ToolPackage) p).getRevision() >= rev) {\r
+                    // We found one already installed. We don't report this dependency\r
+                    // as the UI only cares about resolving "newly added dependencies".\r
+                    return null;\r
+                }\r
+            }\r
+        }\r
+\r
+        // Look in archives already scheduled for install\r
+        for (ArchiveInfo ai : outArchives) {\r
+            Package p = ai.getNewArchive().getParentPackage();\r
+            if (p instanceof PlatformPackage) {\r
+                if (((ToolPackage) p).getRevision() >= rev) {\r
+                    // The dependency is already scheduled for install, nothing else to do.\r
+                    return ai;\r
+                }\r
+            }\r
+        }\r
+\r
+        // Otherwise look in the selected archives.\r
+        for (Archive a : selectedArchives) {\r
+            Package p = a.getParentPackage();\r
+            if (p instanceof ToolPackage) {\r
+                if (((ToolPackage) p).getRevision() >= rev) {\r
+                    // It's not already in the list of things to install, so add it now\r
+                    return insertArchive(a, outArchives,\r
+                            selectedArchives, remotePkgs, localPkgs,\r
+                            true);\r
+                }\r
+            }\r
+        }\r
+\r
+        // Finally nothing matched, so let's look at all available remote packages\r
+        fetchRemotePackages(remotePkgs);\r
+        for (Package p : remotePkgs) {\r
+            if (p instanceof ToolPackage) {\r
+                if (((ToolPackage) p).getRevision() >= rev) {\r
+                    // It's not already in the list of things to install, so add the\r
+                    // first compatible archive we can find.\r
+                    for (Archive a : p.getArchives()) {\r
+                        if (a.isCompatible()) {\r
+                            return insertArchive(a, outArchives,\r
+                                    selectedArchives, remotePkgs, localPkgs,\r
+                                    true);\r
+                        }\r
+                    }\r
+                }\r
+            }\r
+        }\r
+\r
+        // We end up here if nothing matches. We don't have a good tools to match.\r
+        // Seriously, that can't happens unless we totally screwed our repo manifest.\r
+        // We'll let this one go through anyway.\r
+        return null;\r
+    }\r
+\r
+    /**\r
+     * An addon depends on having a platform with the same API version.\r
+     * Finds the platform dependency. If found, add it to the list of things to install.\r
+     * Returns the archive info dependency, if any.\r
+     */\r
+    protected ArchiveInfo findAddonDependency(AddonPackage addon,\r
+            ArrayList<ArchiveInfo> outArchives,\r
+            Collection<Archive> selectedArchives,\r
+            ArrayList<Package> remotePkgs,\r
+            Package[] localPkgs) {\r
+        // This is the requirement to match.\r
+        AndroidVersion v = addon.getVersion();\r
+\r
+        // Find a platform that would satisfy the requirement.\r
+\r
+        // First look in local packages.\r
+        for (Package p : localPkgs) {\r
+            if (p instanceof PlatformPackage) {\r
+                if (v.equals(((PlatformPackage) p).getVersion())) {\r
+                    // We found one already installed. We don't report this dependency\r
+                    // as the UI only cares about resolving "newly added dependencies".\r
+                    return null;\r
+                }\r
+            }\r
+        }\r
+\r
+        // Look in archives already scheduled for install\r
+        for (ArchiveInfo ai : outArchives) {\r
+            Package p = ai.getNewArchive().getParentPackage();\r
+            if (p instanceof PlatformPackage) {\r
+                if (v.equals(((PlatformPackage) p).getVersion())) {\r
+                    // The dependency is already scheduled for install, nothing else to do.\r
+                    return ai;\r
+                }\r
+            }\r
+        }\r
+\r
+        // Otherwise look in the selected archives.\r
+        for (Archive a : selectedArchives) {\r
+            Package p = a.getParentPackage();\r
+            if (p instanceof PlatformPackage) {\r
+                if (v.equals(((PlatformPackage) p).getVersion())) {\r
+                    // It's not already in the list of things to install, so add it now\r
+                    return insertArchive(a, outArchives,\r
+                            selectedArchives, remotePkgs, localPkgs,\r
+                            true);\r
+                }\r
+            }\r
+        }\r
+\r
+        // Finally nothing matched, so let's look at all available remote packages\r
+        fetchRemotePackages(remotePkgs);\r
+        for (Package p : remotePkgs) {\r
+            if (p instanceof PlatformPackage) {\r
+                if (v.equals(((PlatformPackage) p).getVersion())) {\r
+                    // It's not already in the list of things to install, so add the\r
+                    // first compatible archive we can find.\r
+                    for (Archive a : p.getArchives()) {\r
+                        if (a.isCompatible()) {\r
+                            return insertArchive(a, outArchives,\r
+                                    selectedArchives, remotePkgs, localPkgs,\r
+                                    true);\r
+                        }\r
+                    }\r
+                }\r
+            }\r
+        }\r
+\r
+        // We end up here if nothing matches. We don't have a good platform to match.\r
+        // Seriously, that can't happens unless the repository contains a bogus addon\r
+        // entry that does not match any existing platform API level.\r
+        // It's conceivable that a 3rd part addon repo might have error, in which case\r
+        // we'll let this one go through anyway.\r
+        return null;\r
+    }\r
+\r
+    /** Fetch all remote packages only if really needed. */\r
+    protected void fetchRemotePackages(ArrayList<Package> remotePkgs) {\r
+        if (remotePkgs.size() > 0) {\r
+            return;\r
+        }\r
+\r
+        // Get all the available packages from all loaded sources\r
+        RepoSource[] remoteSources = mSources.getSources();\r
+\r
+        for (RepoSource remoteSrc : remoteSources) {\r
+            Package[] pkgs = remoteSrc.getPackages();\r
+            if (pkgs != null) {\r
+                nextPackage: for (Package pkg : pkgs) {\r
+                    for (Archive a : pkg.getArchives()) {\r
+                        // Only add a package if it contains at least one compatible archive\r
+                        if (a.isCompatible()) {\r
+                            remotePkgs.add(pkg);\r
+                            continue nextPackage;\r
+                        }\r
+                    }\r
+                }\r
+            }\r
+        }\r
+    }\r
+\r
+}\r
diff --git a/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/UpdaterLogicTest.java b/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/UpdaterLogicTest.java
new file mode 100755 (executable)
index 0000000..bb1f52e
--- /dev/null
@@ -0,0 +1,101 @@
+/*\r
+ * Copyright (C) 2009 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.sdkuilib.internal.repository;\r
+\r
+import com.android.sdklib.internal.repository.Archive;\r
+import com.android.sdklib.internal.repository.MockAddonPackage;\r
+import com.android.sdklib.internal.repository.MockPlatformPackage;\r
+import com.android.sdklib.internal.repository.MockToolPackage;\r
+import com.android.sdklib.internal.repository.Package;\r
+\r
+import java.util.ArrayList;\r
+import java.util.Arrays;\r
+\r
+import junit.framework.TestCase;\r
+\r
+public class UpdaterLogicTest extends TestCase {\r
+\r
+    private static class MockUpdaterLogic extends UpdaterLogic {\r
+        private final Package[] mRemotePackages;\r
+\r
+        public MockUpdaterLogic(Package[] remotePackages) {\r
+            mRemotePackages = remotePackages;\r
+        }\r
+\r
+        @Override\r
+        protected void fetchRemotePackages(ArrayList<Package> remotePkgs) {\r
+            if (mRemotePackages != null) {\r
+                remotePkgs.addAll(Arrays.asList(mRemotePackages));\r
+            }\r
+        }\r
+    }\r
+\r
+    public void testFindAddonDependency() throws Exception {\r
+        MockUpdaterLogic mul = new MockUpdaterLogic(null);\r
+\r
+        MockPlatformPackage p1 = new MockPlatformPackage(1, 1);\r
+        MockPlatformPackage p2 = new MockPlatformPackage(2, 1);\r
+\r
+        MockAddonPackage a1 = new MockAddonPackage(p1, 1);\r
+        MockAddonPackage a2 = new MockAddonPackage(p2, 2);\r
+\r
+        ArrayList<ArchiveInfo> out = new ArrayList<ArchiveInfo>();\r
+        ArrayList<Archive> selected = new ArrayList<Archive>();\r
+        ArrayList<Package> remote = new ArrayList<Package>();\r
+\r
+        // a2 depends on p2, which is not in the locals\r
+        Package[] locals = { p1, a1 };\r
+        assertNull(mul.findAddonDependency(a2, out, selected, remote, locals));\r
+        assertEquals(0, out.size());\r
+\r
+        // p2 is now selected, and should be scheduled for install in out\r
+        Archive p2_archive = p2.getArchives()[0];\r
+        selected.add(p2_archive);\r
+        ArchiveInfo ai2 = mul.findAddonDependency(a2, out, selected, remote, locals);\r
+        assertNotNull(ai2);\r
+        assertSame(p2_archive, ai2.getNewArchive());\r
+        assertEquals(1, out.size());\r
+        assertSame(p2_archive, out.get(0).getNewArchive());\r
+    }\r
+\r
+    public void testFindPlatformDependency() throws Exception {\r
+        MockUpdaterLogic mul = new MockUpdaterLogic(null);\r
+\r
+        MockToolPackage t1 = new MockToolPackage(1);\r
+        MockToolPackage t2 = new MockToolPackage(2);\r
+\r
+        MockPlatformPackage p2 = new MockPlatformPackage(2, 1, 2);\r
+\r
+        ArrayList<ArchiveInfo> out = new ArrayList<ArchiveInfo>();\r
+        ArrayList<Archive> selected = new ArrayList<Archive>();\r
+        ArrayList<Package> remote = new ArrayList<Package>();\r
+\r
+        // p2 depends on t2, which is not locally installed\r
+        Package[] locals = { t1 };\r
+        assertNull(mul.findPlatformDependency(p2, out, selected, remote, locals));\r
+        assertEquals(0, out.size());\r
+\r
+        // t2 is now selected and can be used as a dependency\r
+        Archive t2_archive = t2.getArchives()[0];\r
+        selected.add(t2_archive);\r
+        ArchiveInfo ai2 = mul.findPlatformDependency(p2, out, selected, remote, locals);\r
+        assertNotNull(ai2);\r
+        assertSame(t2_archive, ai2.getNewArchive());\r
+        assertEquals(1, out.size());\r
+        assertSame(t2_archive, out.get(0).getNewArchive());\r
+    }\r
+}\r