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 ef62f6e8c..13c3ea19a 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 @@ 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; 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 b28019fab..2c9b3fbe8 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 @@ public class AddonPackage extends Package { * {@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. + *

+ * 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 75879b84d..e54b5506b 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 @@ public class DocPackage extends Package { * 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. + *

+ * 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 74e1c5933..587be1d09 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 @@ public class ExtraPackage extends Package { * 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. + *

+ * 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 69d526b1d..657bb143a 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 @@ public abstract class Package implements IDescription { * 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. - * + *

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

+ * 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 e95656a21..e3fb3f293 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 @@ import java.util.Properties; */ 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 @@ public class PlatformPackage extends Package { * 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. + *

+ * 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 ee133793e..974d8ac60 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 @@ public class ToolPackage extends Package { * 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. + *

+ * 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 000000000..4d4199f4e --- /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 000000000..b840b82bc --- /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 000000000..e1ce621e5 --- /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. + *

+ * 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 5a1790fc2..73592c49f 100644 --- a/tools/sdkmanager/libs/sdkuilib/.classpath +++ b/tools/sdkmanager/libs/sdkuilib/.classpath @@ -1,9 +1,11 @@ - - - - - - - - - + + + + + + + + + + + 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 000000000..f60316946 --- /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. + *

+ * 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 mDependencyFor = new ArrayList(); + 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 + * this 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 this + * 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 another one that we + * want to install. + */ + public boolean isDependencyFor() { + return mDependencyFor.size() > 0; + } + + /** + * Set to true if this new archive is a dependency for another one that we + * want to install. + */ + public void addDependencyFor(ArchiveInfo dependencyFor) { + mDependencyFor.add(dependencyFor); + } + + public Collection 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 d7d3a9024..5b8da837e 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.Table; 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 @@ import java.util.TreeMap; */ 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 mNewToOldArchiveMap; private boolean mLicenseAcceptAll; private boolean mInternalLicenseRadioUpdate; - private HashSet mAccepted = new HashSet(); - private HashSet mRejected = new HashSet(); - private ArrayList mResult = new ArrayList(); // UI fields private Shell mDialogShell; @@ -96,40 +86,56 @@ final class UpdateChooserDialog extends Dialog { 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 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 newToOldUpdates) { + ArrayList archives) { super(parentShell, SWT.APPLICATION_MODAL); mUpdaterData = updaterData; - - mNewToOldArchiveMap = new TreeMap(new Comparator() { - 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 getResult() { - return mResult; + public ArrayList getResult() { + ArrayList ais = new ArrayList(); + + if (!mCancelled) { + for (ArchiveInfo ai : mArchives) { + if (ai.isAccepted()) { + ais.add(ai); + } + } + } + + return ais; } /** @@ -249,7 +255,14 @@ final class UpdateChooserDialog extends Dialog { 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 @@ final class UpdateChooserDialog extends Dialog { 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 @@ final class UpdateChooserDialog extends Dialog { } /** - * 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 @@ final class UpdateChooserDialog extends Dialog { * Callback invoked when the Cancel button is selected. */ private void onCancelSelected() { + mCancelled = true; mCompleted = true; } @@ -423,49 +430,123 @@ final class UpdateChooserDialog extends Dialog { * 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 @@ final class UpdateChooserDialog extends Dialog { 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 @@ final class UpdateChooserDialog extends Dialog { } 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 @@ final class UpdateChooserDialog extends Dialog { if (mLicenseAcceptAll) { mTableViewPackage.refresh(); } else { - mTableViewPackage.refresh(a); + mTableViewPackage.refresh(ai); } - updateLicenceRadios(a); + displayMissingDependency(ai); + updateLicenceRadios(ai); } } @@ -564,32 +664,29 @@ final class UpdateChooserDialog extends Dialog { * 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 @@ final class UpdateChooserDialog extends Dialog { @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 @@ final class UpdateChooserDialog extends Dialog { } 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 bec00f8c4..184142160 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.Package; 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.ByteArrayOutputStream; 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 @@ class UpdaterData { * 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 archives) { + public void installArchives(final ArrayList result) { if (mTaskFactory == null) { throw new IllegalArgumentException("Task Factory is null"); } @@ -281,14 +279,23 @@ class UpdaterData { 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 installedArchives = new HashSet(); + 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 @@ class UpdaterData { 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 @@ class UpdaterData { refreshSources(true); } - final Map updates = findUpdates(selectedArchives); + UpdaterLogic ul = new UpdaterLogic(); + ArrayList 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 result = dialog.getResult(); + ArrayList 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 @@ class UpdaterData { } }); } - - /** - * 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 findUpdates(Collection selectedArchives) { - // Map [remote archive => local archive] of suitable update candidates - Map result = new HashMap(); - - // First go thru all sources and make a list of all available remote archives - // sorted by package class. - HashMap, ArrayList> availablePkgs = - new HashMap, ArrayList>(); - - if (selectedArchives != null) { - // Only consider the archives given - - for (Archive a : selectedArchives) { - // Only add compatible archives - if (a.isCompatible()) { - Class clazz = a.getParentPackage().getClass(); - - ArrayList list = availablePkgs.get(clazz); - if (list == null) { - availablePkgs.put(clazz, list = new ArrayList()); - } - - 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 clazz = remotePkg.getClass(); - - ArrayList list = availablePkgs.get(clazz); - if (list == null) { - availablePkgs.put(clazz, list = new ArrayList()); - } - - 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 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 000000000..6d9935d2a --- /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 computeUpdates( + Collection selectedArchives, + RepoSources sources, + Package[] localPkgs) { + + mSources = sources; + ArrayList archives = new ArrayList(); + ArrayList remotePkgs = new ArrayList(); + + 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 findUpdates(Package[] localPkgs, ArrayList remotePkgs) { + ArrayList updates = new ArrayList(); + + 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 outArchives, + Collection selectedArchives, + ArrayList 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 outArchives, + Collection selectedArchives, + ArrayList 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 outArchives, + Collection selectedArchives, + ArrayList 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 outArchives, + Collection selectedArchives, + ArrayList 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 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 000000000..bb1f52e99 --- /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 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 out = new ArrayList(); + ArrayList selected = new ArrayList(); + ArrayList remote = new ArrayList(); + + // 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 out = new ArrayList(); + ArrayList selected = new ArrayList(); + ArrayList remote = new ArrayList(); + + // 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()); + } +}