diff options
author | Raphael Moll <ralf@android.com> | 2011-07-13 16:48:23 -0700 |
---|---|---|
committer | Android Code Review <code-review@android.com> | 2011-07-13 16:48:23 -0700 |
commit | 721d33f854f3e866b05aa76b997a71ca0bcc41e2 (patch) | |
tree | c640e98b00000df8011c7d7f4165f835456ea6ac | |
parent | cbe918621e701dec553f0a434f6f75886f154f6a (diff) | |
parent | 122ed4977a42719239e501fc522592994267bd9d (diff) | |
download | sdk-721d33f854f3e866b05aa76b997a71ca0bcc41e2.zip sdk-721d33f854f3e866b05aa76b997a71ca0bcc41e2.tar.gz sdk-721d33f854f3e866b05aa76b997a71ca0bcc41e2.tar.bz2 |
Merge "SdkManager: fix issue with double-packages on reload."
10 files changed, 1201 insertions, 623 deletions
diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ExtraPackage.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ExtraPackage.java index 793c98d..ab074ce 100755 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ExtraPackage.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ExtraPackage.java @@ -16,6 +16,8 @@ package com.android.sdklib.internal.repository;
+import com.android.annotations.VisibleForTesting;
+import com.android.annotations.VisibleForTesting.Visibility;
import com.android.sdklib.NullSdkLog;
import com.android.sdklib.SdkConstants;
import com.android.sdklib.SdkManager;
@@ -159,7 +161,8 @@ public class ExtraPackage extends MinToolsPackage }
}
- private ExtraPackage(SdkSource source,
+ @VisibleForTesting(visibility=Visibility.PRIVATE)
+ protected ExtraPackage(SdkSource source,
Properties props,
String vendor,
String path,
diff --git a/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/MockAddonPackage.java b/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/MockAddonPackage.java index 1279529..76f645a 100755 --- a/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/MockAddonPackage.java +++ b/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/MockAddonPackage.java @@ -31,12 +31,22 @@ public class MockAddonPackage extends AddonPackage { /**
* Creates a {@link MockAddonTarget} with the requested base platform and addon revision
- * and then a {@link MockAddonPackage} wrapping it.
+ * and then a {@link MockAddonPackage} wrapping it and a default name of "addon".
*
* By design, this package contains one and only one archive.
*/
public MockAddonPackage(MockPlatformPackage basePlatform, int revision) {
- super(new MockAddonTarget(basePlatform.getTarget(), revision), null /*props*/);
+ this("addon", basePlatform, revision); //$NON-NLS-1$
+ }
+
+ /**
+ * 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(String name, MockPlatformPackage basePlatform, int revision) {
+ super(new MockAddonTarget(name, basePlatform.getTarget(), revision), null /*props*/);
}
/**
@@ -47,8 +57,10 @@ public class MockAddonPackage extends AddonPackage { private final IAndroidTarget mParentTarget;
private final int mRevision;
+ private final String mName;
- public MockAddonTarget(IAndroidTarget parentTarget, int revision) {
+ public MockAddonTarget(String name, IAndroidTarget parentTarget, int revision) {
+ mName = name;
mParentTarget = parentTarget;
mRevision = revision;
}
@@ -85,10 +97,6 @@ public class MockAddonPackage extends AddonPackage { return "";
}
- public String getName() {
- return "mock addon target";
- }
-
public IOptionalLibrary[] getOptionalLibraries() {
return null;
}
@@ -133,14 +141,18 @@ public class MockAddonPackage extends AddonPackage { return 0;
}
- public String getVendor() {
- return null;
- }
-
public AndroidVersion getVersion() {
return mParentTarget.getVersion();
}
+ public String getName() {
+ return mName;
+ }
+
+ public String getVendor() {
+ return mParentTarget.getVendor();
+ }
+
public String getVersionName() {
return String.format("mock-addon-%1$d", getVersion().getApiLevel());
}
diff --git a/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/MockExtraPackage.java b/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/MockExtraPackage.java new file mode 100755 index 0000000..9319c6c --- /dev/null +++ b/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/MockExtraPackage.java @@ -0,0 +1,59 @@ +/*
+ * Copyright (C) 2011 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;
+
+import java.util.Properties;
+
+/**
+ * A mock {@link ToolPackage} for testing.
+ *
+ * By design, this package contains one and only one archive.
+ */
+public class MockExtraPackage extends ExtraPackage {
+
+ /**
+ * Creates a {@link MockExtraPackage} with the given revision and hardcoded defaults
+ * for everything else.
+ * <p/>
+ * By design, this creates a package with one and only one archive.
+ */
+ public MockExtraPackage(String vendor, String path, int revision, int min_platform_tools_rev) {
+ super(
+ null, // source,
+ createProps(min_platform_tools_rev), // props,
+ vendor,
+ path,
+ revision,
+ null, // license,
+ "desc", // description,
+ "url", // descUrl,
+ Os.getCurrentOs(), // archiveOs,
+ Arch.getCurrentArch(), // archiveArch,
+ "foo" // archiveOsPath
+ );
+ }
+
+ private static Properties createProps(int min_platform_tools_rev) {
+ Properties props = new Properties();
+ props.setProperty(ToolPackage.PROP_MIN_PLATFORM_TOOLS_REV,
+ Integer.toString((min_platform_tools_rev)));
+ return props;
+ }
+}
diff --git a/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/MockPlatformPackage.java b/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/MockPlatformPackage.java index d3c1235..fc24658 100755 --- a/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/MockPlatformPackage.java +++ b/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/MockPlatformPackage.java @@ -117,10 +117,6 @@ public class MockPlatformPackage extends PlatformPackage { return "";
}
- public String getName() {
- return "mock platform target";
- }
-
public IOptionalLibrary[] getOptionalLibraries() {
return null;
}
@@ -165,8 +161,20 @@ public class MockPlatformPackage extends PlatformPackage { return 0;
}
+ /**
+ * Returns a vendor that depends on the parent *platform* API.
+ * This works well in Unit Tests where we'll typically have different
+ * platforms as unique identifiers.
+ */
public String getVendor() {
- return null;
+ return "vendor " + Integer.toString(mApiLevel);
+ }
+
+ /**
+ * Create a synthetic name using the target API level.
+ */
+ public String getName() {
+ return "platform r" + Integer.toString(mApiLevel);
}
public AndroidVersion getVersion() {
diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/PackageLoader.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/PackageLoader.java index 6a6b423..6aaabe9 100755 --- a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/PackageLoader.java +++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/PackageLoader.java @@ -30,7 +30,6 @@ import org.eclipse.swt.widgets.Shell; import java.io.File; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; /** @@ -142,16 +141,15 @@ class PackageLoader { } // get local packages and offer them to the callback - final List<PkgItem> allPkgItems = loadLocalPackages(); - if (!allPkgItems.isEmpty()) { - // Notify the callback by giving it a copy of the current list. - // (in case the callback holds to the list... we still need this list of - // ourselves below). - if (!sourceLoadedCallback.onSourceLoaded(new ArrayList<PkgItem>(allPkgItems))) { + List<PkgItem> localPkgItems = loadLocalPackages(); + if (!localPkgItems.isEmpty()) { + if (!sourceLoadedCallback.onSourceLoaded(localPkgItems)) { return; } } + final int[] numPackages = { localPkgItems.size() }; + // get remote packages final boolean forceHttp = mUpdaterData.getSettingsController().getForceHttp(); mUpdaterData.loadRemoteAddonsList(); @@ -170,26 +168,13 @@ class PackageLoader { } List<PkgItem> sourcePkgItems = new ArrayList<PkgItem>(); - - nextPkg: for(Package pkg : pkgs) { - boolean isUpdate = false; - for (PkgItem pi: allPkgItems) { - if (pi.isSamePackageAs(pkg)) { - continue nextPkg; - } - if (pi.isUpdatedBy(pkg)) { - isUpdate = true; - break; - } - } - - if (!isUpdate) { - PkgItem pi = new PkgItem(pkg, PkgState.NEW); - sourcePkgItems.add(pi); - allPkgItems.add(pi); - } + for(Package pkg : pkgs) { + PkgItem pi = new PkgItem(pkg, PkgState.NEW); + sourcePkgItems.add(pi); } + numPackages[0] += sourcePkgItems.size(); + // Notify the callback a new source has finished loading. // If the callback requests so, stop right away. if (!sourceLoadedCallback.onSourceLoaded(sourcePkgItems)) { @@ -200,7 +185,7 @@ class PackageLoader { monitor.logError("Loading source failed: %1$s", e.toString()); } finally { monitor.setDescription("Done loading %1$d packages from %2$d sources", - allPkgItems.size(), + numPackages[0], sources.length); } } @@ -265,23 +250,18 @@ class PackageLoader { Package acceptedPkg = null; switch(item.getState()) { case NEW: - if (installTask.acceptPackage(item.getPackage())) { - acceptedPkg = item.getPackage(); + if (installTask.acceptPackage(item.getMainPackage())) { + acceptedPkg = item.getMainPackage(); } - break; - case HAS_UPDATE: - for (Package upd : item.getUpdatePkgs()) { - if (installTask.acceptPackage(upd)) { - acceptedPkg = upd; - break; - } + if (item.hasUpdatePkg() && installTask.acceptPackage(item.getUpdatePkg())) { + acceptedPkg = item.getUpdatePkg(); } break; case INSTALLED: - if (installTask.acceptPackage(item.getPackage())) { + if (installTask.acceptPackage(item.getMainPackage())) { // If the caller is accepting an installed package, // return a success and give the package's install path - acceptedPkg = item.getPackage(); + acceptedPkg = item.getMainPackage(); Archive[] a = acceptedPkg.getArchives(); // an installed package should have one local compatible archive if (a.length == 1 && a[0].isCompatible()) { @@ -366,7 +346,7 @@ class PackageLoader { // Try to locate the installed package in the new package list for (PkgItem localItem : localPkgItems) { - Package localPkg = localItem.getPackage(); + Package localPkg = localItem.getMainPackage(); if (localPkg.canBeUpdatedBy(packageToInstall) == UpdateInfo.NOT_UPDATE) { Archive[] localArchive = localPkg.getArchives(); if (localArchive.length == 1 && localArchive[0].isCompatible()) { @@ -397,117 +377,65 @@ class PackageLoader { */ public enum PkgState { /** - * Package is locally installed and has no update available on remote sites. + * Package is locally installed and may or may not have an update. */ INSTALLED, /** - * Package is installed and has an update available. - * In this case, {@link PkgItem#getUpdatePkgs()} provides the list of 1 or more - * packages that can update this {@link PkgItem}. - * <p/> - * Although not structurally enforced, it can be reasonably expected that - * the original package and the updating packages all come from the same source. - */ - HAS_UPDATE, - - /** - * There's a new package available on the remote site that isn't - * installed locally. + * There's a new package available on the remote site that isn't installed locally. */ NEW } /** - * A {@link PkgItem} represents one {@link Package} combined with its state. + * A {@link PkgItem} represents one main {@link Package} combined with its state + * and an optional update package. * <p/> - * It can be either a locally installed package, or a remotely available package. - * If the later, it can be either a new package or an update for a locally installed one. - * <p/> - * In the case of an update, the {@link PkgItem#getPackage()} represents the currently - * installed package and there's a separate list of {@link PkgItem#getUpdatePkgs()} that - * links to the updating packages. Note that in a typical repository there should only - * one update for a given installed package however the system is designed to be more - * generic and allow many. + * The main package is final and cannot change since it's what "defines" this PkgItem. + * The state or update package can change later. */ public static class PkgItem implements Comparable<PkgItem> { - private final Package mPkg; private PkgState mState; - private List<Package> mUpdatePkgs; + private final Package mMainPkg; + private Package mUpdatePkg; - public PkgItem(Package pkg, PkgState state) { - mPkg = pkg; + /** + * Create a new {@link PkgItem} for this main package. + * The main package is final and cannot change since it's what "defines" this PkgItem. + * The state or update package can change later. + */ + public PkgItem(Package mainPkg, PkgState state) { + mMainPkg = mainPkg; mState = state; - assert mPkg != null; + assert mMainPkg != null; } public boolean isObsolete() { - return mPkg.isObsolete(); - } - - public boolean isSameItemAs(PkgItem item) { - boolean same = this.mState == item.mState; - if (same) { - same = isSamePackageAs(item.getPackage()); - } - // check updating packages are the same - if (same) { - List<Package> u1 = getUpdatePkgs(); - List<Package> u2 = item.getUpdatePkgs(); - same = (u1 == null && u2 == null) || - (u1 != null && u2 != null); - if (same && u1 != null && u2 != null) { - int n = u1.size(); - same = n == u2.size(); - if (same) { - for (int i = 0; same && i < n; i++) { - Package p1 = u1.get(i); - Package p2 = u2.get(i); - same = p1.canBeUpdatedBy(p2) == UpdateInfo.NOT_UPDATE; - } - } - } - } - - return same; + return mMainPkg.isObsolete(); } - public boolean isSamePackageAs(Package pkg) { - return mPkg.canBeUpdatedBy(pkg) == UpdateInfo.NOT_UPDATE; + public Package getUpdatePkg() { + return mUpdatePkg; } - /** - * Check whether the 'pkg' argument updates this package. - * If it does, record it as a sub-package. - * Returns true if it was recorded as an update, false otherwise. - */ - public boolean isUpdatedBy(Package pkg) { - if (mPkg.canBeUpdatedBy(pkg) == UpdateInfo.UPDATE) { - if (mUpdatePkgs == null) { - mUpdatePkgs = new ArrayList<Package>(); - } - mUpdatePkgs.add(pkg); - mState = PkgState.HAS_UPDATE; - return true; - } - - return false; + public boolean hasUpdatePkg() { + return mState == PkgState.INSTALLED && mUpdatePkg != null; } public String getName() { - return mPkg.getListDescription(); + return mMainPkg.getListDescription(); } public int getRevision() { - return mPkg.getRevision(); + return mMainPkg.getRevision(); } public String getDescription() { - return mPkg.getDescription(); + return mMainPkg.getDescription(); } - public Package getPackage() { - return mPkg; + public Package getMainPackage() { + return mMainPkg; } public PkgState getState() { @@ -516,65 +444,146 @@ class PackageLoader { public SdkSource getSource() { if (mState == PkgState.NEW) { - return mPkg.getParentSource(); + return mMainPkg.getParentSource(); } else { return null; } } public int getApi() { - return mPkg instanceof IPackageVersion ? - ((IPackageVersion) mPkg).getVersion().getApiLevel() : + return mMainPkg instanceof IPackageVersion ? + ((IPackageVersion) mMainPkg).getVersion().getApiLevel() : -1; } - public List<Package> getUpdatePkgs() { - return mUpdatePkgs; - } - public Archive[] getArchives() { - return mPkg.getArchives(); + return mMainPkg.getArchives(); } public int compareTo(PkgItem pkg) { - return getPackage().compareTo(pkg.getPackage()); + return getMainPackage().compareTo(pkg.getMainPackage()); } /** - * Returns true if this package or any of the updating packages contains + * Returns true if this package or its updating packages contains * the exact given archive. * Important: This compares object references, not object equality. */ public boolean hasArchive(Archive archive) { - if (mPkg.hasArchive(archive)) { + if (mMainPkg.hasArchive(archive)) { return true; } - if (mUpdatePkgs != null && !mUpdatePkgs.isEmpty()) { - for (Package p : mUpdatePkgs) { - if (p.hasArchive(archive)) { - return true; - } + if (mUpdatePkg != null && mUpdatePkg.hasArchive(archive)) { + return true; + } + return false; + } + + /** + * Checks whether the main packages are of the same type and are + * not an update of each other. + */ + public boolean isSameMainPackageAs(Package pkg) { + return mMainPkg.canBeUpdatedBy(pkg) == UpdateInfo.NOT_UPDATE; + } + + /** + * Checks whether too {@link PkgItem} are the same. + * This checks both items have the same state, both main package are similar + * and that they have the same updating packages. + */ + public boolean isSameItemAs(PkgItem item) { + if (this == item) { + return true; + } + boolean same = this.mState == item.mState; + if (same) { + same = isSameMainPackageAs(item.getMainPackage()); + } + + if (same) { + // check updating packages are the same + Package p1 = this.mUpdatePkg; + Package p2 = item.getUpdatePkg(); + same = (p1 == p2) || (p1 == null && p2 == null) || (p1 != null && p2 != null); + + if (same && p1 != null) { + same = p1.canBeUpdatedBy(p2) == UpdateInfo.NOT_UPDATE; + } + } + + return same; + } + + /** + * Equality is defined as {@link #isSameItemAs(PkgItem)}: state, main package + * and update package must be the similar. + */ + @Override + public boolean equals(Object obj) { + return (obj instanceof PkgItem) && this.isSameItemAs((PkgItem) obj); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mState == null) ? 0 : mState.hashCode()); + result = prime * result + ((mMainPkg == null) ? 0 : mMainPkg.hashCode()); + result = prime * result + ((mUpdatePkg == null) ? 0 : mUpdatePkg.hashCode()); + return result; + } + + /** + * Check whether the 'pkg' argument is an update for this package. + * If it is, record it as an updating package. + * If there's already an updating package, only keep the most recent update. + * Returns true if it is update (even if there was already an update and this + * ended up not being the most recent), false if incompatible or not an update. + * + * This should only be used for installed packages. + */ + public boolean mergeUpdate(Package pkg) { + if (mUpdatePkg == pkg) { + return true; + } + if (mMainPkg.canBeUpdatedBy(pkg) == UpdateInfo.UPDATE) { + if (mUpdatePkg == null) { + mUpdatePkg = pkg; + } else if (mUpdatePkg.canBeUpdatedBy(pkg) == UpdateInfo.UPDATE) { + // If we have more than one, keep only the most recent update + mUpdatePkg = pkg; } + return true; } + return false; } + public void removeUpdate() { + mUpdatePkg = null; + } + /** Returns a string representation of this item, useful when debugging. */ @Override public String toString() { - StringBuilder sb = new StringBuilder(mState.toString()); + StringBuilder sb = new StringBuilder(); + sb.append('<'); + sb.append(mState.toString()); - if (mPkg != null) { + if (mMainPkg != null) { sb.append(", pkg:"); //$NON-NLS-1$ - sb.append(mPkg.toString()); + sb.append(mMainPkg.toString()); } - if (mUpdatePkgs != null && !mUpdatePkgs.isEmpty()) { + if (mUpdatePkg != null) { sb.append(", updated by:"); //$NON-NLS-1$ - sb.append(Arrays.toString(mUpdatePkgs.toArray())); + sb.append(mUpdatePkg.toString()); } + sb.append('>'); return sb.toString(); } + } } diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/PackagesPage.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/PackagesPage.java index e858831..f690a8e 100755 --- a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/PackagesPage.java +++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/PackagesPage.java @@ -33,7 +33,6 @@ import com.android.sdkuilib.internal.repository.icons.ImageFactory; import com.android.sdkuilib.repository.ISdkChangeListener; import com.android.sdkuilib.ui.GridDataBuilder; import com.android.sdkuilib.ui.GridLayoutBuilder; -import com.android.util.Pair; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.viewers.CheckStateChangedEvent; @@ -125,13 +124,7 @@ public class PackagesPage extends UpdaterPage private final Map<MenuAction, MenuItem> mMenuActions = new HashMap<MenuAction, MenuItem>(); - private final PackageLoader mPackageLoader; - - private final List<PkgCategory> mCategories = new ArrayList<PkgCategory>(); - /** Access to this list must be synchronized on {@link #mPackages}. */ - private final List<PkgItem> mPackages = new ArrayList<PkgItem>(); - private final UpdaterData mUpdaterData; - + private final PackagesPageLogic mLogic; private boolean mDisplayArchives = false; private Text mTextSdkOsPath; @@ -153,21 +146,24 @@ public class PackagesPage extends UpdaterPage private TreeViewerColumn mColumnStatus; private Font mTreeFontItalic; private TreeColumn mTreeColumnName; - private boolean mLastSortWasByApi; private boolean mOperationPending; public PackagesPage(Composite parent, int swtStyle, UpdaterData updaterData) { super(parent, swtStyle); - mUpdaterData = updaterData; - mPackageLoader = new PackageLoader(updaterData); - createContents(this); + mLogic = new PackagesPageLogic(updaterData) { + @Override + boolean keepItem(PkgItem item) { + return PackagesPage.this.keepItem(item); + } + }; + createContents(this); postCreate(); //$hide$ } public void onPageSelected() { - if (mPackages.isEmpty()) { + if (mLogic.mAllPkgItems.isEmpty()) { // Initialize the package list the first time the page is shown. loadPackages(); } @@ -309,7 +305,7 @@ public class PackagesPage extends UpdaterPage public void widgetSelected(SelectionEvent e) { sortPackages(true /*updateButtons*/); // Reset the expanded state when changing sort algorithm - expandInitial(mCategories); + expandInitial(mLogic.mCurrentCategories); } }); mCheckSortApi.setText("API level"); @@ -323,7 +319,7 @@ public class PackagesPage extends UpdaterPage public void widgetSelected(SelectionEvent e) { sortPackages(true /*updateButtons*/); // Reset the expanded state when changing sort algorithm - expandInitial(mCategories); + expandInitial(mLogic.mCurrentCategories); } }); @@ -354,8 +350,8 @@ public class PackagesPage extends UpdaterPage } private Image getImage(String filename) { - if (mUpdaterData != null) { - ImageFactory imgFactory = mUpdaterData.getImageFactory(); + if (mLogic.mUpdaterData != null) { + ImageFactory imgFactory = mLogic.mUpdaterData.getImageFactory(); if (imgFactory != null) { return imgFactory.getImageByName(filename); } @@ -382,7 +378,7 @@ public class PackagesPage extends UpdaterPage loadPackages(); break; case SHOW_ADDON_SITES: - AddonSitesDialog d = new AddonSitesDialog(getShell(), mUpdaterData); + AddonSitesDialog d = new AddonSitesDialog(getShell(), mLogic.mUpdaterData); if (d.open()) { loadPackages(); } @@ -487,8 +483,8 @@ public class PackagesPage extends UpdaterPage } private void postCreate() { - if (mUpdaterData != null) { - mTextSdkOsPath.setText(mUpdaterData.getOsSdkRoot()); + if (mLogic.mUpdaterData != null) { + mTextSdkOsPath.setText(mLogic.mUpdaterData.getOsSdkRoot()); } mTreeViewer.setContentProvider(new PkgContentProvider()); @@ -515,31 +511,31 @@ public class PackagesPage extends UpdaterPage } private void loadPackages() { - if (mUpdaterData == null) { + if (mLogic.mUpdaterData == null) { return; } - final boolean firstLoad = mPackages.isEmpty(); + final boolean firstLoad = mLogic.mAllPkgItems.isEmpty(); - // Load package is synchronous but does not block the UI. + // LoadPackage is synchronous but does not block the UI. // Consequently it's entirely possible for the user // to request the app to close whilst the packages are loading. Any // action done after loadPackages must check the UI hasn't been // disposed yet. Otherwise hilarity ensues. - mPackageLoader.loadPackages(new ISourceLoadedCallback() { + mLogic.mPackageLoader.loadPackages(new ISourceLoadedCallback() { public boolean onSourceLoaded(List<PkgItem> newPkgItems) { boolean somethingNew = false; - synchronized(mPackages) { + synchronized(mLogic.mAllPkgItems) { nextNewItem: for (PkgItem newItem : newPkgItems) { - for (PkgItem existingItem : mPackages) { + for (PkgItem existingItem : mLogic.mAllPkgItems) { if (existingItem.isSameItemAs(newItem)) { // This isn't a new package, we already have it. continue nextNewItem; } } - mPackages.add(newItem); + mLogic.mAllPkgItems.add(newItem); somethingNew = true; } } @@ -556,7 +552,7 @@ public class PackagesPage extends UpdaterPage if (!mGroupPackages.isDisposed()) { if (firstLoad) { // set the initial expanded state - expandInitial(mCategories); + expandInitial(mLogic.mCurrentCategories); } updateButtonsState(); updateMenuCheckmarks(); @@ -599,165 +595,20 @@ public class PackagesPage extends UpdaterPage /** * Recompute the tree by sorting all the packages by API. - * This does an update in-place of the mCategories list so that the table + * This does an update in-place of the mApiCategories list so that the table * can preserve its state (checked / expanded / selected) properly. */ private void sortByApiLevel() { - ImageFactory imgFactory = mUpdaterData.getImageFactory(); - if (!mTreeColumnName.isDisposed()) { mTreeColumnName.setImage(getImage(ICON_SORT_BY_API)); } - // If the sorting mode changed, clear the categories. - if (!mLastSortWasByApi) { - mLastSortWasByApi = true; - mCategories.clear(); - } - - // keep a map of the initial state so that we can detect which items or categories are - // no longer being used, so that we can removed them at the end of the in-place update. - final Map<Integer, Pair<PkgApiCategory, HashSet<PkgItem>> > unusedItemsMap = - new HashMap<Integer, Pair<PkgApiCategory, HashSet<PkgItem>> >(); - final Set<PkgApiCategory> unusedCatSet = new HashSet<PkgApiCategory>(); - - // get existing categories - for (PkgCategory cat : mCategories) { - if (cat instanceof PkgApiCategory) { - PkgApiCategory acat = (PkgApiCategory) cat; - unusedCatSet.add(acat); - unusedItemsMap.put(acat.getKey(), - Pair.of(acat, new HashSet<PkgItem>(acat.getItems()))); - } - } - - // always add the tools & extras categories, even if empty (unlikely anyway) - if (!unusedItemsMap.containsKey(PkgApiCategory.KEY_TOOLS)) { - PkgApiCategory cat = new PkgApiCategory( - PkgApiCategory.KEY_TOOLS, - null, - imgFactory.getImageByName(ICON_CAT_OTHER)); - unusedItemsMap.put(PkgApiCategory.KEY_TOOLS, Pair.of(cat, new HashSet<PkgItem>())); - mCategories.add(cat); - } - - if (!unusedItemsMap.containsKey(PkgApiCategory.KEY_EXTRA)) { - PkgApiCategory cat = new PkgApiCategory( - PkgApiCategory.KEY_EXTRA, - null, - imgFactory.getImageByName(ICON_CAT_OTHER)); - unusedItemsMap.put(PkgApiCategory.KEY_EXTRA, Pair.of(cat, new HashSet<PkgItem>())); - mCategories.add(cat); - } - - synchronized (mPackages) { - for (PkgItem item : mPackages) { - if (!keepItem(item)) { - continue; - } - - int apiKey = item.getApi(); - - if (apiKey < 1) { - Package p = item.getPackage(); - if (p instanceof ToolPackage || p instanceof PlatformToolPackage) { - apiKey = PkgApiCategory.KEY_TOOLS; - } else { - apiKey = PkgApiCategory.KEY_EXTRA; - } - } - - Pair<PkgApiCategory, HashSet<PkgItem>> mapEntry = unusedItemsMap.get(apiKey); - - if (mapEntry == null) { - // This is a new category. Create it and add it to the map. - - // We need a label for the category. - // If we have an API level, try to get the info from the SDK Manager. - // If we don't (e.g. when installing a new platform that isn't yet available - // locally in the SDK Manager), it's OK we'll try to find the first platform - // package available. - String platformName = null; - if (apiKey != -1) { - for (IAndroidTarget target : mUpdaterData.getSdkManager().getTargets()) { - if (target.isPlatform() && target.getVersion().getApiLevel() == apiKey) { - platformName = target.getVersionName(); - break; - } - } - } - - PkgApiCategory cat = new PkgApiCategory( - apiKey, - platformName, - imgFactory.getImageByName(ICON_CAT_PLATFORM)); - mapEntry = Pair.of(cat, new HashSet<PkgItem>()); - unusedItemsMap.put(apiKey, mapEntry); - mCategories.add(0, cat); - } - PkgApiCategory cat = mapEntry.getFirst(); - assert cat != null; - unusedCatSet.remove(cat); - - HashSet<PkgItem> unusedItemsSet = mapEntry.getSecond(); - unusedItemsSet.remove(item); - if (!cat.getItems().contains(item)) { - cat.getItems().add(item); - } - - if (apiKey != -1 && cat.getPlatformName() == null) { - // Check whether we can get the actual platform version name (e.g. "1.5") - // from the first Platform package we find in this category. - Package p = item.getPackage(); - if (p instanceof PlatformPackage) { - String platformName = ((PlatformPackage) p).getVersionName(); - cat.setPlatformName(platformName); - } - } - } - } - - for (Iterator<PkgCategory> iterCat = mCategories.iterator(); iterCat.hasNext(); ) { - PkgCategory cat = iterCat.next(); - - // Remove any unused categories. - if (unusedCatSet.contains(cat)) { - iterCat.remove(); - continue; - } + mLogic.sortByApiLevel(); - // Remove any unused items in the category. - int apikey = cat.getKey(); - Pair<PkgApiCategory, HashSet<PkgItem>> mapEntry = unusedItemsMap.get(apikey); - if (mapEntry == null) { //DEBUG - apikey = (apikey + 1) - 1; - } - assert mapEntry != null; - HashSet<PkgItem> unusedItems = mapEntry.getSecond(); - for (Iterator<PkgItem> iterItem = cat.getItems().iterator(); iterItem.hasNext(); ) { - PkgItem item = iterItem.next(); - if (unusedItems.contains(item)) { - iterItem.remove(); - } - } - - // Sort the items - Collections.sort(cat.getItems()); - } - - // Sort the categories list. - Collections.sort(mCategories, new Comparator<PkgCategory>() { - public int compare(PkgCategory cat1, PkgCategory cat2) { - // We always want categories in order tools..platforms..extras. - // For platform, we compare in descending order (o2-o1). - return cat2.getKey() - cat1.getKey(); - } - }); - - if (mTreeViewer.getInput() != mCategories) { + if (mTreeViewer.getInput() != mLogic.mCurrentCategories) { // set initial input - mTreeViewer.setInput(mCategories); + mTreeViewer.setInput(mLogic.mCurrentCategories); } else { // refresh existing, which preserves the expanded state, the selection // and the checked state. @@ -774,64 +625,17 @@ public class PackagesPage extends UpdaterPage mTreeColumnName.setImage(getImage(ICON_SORT_BY_SOURCE)); } - mLastSortWasByApi = false; - mCategories.clear(); - - Map<SdkSource, List<PkgItem>> sourceMap = new HashMap<SdkSource, List<PkgItem>>(); - - synchronized(mPackages) { - for (PkgItem item : mPackages) { - if (keepItem(item)) { - SdkSource source = item.getSource(); - List<PkgItem> list = sourceMap.get(source); - if (list == null) { - list = new ArrayList<PkgItem>(); - sourceMap.put(source, list); - } - list.add(item); - } - } - } - - // Sort the sources so that we can create categories sorted the same way - // (the categories don't link to the sources, so we can't just sort the categories.) - Set<SdkSource> sources = new TreeSet<SdkSource>(new Comparator<SdkSource>() { - public int compare(SdkSource o1, SdkSource o2) { - if (o1 == o2) { - return 0; - } else if (o1 == null && o2 != null) { - return -1; - } else if (o1 != null && o2 == null) { - return 1; - } - assert o1 != null; - return o1.toString().compareTo(o2.toString()); - } - }); - sources.addAll(sourceMap.keySet()); - - for (SdkSource source : sources) { - Object key = source != null ? source : "Locally Installed Packages"; - Object iconRef = source != null ? source : - mUpdaterData.getImageFactory().getImageByName(ICON_PKG_INSTALLED); - - PkgCategory cat = new PkgCategory( - key.hashCode(), - key.toString(), - iconRef); - - for (PkgItem item : sourceMap.get(source)) { - if (item.getSource() == source) { - cat.getItems().add(item); - } - } - - mCategories.add(cat); - } + mLogic.sortBySource(); // We don't support in-place incremental updates so the table gets reset // each time we load when sorted by source. - mTreeViewer.setInput(mCategories); + if (mTreeViewer.getInput() != mLogic.mCurrentCategories) { + mTreeViewer.setInput(mLogic.mCurrentCategories); + } else { + // refresh existing, which preserves the expanded state, the selection + // and the checked state. + mTreeViewer.refresh(); + } } /** @@ -851,8 +655,7 @@ public class PackagesPage extends UpdaterPage } if (!mCheckFilterNew.getSelection()) { - if (item.getState() == PkgState.NEW || - item.getState() == PkgState.HAS_UPDATE) { + if (item.getState() == PkgState.NEW || item.hasUpdatePkg()) { return false; } } @@ -875,8 +678,7 @@ public class PackagesPage extends UpdaterPage if (pkg instanceof PkgCategory) { PkgCategory cat = (PkgCategory) pkg; for (PkgItem item : cat.getItems()) { - if (item.getState() == PkgState.INSTALLED - || item.getState() == PkgState.HAS_UPDATE) { + if (item.getState() == PkgState.INSTALLED) { expandInitial(pkg); break; } @@ -986,7 +788,7 @@ public class PackagesPage extends UpdaterPage break; } } else if (c instanceof PkgItem) { - if (((PkgItem) c).getPackage().hasCompatibleArchive()) { + if (((PkgItem) c).getMainPackage().hasCompatibleArchive()) { canInstall = true; break; } @@ -1004,7 +806,7 @@ public class PackagesPage extends UpdaterPage for (Object c : checked) { if (c instanceof PkgItem) { PkgState state = ((PkgItem) c).getState(); - if (state == PkgState.INSTALLED || state == PkgState.HAS_UPDATE) { + if (state == PkgState.INSTALLED) { canDelete = true; break; } @@ -1017,12 +819,11 @@ public class PackagesPage extends UpdaterPage private void onSelectNewUpdates() { ITreeContentProvider provider = (ITreeContentProvider) mTreeViewer.getContentProvider(); - synchronized(mPackages) { - for (PkgCategory cat : mCategories) { + synchronized(mLogic.mAllPkgItems) { + for (PkgCategory cat : mLogic.mCurrentCategories) { boolean selected = false; for (PkgItem item : cat.getItems()) { - PkgState state = item.getState(); - if (state == PkgState.NEW || state == PkgState.HAS_UPDATE) { + if (item.getState() == PkgState.NEW || item.hasUpdatePkg()) { mTreeViewer.setChecked(item, true); checkExpandItem(item, provider); selected = true; @@ -1071,14 +872,14 @@ public class PackagesPage extends UpdaterPage // This is an update package p = (Package) c; } else if (c instanceof PkgItem) { - p = ((PkgItem) c).getPackage(); + p = ((PkgItem) c).getMainPackage(); PkgItem pi = (PkgItem) c; - if (pi.getState() == PkgState.HAS_UPDATE) { - List<Package> updates = pi.getUpdatePkgs(); + if (pi.getState() == PkgState.INSTALLED) { + Package updPkg = pi.getUpdatePkg(); + if (updPkg != null) { // If there's one and only one update, auto-select it instead. - if (updates != null && updates.size() == 1) { - p = updates.get(0); + p = updPkg; } } } @@ -1093,11 +894,11 @@ public class PackagesPage extends UpdaterPage } } - if (mUpdaterData != null) { + if (mLogic.mUpdaterData != null) { try { beginOperationPending(); - mUpdaterData.updateOrInstallAll_WithGUI( + mLogic.mUpdaterData.updateOrInstallAll_WithGUI( archives, mCheckFilterObsolete.getSelection() /* includeObsoletes */); } finally { @@ -1106,9 +907,9 @@ public class PackagesPage extends UpdaterPage // Remove any pkg item matching anything we potentially installed // then request the package list to be updated. This will prevent // from having stale entries. - synchronized(mPackages) { + synchronized(mLogic.mAllPkgItems) { for (Archive a : archives) { - for (Iterator<PkgItem> it = mPackages.iterator(); it.hasNext(); ) { + for (Iterator<PkgItem> it = mLogic.mAllPkgItems.iterator(); it.hasNext(); ) { PkgItem pi = it.next(); if (pi.hasArchive(a)) { it.remove(); @@ -1119,7 +920,7 @@ public class PackagesPage extends UpdaterPage } // The local package list has changed, make sure to refresh it - mUpdaterData.getLocalSdkParser().clearPackages(); + mLogic.mUpdaterData.getLocalSdkParser().clearPackages(); loadPackages(); } } @@ -1143,8 +944,8 @@ public class PackagesPage extends UpdaterPage if (c instanceof PkgItem) { PkgItem pi = (PkgItem) c; PkgState state = pi.getState(); - if (state == PkgState.INSTALLED || state == PkgState.HAS_UPDATE) { - Package p = pi.getPackage(); + if (state == PkgState.INSTALLED) { + Package p = pi.getMainPackage(); Archive[] as = p.getArchives(); if (as.length == 1 && as[0] != null && as[0].isLocal()) { @@ -1167,7 +968,7 @@ public class PackagesPage extends UpdaterPage try { beginOperationPending(); - mUpdaterData.getTaskFactory().start("Delete Package", new ITask() { + mLogic.mUpdaterData.getTaskFactory().start("Delete Package", new ITask() { public void run(ITaskMonitor monitor) { monitor.setProgressMax(archives.size() + 1); for (Entry<Archive, PkgItem> entry : archives.entrySet()) { @@ -1180,8 +981,8 @@ public class PackagesPage extends UpdaterPage // Delete the actual package and its internal representation a.deleteLocal(); - synchronized(mPackages) { - mPackages.remove(entry.getValue()); + synchronized(mLogic.mAllPkgItems) { + mLogic.mAllPkgItems.remove(entry.getValue()); } monitor.incProgress(1); @@ -1198,7 +999,7 @@ public class PackagesPage extends UpdaterPage endOperationPending(); // The local package list has changed, make sure to refresh it - mUpdaterData.getLocalSdkParser().clearPackages(); + mLogic.mUpdaterData.getLocalSdkParser().clearPackages(); loadPackages(); } } @@ -1244,8 +1045,7 @@ public class PackagesPage extends UpdaterPage if (element instanceof PkgItem) { PkgItem pkg = (PkgItem) element; - if (pkg.getState() == PkgState.INSTALLED || - pkg.getState() == PkgState.HAS_UPDATE) { + if (pkg.getState() == PkgState.INSTALLED) { return Integer.toString(pkg.getRevision()); } } @@ -1257,19 +1057,16 @@ public class PackagesPage extends UpdaterPage switch(pkg.getState()) { case INSTALLED: - return "Installed"; - case HAS_UPDATE: - List<Package> updates = pkg.getUpdatePkgs(); - if (updates.size() == 1) { - return String.format("Update available: rev. %1$s", - updates.get(0).getRevision()); - } else { - // This case should not happen in *our* typical release workflow. - return "Multiple updates available"; + Package update = pkg.getUpdatePkg(); + if (update != null) { + return String.format( + "Update available: rev. %1$s", + update.getRevision()); } + return "Installed"; + case NEW: return "Not installed"; - // TODO display pkg.getRevision() in a tooltip } return pkg.getState().toString(); @@ -1279,7 +1076,7 @@ public class PackagesPage extends UpdaterPage } } - return ""; //$NON-NLS-1$ + return ""; } private String getPkgItemname(PkgItem item) { @@ -1306,7 +1103,7 @@ public class PackagesPage extends UpdaterPage } private PkgCategory findCategoryForItem(PkgItem item) { - for (PkgCategory cat : mCategories) { + for (PkgCategory cat : mLogic.mCurrentCategories) { for (PkgItem i : cat.getItems()) { if (i == item) { return cat; @@ -1319,23 +1116,26 @@ public class PackagesPage extends UpdaterPage @Override public Image getImage(Object element) { - ImageFactory imgFactory = mUpdaterData.getImageFactory(); + ImageFactory imgFactory = mLogic.mUpdaterData.getImageFactory(); if (imgFactory != null) { if (mColumn == mColumnName) { if (element instanceof PkgCategory) { return imgFactory.getImageForObject(((PkgCategory) element).getIconRef()); } else if (element instanceof PkgItem) { - return imgFactory.getImageForObject(((PkgItem) element).getPackage()); + return imgFactory.getImageForObject(((PkgItem) element).getMainPackage()); } return imgFactory.getImageForObject(element); } else if (mColumn == mColumnStatus && element instanceof PkgItem) { - switch(((PkgItem) element).getState()) { + PkgItem pi = (PkgItem) element; + switch(pi.getState()) { case INSTALLED: - return imgFactory.getImageByName(ICON_PKG_INSTALLED); - case HAS_UPDATE: - return imgFactory.getImageByName(ICON_PKG_UPDATE); + if (pi.hasUpdatePkg()) { + return imgFactory.getImageByName(ICON_PKG_UPDATE); + } else { + return imgFactory.getImageByName(ICON_PKG_INSTALLED); + } case NEW: return imgFactory.getImageByName(ICON_PKG_NEW); } @@ -1370,15 +1170,15 @@ public class PackagesPage extends UpdaterPage return ((PkgCategory) parentElement).getItems().toArray(); } else if (parentElement instanceof PkgItem) { - List<Package> pkgs = ((PkgItem) parentElement).getUpdatePkgs(); + if (mDisplayArchives) { - // Display update packages as sub-items if there's more than one - // or if the archive/details mode is activated. - if (pkgs != null && (pkgs.size() > 1 || mDisplayArchives)) { - return pkgs.toArray(); - } + Package pkg = ((PkgItem) parentElement).getUpdatePkg(); + + // Display update packages as sub-items if the details mode is activated. + if (pkg != null) { + return new Object[] { pkg }; + } - if (mDisplayArchives) { return ((PkgItem) parentElement).getArchives(); } @@ -1406,15 +1206,14 @@ public class PackagesPage extends UpdaterPage return true; } else if (parentElement instanceof PkgItem) { - List<Package> pkgs = ((PkgItem) parentElement).getUpdatePkgs(); + if (mDisplayArchives) { + Package pkg = ((PkgItem) parentElement).getUpdatePkg(); - // Display update packages as sub-items if there's more than one - // or if the archive/details mode is activated. - if (pkgs != null && (pkgs.size() > 1 || mDisplayArchives)) { - return !pkgs.isEmpty(); - } + // Display update packages as sub-items if the details mode is activated. + if (pkg != null) { + return true; + } - if (mDisplayArchives) { Archive[] archives = ((PkgItem) parentElement).getArchives(); return archives.length > 0; } @@ -1441,7 +1240,7 @@ public class PackagesPage extends UpdaterPage } } - private static class PkgCategory { + static class PkgCategory { private final int mKey; private final Object mIconRef; private final List<PkgItem> mItems = new ArrayList<PkgItem>(); @@ -1476,6 +1275,35 @@ public class PackagesPage extends UpdaterPage public List<PkgItem> getItems() { return mItems; } + + @Override + public String toString() { + return String.format("%s <key=%08x, label=%s, #items=%d>", + this.getClass().getSimpleName(), + mKey, + mLabel, + mItems.size()); + } + + /** {@link PkgCategory} are equal if their internal key is the same. */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + mKey; + return result; + } + + /** {@link PkgCategory} are equal if their internal key is the same. */ + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + PkgCategory other = (PkgCategory) obj; + if (mKey != other.mKey) return false; + return true; + } } private static class PkgApiCategory extends PkgCategory { @@ -1510,10 +1338,13 @@ public class PackagesPage extends UpdaterPage public String getApiLabel() { int api = getKey(); - if (api > 0) { + if (api == KEY_TOOLS) { + return "TOOLS"; //$NON-NLS-1$ for internal use only + } else if (api == KEY_EXTRA) { + return "EXTRAS"; //$NON-NLS-1$ for internal use only + } else { return String.format("API %1$d", getKey()); } - return null; } @Override @@ -1540,7 +1371,16 @@ public class PackagesPage extends UpdaterPage @Override public void setLabel(String label) { - throw new UnsupportedOperationException("Use setPlatformName() instead."); //$NON-NLS-1$ + throw new UnsupportedOperationException("Use setPlatformName() instead."); + } + + @Override + public String toString() { + return String.format("%s <API=%s, label=%s, #items=%d>", + this.getClass().getSimpleName(), + getApiLabel(), + getLabel(), + getItems().size()); } } @@ -1563,6 +1403,296 @@ public class PackagesPage extends UpdaterPage // nothing to be done for now. } + + /** + * Helper class that separate the logic of package management from the UI + * so that we can test it using head-less unit tests. + */ + static abstract class PackagesPageLogic { + final PackageLoader mPackageLoader; + final UpdaterData mUpdaterData; + + final List<PkgCategory> mApiCategories = new ArrayList<PkgCategory>(); + final List<PkgCategory> mSourceCategories = new ArrayList<PkgCategory>(); + List<PkgCategory> mCurrentCategories = mApiCategories; + /** Access to this list must be synchronized on {@link #mAllPkgItems}. */ + final List<PkgItem> mAllPkgItems = new ArrayList<PkgItem>(); + + public PackagesPageLogic(UpdaterData updaterData) { + mUpdaterData = updaterData; + mPackageLoader = new PackageLoader(updaterData); + } + + /** + * Recompute the tree by sorting all the packages by API. + * This does an update in-place of the mApiCategories list so that the table + * can preserve its state (checked / expanded / selected) properly. + */ + void sortByApiLevel() { + ImageFactory imgFactory = mUpdaterData.getImageFactory(); + + mCurrentCategories = mApiCategories; + + // We'll do an in-place update: first make a map of existing categories and + // whatever pkg items they contain. Then prepare the new categories we want + // which is all the existing categories + tools & extra (creating them the first time). + // We mark all the existing items as "unused" then remove from the unused set the + // items that we want to keep. At the end, whatever is left in the unused maps + // are obsolete items we remove from the tree. + + // Keep a map of the initial state so that we can detect which items or categories are + // no longer being used, so that we can remove them at the end of the in-place update. + + final Map<Integer, PkgApiCategory> categoryKeyMap = new HashMap<Integer, PkgApiCategory>(); + final Set<Integer> unusedCategoryKey = new HashSet<Integer>(); + final Set<Package> unusedPackages = new HashSet<Package>(); + + // Get existing categories and packages + for (PkgCategory cat : mApiCategories) { + if (cat instanceof PkgApiCategory) { + PkgApiCategory acat = (PkgApiCategory) cat; + categoryKeyMap.put(acat.getKey(), acat); + unusedCategoryKey.add(acat.getKey()); + + for (PkgItem pi : cat.getItems()) { + unusedPackages.add(pi.getMainPackage()); + if (pi.hasUpdatePkg()) { + unusedPackages.add(pi.getUpdatePkg()); + } + } + } + } + + // Always add the tools & extras categories, even if empty (unlikely anyway) + if (!unusedCategoryKey.contains(PkgApiCategory.KEY_TOOLS)) { + PkgApiCategory acat = new PkgApiCategory( + PkgApiCategory.KEY_TOOLS, + null, + imgFactory.getImageByName(ICON_CAT_OTHER)); + mApiCategories.add(acat); + categoryKeyMap.put(acat.getKey(), acat); + unusedCategoryKey.add(acat.getKey()); + } + + if (!unusedCategoryKey.contains(PkgApiCategory.KEY_EXTRA)) { + PkgApiCategory acat = new PkgApiCategory( + PkgApiCategory.KEY_EXTRA, + null, + imgFactory.getImageByName(ICON_CAT_OTHER)); + mApiCategories.add(acat); + categoryKeyMap.put(acat.getKey(), acat); + unusedCategoryKey.add(acat.getKey()); + } + + // Go through the new package item list + synchronized (mAllPkgItems) { + for (PkgItem newItem : mAllPkgItems) { + // Is this a package we want to display? That may change depending on the + // display filter (obsolete, new/updates, etc.) + if (!keepItem(newItem)) { + continue; + } + + // Get the category for this item. + int apiKey = newItem.getApi(); + + if (apiKey < 1) { + Package p = newItem.getMainPackage(); + if (p instanceof ToolPackage || p instanceof PlatformToolPackage) { + apiKey = PkgApiCategory.KEY_TOOLS; + } else { + apiKey = PkgApiCategory.KEY_EXTRA; + } + } + + PkgApiCategory cat = categoryKeyMap.get(apiKey); + + if (cat == null) { + // This is a new category. Create it and add it to the map. + + // We need a label for the category. + // If we have an API level, try to get the info from the SDK Manager. + // If we don't (e.g. when installing a new platform that isn't yet available + // locally in the SDK Manager), it's OK we'll try to find the first platform + // package available. + String platformName = null; + if (apiKey != -1) { + for (IAndroidTarget target : mUpdaterData.getSdkManager().getTargets()) { + if (target.isPlatform() && target.getVersion().getApiLevel() == apiKey) { + platformName = target.getVersionName(); + break; + } + } + } + + cat = new PkgApiCategory( + apiKey, + platformName, + imgFactory.getImageByName(ICON_CAT_PLATFORM)); + mApiCategories.add(0, cat); + categoryKeyMap.put(cat.getKey(), cat); + } else { + // Remove the category key from the unused list. + unusedCategoryKey.remove(apiKey); + } + + // Add the item to the category or merge as an update of an existing package + boolean found = false; + if (newItem.getState() == PkgState.NEW) { + for (PkgItem pi : cat.getItems()) { + Package p = newItem.getMainPackage(); + if (pi.isSameItemAs(newItem) || + pi.isSameMainPackageAs(p)) { + // It's the same item or + // it's not exactly the same item but the main package is the same. + unusedPackages.remove(pi.getMainPackage()); + found = true; + + } else if (pi.mergeUpdate(p)) { + // The new package is an update for the existing package. + unusedPackages.remove(pi.getMainPackage()); + unusedPackages.remove(pi.getUpdatePkg()); + found = true; + } + if (found) { + break; + } + } + } else { + // We do not try to merge installed packages. This prevents a bug in + // the edge case where a new update might be present in the package + // list before the installed item it would update. If we were trying + // to merge the installed item into the new package, the installed item + // would most likely be hidden because it would have a lesser revision. + // This case is not supposed to happen but if it does, we 'd better have + // a dup than a missing displayed package. + + for (PkgItem pi : cat.getItems()) { + if (pi.isSameItemAs(newItem)) { + unusedPackages.remove(newItem.getMainPackage()); + found = true; + break; + } + } + } + + if (!found) { + cat.getItems().add(newItem); + unusedPackages.remove(newItem.getMainPackage()); + unusedPackages.remove(newItem.getUpdatePkg()); + } + + if (apiKey != -1 && cat.getPlatformName() == null) { + // Check whether we can get the actual platform version name (e.g. "1.5") + // from the first Platform package we find in this category. + Package p = newItem.getMainPackage(); + if (p instanceof PlatformPackage) { + String platformName = ((PlatformPackage) p).getVersionName(); + cat.setPlatformName(platformName); + } + } + } + } + + // Now go through all the remaining categories used for the tree and clear unused items. + for (Iterator<PkgCategory> iterCat = mApiCategories.iterator(); iterCat.hasNext(); ) { + PkgCategory cat = iterCat.next(); + + // Remove any unused categories. + if (unusedCategoryKey.contains(cat.getKey())) { + iterCat.remove(); + continue; + } + + // Remove any unused items in the category. + for (Iterator<PkgItem> iterItem = cat.getItems().iterator(); iterItem.hasNext(); ) { + PkgItem item = iterItem.next(); + + if (unusedPackages.contains(item.getMainPackage())) { + iterItem.remove(); + } else if (item.hasUpdatePkg() && unusedPackages.contains(item.getUpdatePkg())) { + item.removeUpdate(); + } + } + + // Sort the items + Collections.sort(cat.getItems()); + } + + // Sort the categories list. + Collections.sort(mApiCategories, new Comparator<PkgCategory>() { + public int compare(PkgCategory cat1, PkgCategory cat2) { + // We always want categories in order tools..platforms..extras. + // For platform, we compare in descending order (o2-o1). + return cat2.getKey() - cat1.getKey(); + } + }); + } + + /** + * Recompute the tree by sorting all packages by source. + */ + void sortBySource() { + + mCurrentCategories = mSourceCategories; + + Map<SdkSource, List<PkgItem>> sourceMap = new HashMap<SdkSource, List<PkgItem>>(); + + synchronized(mAllPkgItems) { + for (PkgItem item : mAllPkgItems) { + if (keepItem(item)) { + SdkSource source = item.getSource(); + List<PkgItem> list = sourceMap.get(source); + if (list == null) { + list = new ArrayList<PkgItem>(); + sourceMap.put(source, list); + } + list.add(item); + } + } + } + + // Sort the sources so that we can create categories sorted the same way + // (the categories don't link to the sources, so we can't just sort the categories.) + Set<SdkSource> sources = new TreeSet<SdkSource>(new Comparator<SdkSource>() { + public int compare(SdkSource o1, SdkSource o2) { + if (o1 == o2) { + return 0; + } else if (o1 == null && o2 != null) { + return -1; + } else if (o1 != null && o2 == null) { + return 1; + } + assert o1 != null; + return o1.toString().compareTo(o2.toString()); + } + }); + sources.addAll(sourceMap.keySet()); + + for (SdkSource source : sources) { + Object key = source != null ? source : "Locally Installed Packages"; + Object iconRef = source != null ? source : + mUpdaterData.getImageFactory().getImageByName(ICON_PKG_INSTALLED); + + PkgCategory cat = new PkgCategory( + key.hashCode(), + key.toString(), + iconRef); + + for (PkgItem item : sourceMap.get(source)) { + if (item.getSource() == source) { + cat.getItems().add(item); + } + } + + mSourceCategories.add(cat); + } + } + + abstract boolean keepItem(PkgItem item); + } + + // --- End of hiding from SWT Designer --- //$hide<<$ } diff --git a/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/MockEmptyPackage.java b/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/MockEmptyPackage.java new file mode 100755 index 0000000..77b5992 --- /dev/null +++ b/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/MockEmptyPackage.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2011 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.SdkManager; +import com.android.sdklib.internal.repository.Archive; +import com.android.sdklib.internal.repository.Package; +import com.android.sdklib.internal.repository.Archive.Arch; +import com.android.sdklib.internal.repository.Archive.Os; + +import java.io.File; +import java.util.Properties; + +class MockEmptyPackage extends Package { + private final String mTestHandle; + + public MockEmptyPackage(String testHandle) { + super( + null /*source*/, + null /*props*/, + 0 /*revision*/, + null /*license*/, + null /*description*/, + null /*descUrl*/, + Os.ANY /*archiveOs*/, + Arch.ANY /*archiveArch*/, + null /*archiveOsPath*/ + ); + mTestHandle = testHandle; + } + + public MockEmptyPackage(String testHandle, int revision) { + super( + null /*source*/, + null /*props*/, + revision, + null /*license*/, + null /*description*/, + null /*descUrl*/, + Os.ANY /*archiveOs*/, + Arch.ANY /*archiveArch*/, + null /*archiveOsPath*/ + ); + mTestHandle = testHandle; + } + + @Override + protected Archive createLocalArchive( + Properties props, + Os archiveOs, + Arch archiveArch, + String archiveOsPath) { + return new Archive(this, props, archiveOs, archiveArch, archiveOsPath) { + @Override + public String toString() { + return mTestHandle; + } + }; + } + + public Archive getLocalArchive() { + return getArchives()[0]; + } + + @Override + public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) { + return null; + } + + @Override + public String getListDescription() { + return this.getClass().getSimpleName(); + } + + @Override + public String getShortDescription() { + StringBuilder sb = new StringBuilder(this.getClass().getSimpleName()); + if (getRevision() > 0) { + sb.append(" rev=").append(getRevision()); + } + return sb.toString(); + } + + @Override + public boolean sameItemAs(Package pkg) { + return (pkg instanceof MockEmptyPackage) && + mTestHandle.equals(((MockEmptyPackage) pkg).mTestHandle); + } + +} diff --git a/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/MockUpdaterData.java b/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/MockUpdaterData.java new file mode 100755 index 0000000..20cd48a --- /dev/null +++ b/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/MockUpdaterData.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2011 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.SdkManager; +import com.android.sdklib.internal.repository.Archive; +import com.android.sdklib.internal.repository.ArchiveInstaller; +import com.android.sdklib.internal.repository.ITask; +import com.android.sdklib.internal.repository.ITaskFactory; +import com.android.sdklib.internal.repository.ITaskMonitor; +import com.android.sdklib.internal.repository.MockEmptySdkManager; +import com.android.sdklib.mock.MockLog; +import com.android.sdkuilib.internal.repository.icons.ImageFactory; + +import org.eclipse.swt.graphics.Image; + +import java.util.ArrayList; +import java.util.List; + +/** A mock UpdaterData that simply records what would have been installed. */ +class MockUpdaterData extends UpdaterData { + + public final static String SDK_PATH = "/tmp/SDK"; + + private final List<Archive> mInstalled = new ArrayList<Archive>(); + + public MockUpdaterData() { + super(SDK_PATH, new MockLog()); + + setTaskFactory(new MockTaskFactory()); + setImageFactory(new NullImageFactory()); + } + + /** Gives access to the internal {@link #installArchives(List)}. */ + public void _installArchives(List<ArchiveInfo> result) { + installArchives(result); + } + + public Archive[] getInstalled() { + return mInstalled.toArray(new Archive[mInstalled.size()]); + } + + @Override + protected void initSdk() { + setSdkManager(new MockEmptySdkManager(SDK_PATH)); + } + + @Override + public void reloadSdk() { + // bypass original implementation + } + + /** Returns a mock installer that simply records what would have been installed. */ + @Override + protected ArchiveInstaller createArchiveInstaler() { + return new ArchiveInstaller() { + @Override + public boolean install( + Archive archive, + String osSdkRoot, + boolean forceHttp, + SdkManager sdkManager, + ITaskMonitor monitor) { + mInstalled.add(archive); + return true; + } + }; + } + + //------------ + + private class MockTaskFactory implements ITaskFactory { + public void start(String title, ITask task) { + new MockTask(task); + } + } + + //------------ + + private static class MockTask implements ITaskMonitor { + public MockTask(ITask task) { + task.run(this); + } + + public ITaskMonitor createSubMonitor(int tickCount) { + return this; + } + + public boolean displayPrompt(String title, String message) { + return false; + } + + public int getProgress() { + return 0; + } + + public void incProgress(int delta) { + // ignore + } + + public boolean isCancelRequested() { + return false; + } + + public void setDescription(String format, Object... args) { + // ignore + } + + public void setProgressMax(int max) { + // ignore + } + + public void log(String format, Object... args) { + // ignore + } + + public void logError(String format, Object... args) { + // ignore + } + + public void logVerbose(String format, Object... args) { + // ignore + } + } + + //------------ + + private static class NullImageFactory extends ImageFactory { + public NullImageFactory() { + // pass + super(null /*display*/); + } + + @Override + public Image getImageByName(String imageName) { + return null; + } + + @Override + public Image getImageForObject(Object object) { + return null; + } + + @Override + public void dispose() { + // pass + } + + } +} diff --git a/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/PackagesPageLogicTest.java b/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/PackagesPageLogicTest.java new file mode 100755 index 0000000..e0a28f6 --- /dev/null +++ b/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/PackagesPageLogicTest.java @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2011 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.MockAddonPackage; +import com.android.sdklib.internal.repository.MockExtraPackage; +import com.android.sdklib.internal.repository.MockPlatformPackage; +import com.android.sdklib.internal.repository.MockPlatformToolPackage; +import com.android.sdklib.internal.repository.MockToolPackage; +import com.android.sdkuilib.internal.repository.PackageLoader.PkgItem; +import com.android.sdkuilib.internal.repository.PackageLoader.PkgState; +import com.android.sdkuilib.internal.repository.PackagesPage.PackagesPageLogic; +import com.android.sdkuilib.internal.repository.PackagesPage.PkgCategory; + +import junit.framework.TestCase; + +public class PackagesPageLogicTest extends TestCase { + + private PackagesPageLogic m; + private MockUpdaterData u; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + u = new MockUpdaterData(); + m = new PackagesPageLogic(u) { + @Override + boolean keepItem(PkgItem item) { + return true; + } + }; + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + // ---- + + public void testSortByApi_Empty() { + assertTrue(m.mAllPkgItems.isEmpty()); + m.sortByApiLevel(); + assertSame(m.mCurrentCategories, m.mApiCategories); + assertTrue(m.mApiCategories.isEmpty()); + } + + public void testSortByApi_AddPackages() { + assertTrue(m.mAllPkgItems.isEmpty()); + m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage("1_new"), PkgState.NEW)); + m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage("2_installed"), PkgState.INSTALLED)); + + m.sortByApiLevel(); + + assertEquals( + "PkgApiCategory <API=EXTRAS, label=Extras, #items=2>\n" + + "-- <NEW, pkg:MockEmptyPackage>\n" + + "-- <INSTALLED, pkg:MockEmptyPackage>\n", + getTree(m)); + } + + public void testSortByApi_Update1() { + assertTrue(m.mAllPkgItems.isEmpty()); + + // Typical case: user has a locally installed package in revision 1 + // The display list after sort should show that instaled package. + m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage("type1", 1), PkgState.INSTALLED)); + + m.sortByApiLevel(); + + assertEquals( + "PkgApiCategory <API=EXTRAS, label=Extras, #items=1>\n" + + "-- <INSTALLED, pkg:MockEmptyPackage rev=1>\n", + getTree(m)); + + // Then loading sources reveals an update in revision 4 + // Edge case: another source reveals an update in revision 2. + // The display list after sort should show an update as available with rev 4 + // and rev 2 should be ignored since we have a better one. + m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage("type1", 4), PkgState.NEW)); + m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage("type1", 2), PkgState.NEW)); + + m.sortByApiLevel(); + + assertEquals( + "PkgApiCategory <API=EXTRAS, label=Extras, #items=1>\n" + + "-- <INSTALLED, pkg:MockEmptyPackage rev=1, updated by:MockEmptyPackage rev=4>\n", + getTree(m)); + } + + public void testSortByApi_CompleteUpdate() { + assertTrue(m.mAllPkgItems.isEmpty()); + + // Resulting categories are sorted by Tools, descending platform API and finally Extras. + // Addons are sorted by name within their API. + // Extras are sorted by vendor name. + // The order packages are added to the mAllPkgItems list is purposedly different from + // the final order we get. + + // Typical case is to have these 2 tools, which should get sorted in their own category + m.mAllPkgItems.add(new PkgItem(new MockToolPackage(10, 3), PkgState.INSTALLED)); + m.mAllPkgItems.add(new PkgItem(new MockPlatformToolPackage(3), PkgState.INSTALLED)); + + // Load a few extra packages + m.mAllPkgItems.add(new PkgItem(new MockExtraPackage("carrier", "custom_rom", 1, 0), PkgState.NEW)); + + // We call sortByApiLevel() multiple times to simulate the fact it works as an + // incremental diff. In real usage, it is called after each source is loaded so + // that we can progressively update the display. + m.sortByApiLevel(); + + assertEquals( + "PkgApiCategory <API=TOOLS, label=Tools, #items=2>\n" + + "-- <INSTALLED, pkg:Android SDK Tools, revision 10>\n" + + "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3>\n" + + "PkgApiCategory <API=EXTRAS, label=Extras, #items=1>\n" + + "-- <NEW, pkg:Carrier Custom Rom package, revision 1>\n", + getTree(m)); + + m.mAllPkgItems.add(new PkgItem(new MockExtraPackage("android", "usb_driver", 4, 3), PkgState.INSTALLED)); + m.mAllPkgItems.add(new PkgItem(new MockExtraPackage("android", "usb_driver", 5, 3), PkgState.NEW)); + + m.sortByApiLevel(); + + assertEquals( + "PkgApiCategory <API=TOOLS, label=Tools, #items=2>\n" + + "-- <INSTALLED, pkg:Android SDK Tools, revision 10>\n" + + "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3>\n" + + "PkgApiCategory <API=EXTRAS, label=Extras, #items=2>\n" + + "-- <INSTALLED, pkg:Android USB Driver package, revision 4, updated by:Android USB Driver package, revision 5>\n" + + "-- <NEW, pkg:Carrier Custom Rom package, revision 1>\n", + getTree(m)); + + // Platforms and addon are sorted in a category based on their API level + MockPlatformPackage p1; + MockPlatformPackage p2; + m.mAllPkgItems.add(new PkgItem(p1 = new MockPlatformPackage(1, 2, 3), PkgState.INSTALLED)); + m.mAllPkgItems.add(new PkgItem(p2 = new MockPlatformPackage(2, 4, 3), PkgState.NEW)); + m.mAllPkgItems.add(new PkgItem( new MockPlatformPackage(3, 6, 3), PkgState.INSTALLED)); + + m.sortByApiLevel(); + + assertEquals( + "PkgApiCategory <API=TOOLS, label=Tools, #items=2>\n" + + "-- <INSTALLED, pkg:Android SDK Tools, revision 10>\n" + + "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3>\n" + + "PkgApiCategory <API=API 3, label=Android android-3 (API 3), #items=1>\n" + + "-- <INSTALLED, pkg:SDK Platform Android android-3, API 3, revision 6>\n" + + "PkgApiCategory <API=API 2, label=Android android-2 (API 2), #items=1>\n" + + "-- <NEW, pkg:SDK Platform Android android-2, API 2, revision 4>\n" + + "PkgApiCategory <API=API 1, label=Android android-1 (API 1), #items=1>\n" + + "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n" + + "PkgApiCategory <API=EXTRAS, label=Extras, #items=2>\n" + + "-- <INSTALLED, pkg:Android USB Driver package, revision 4, updated by:Android USB Driver package, revision 5>\n" + + "-- <NEW, pkg:Carrier Custom Rom package, revision 1>\n", + getTree(m)); + + m.mAllPkgItems.add(new PkgItem(new MockAddonPackage("addon C",p2, 9), PkgState.NEW)); + m.mAllPkgItems.add(new PkgItem(new MockAddonPackage("addon A", p1, 5), PkgState.INSTALLED)); + m.mAllPkgItems.add(new PkgItem(new MockAddonPackage("addon A", p1, 6), PkgState.NEW)); + m.mAllPkgItems.add(new PkgItem(new MockAddonPackage("addon B",p2, 7), PkgState.NEW)); + m.mAllPkgItems.add(new PkgItem(new MockAddonPackage("addon B",p2, 8), PkgState.NEW)); // this update is ignored + m.mAllPkgItems.add(new PkgItem(new MockAddonPackage("addon B",p2, 9), PkgState.NEW)); // this updates new addon B in rev 7 + + m.sortByApiLevel(); + + assertEquals( + "PkgApiCategory <API=TOOLS, label=Tools, #items=2>\n" + + "-- <INSTALLED, pkg:Android SDK Tools, revision 10>\n" + + "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3>\n" + + "PkgApiCategory <API=API 3, label=Android android-3 (API 3), #items=1>\n" + + "-- <INSTALLED, pkg:SDK Platform Android android-3, API 3, revision 6>\n" + + "PkgApiCategory <API=API 2, label=Android android-2 (API 2), #items=3>\n" + + "-- <NEW, pkg:SDK Platform Android android-2, API 2, revision 4>\n" + + "-- <NEW, pkg:addon B by vendor 2, Android API 2, revision 7, updated by:addon B by vendor 2, Android API 2, revision 9>\n" + + "-- <NEW, pkg:addon C by vendor 2, Android API 2, revision 9>\n" + + "PkgApiCategory <API=API 1, label=Android android-1 (API 1), #items=2>\n" + + "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n" + + "-- <INSTALLED, pkg:addon A by vendor 1, Android API 1, revision 5, updated by:addon A by vendor 1, Android API 1, revision 6>\n" + + "PkgApiCategory <API=EXTRAS, label=Extras, #items=2>\n" + + "-- <INSTALLED, pkg:Android USB Driver package, revision 4, updated by:Android USB Driver package, revision 5>\n" + + "-- <NEW, pkg:Carrier Custom Rom package, revision 1>\n", + getTree(m)); + + // The only requirement is that updates MUST appear AFTER the packages they will update. + // The reverse order is not supported by the sorting algorithm and both will be shown. + + m.mAllPkgItems.add(new PkgItem(new MockAddonPackage("addon D",p2, 15), PkgState.NEW)); + m.mAllPkgItems.add(new PkgItem(new MockAddonPackage("addon D",p2, 14), PkgState.INSTALLED)); + + m.sortByApiLevel(); + + assertEquals( + "PkgApiCategory <API=TOOLS, label=Tools, #items=2>\n" + + "-- <INSTALLED, pkg:Android SDK Tools, revision 10>\n" + + "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3>\n" + + "PkgApiCategory <API=API 3, label=Android android-3 (API 3), #items=1>\n" + + "-- <INSTALLED, pkg:SDK Platform Android android-3, API 3, revision 6>\n" + + "PkgApiCategory <API=API 2, label=Android android-2 (API 2), #items=5>\n" + + "-- <NEW, pkg:SDK Platform Android android-2, API 2, revision 4>\n" + + "-- <NEW, pkg:addon B by vendor 2, Android API 2, revision 7, updated by:addon B by vendor 2, Android API 2, revision 9>\n" + + "-- <NEW, pkg:addon C by vendor 2, Android API 2, revision 9>\n" + + "-- <INSTALLED, pkg:addon D by vendor 2, Android API 2, revision 14>\n" + + "-- <NEW, pkg:addon D by vendor 2, Android API 2, revision 15>\n" + + "PkgApiCategory <API=API 1, label=Android android-1 (API 1), #items=2>\n" + + "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n" + + "-- <INSTALLED, pkg:addon A by vendor 1, Android API 1, revision 5, updated by:addon A by vendor 1, Android API 1, revision 6>\n" + + "PkgApiCategory <API=EXTRAS, label=Extras, #items=2>\n" + + "-- <INSTALLED, pkg:Android USB Driver package, revision 4, updated by:Android USB Driver package, revision 5>\n" + + "-- <NEW, pkg:Carrier Custom Rom package, revision 1>\n", + getTree(m)); + } + + // ---- + + public void testSortBySource_Empty() { + assertTrue(m.mAllPkgItems.isEmpty()); + m.sortBySource(); + assertSame(m.mCurrentCategories, m.mSourceCategories); + assertTrue(m.mApiCategories.isEmpty()); + } + + + // ---- + + /** + * Simulates the display we would have in the Packages Tree. + * This always depends on mCurrentCategories like the tree does. + * The display format is something like: + * <pre> + * PkgCategory <description> + * -- <PkgItem description> + * </pre> + */ + public String getTree(PackagesPageLogic l) { + StringBuilder sb = new StringBuilder(); + + for (PkgCategory cat : l.mCurrentCategories) { + sb.append(cat.toString()).append('\n'); + for (PkgItem item : cat.getItems()) { + sb.append("-- ").append(item.toString()).append('\n'); + } + } + + return sb.toString(); + } +} diff --git a/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/UpdaterDataTest.java b/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/UpdaterDataTest.java index 9470f91..303c24f 100755 --- a/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/UpdaterDataTest.java +++ b/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/UpdaterDataTest.java @@ -16,23 +16,10 @@ package com.android.sdkuilib.internal.repository; -import com.android.sdklib.SdkManager; import com.android.sdklib.internal.repository.Archive; -import com.android.sdklib.internal.repository.ArchiveInstaller; -import com.android.sdklib.internal.repository.ITask; -import com.android.sdklib.internal.repository.ITaskFactory; -import com.android.sdklib.internal.repository.ITaskMonitor; -import com.android.sdklib.internal.repository.MockEmptySdkManager; -import com.android.sdklib.internal.repository.Package; -import com.android.sdklib.internal.repository.Archive.Arch; -import com.android.sdklib.internal.repository.Archive.Os; -import com.android.sdklib.mock.MockLog; -import java.io.File; import java.util.ArrayList; import java.util.Arrays; -import java.util.List; -import java.util.Properties; import junit.framework.TestCase; @@ -104,164 +91,4 @@ public class UpdaterDataTest extends TestCase { assertEquals("[a1, a2]", Arrays.toString(m.getInstalled())); } - // --- - - - /** A mock UpdaterData that simply records what would have been installed. */ - private static class MockUpdaterData extends UpdaterData { - - private final List<Archive> mInstalled = new ArrayList<Archive>(); - - public MockUpdaterData() { - super("/tmp/SDK", new MockLog()); - - setTaskFactory(new MockTaskFactory()); - } - - /** Gives access to the internal {@link #installArchives(List)}. */ - public void _installArchives(List<ArchiveInfo> result) { - installArchives(result); - } - - public Archive[] getInstalled() { - return mInstalled.toArray(new Archive[mInstalled.size()]); - } - - @Override - protected void initSdk() { - setSdkManager(new MockEmptySdkManager("/tmp/SDK")); - } - - @Override - public void reloadSdk() { - // bypass original implementation - } - - /** Returns a mock installer that simply records what would have been installed. */ - @Override - protected ArchiveInstaller createArchiveInstaler() { - return new ArchiveInstaller() { - @Override - public boolean install( - Archive archive, - String osSdkRoot, - boolean forceHttp, - SdkManager sdkManager, - ITaskMonitor monitor) { - mInstalled.add(archive); - return true; - } - }; - } - } - - private static class MockTaskFactory implements ITaskFactory { - public void start(String title, ITask task) { - new MockTask(task); - } - } - - private static class MockTask implements ITaskMonitor { - public MockTask(ITask task) { - task.run(this); - } - - public ITaskMonitor createSubMonitor(int tickCount) { - return this; - } - - public boolean displayPrompt(String title, String message) { - return false; - } - - public int getProgress() { - return 0; - } - - public void incProgress(int delta) { - // ignore - } - - public boolean isCancelRequested() { - return false; - } - - public void setDescription(String format, Object... args) { - // ignore - } - - public void setProgressMax(int max) { - // ignore - } - - public void log(String format, Object... args) { - // ignore - } - - public void logError(String format, Object... args) { - // ignore - } - - public void logVerbose(String format, Object... args) { - // ignore - } - } - - private static class MockEmptyPackage extends Package { - private final String mTestHandle; - - public MockEmptyPackage(String testHandle) { - super( - null /*source*/, - null /*props*/, - 0 /*revision*/, - null /*license*/, - null /*description*/, - null /*descUrl*/, - Os.ANY /*archiveOs*/, - Arch.ANY /*archiveArch*/, - null /*archiveOsPath*/ - ); - mTestHandle = testHandle; - } - - @Override - protected Archive createLocalArchive( - Properties props, - Os archiveOs, - Arch archiveArch, - String archiveOsPath) { - return new Archive(this, props, archiveOs, archiveArch, archiveOsPath) { - @Override - public String toString() { - return mTestHandle; - } - }; - } - - public Archive getLocalArchive() { - return getArchives()[0]; - } - - @Override - public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) { - return null; - } - - @Override - public String getListDescription() { - return this.getClass().getSimpleName(); - } - - @Override - public String getShortDescription() { - return this.getClass().getSimpleName(); - } - - @Override - public boolean sameItemAs(Package pkg) { - return false; - } - - } } |