diff options
9 files changed, 307 insertions, 444 deletions
diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonPackage.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonPackage.java index 98c39be..385bd73 100755 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonPackage.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonPackage.java @@ -495,4 +495,22 @@ public class AddonPackage extends Package }
return true;
}
+
+ /**
+ * For addon packages, we want to add vendor|name to the sorting key
+ * <em>before<em/> the revision number.
+ * <p/>
+ * {@inheritDoc}
+ */
+ @Override
+ protected String comparisonKey() {
+ String s = super.comparisonKey();
+ int pos = s.indexOf("|r:"); //$NON-NLS-1$
+ assert pos > 0;
+ s = s.substring(0, pos) +
+ "|ve:" + getVendor() + //$NON-NLS-1$
+ "|na:" + getName() + //$NON-NLS-1$
+ s.substring(pos);
+ return s;
+ }
}
diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Package.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Package.java index b99da5c..01f713a 100755 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Package.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Package.java @@ -672,14 +672,13 @@ public abstract class Package implements IDescription, Comparable<Package> { // extras and everything else
sb.append(9);
}
- sb.append("|v:"); //$NON-NLS-1$
// We insert the package version here because it is more important
// than the revision number. We want package version to be sorted
// top-down, so we'll use 10k-api as the sorting key. The day we
// get reach 10k APIs, we'll need to revisit this.
-
+ sb.append("|v:"); //$NON-NLS-1$
if (this instanceof IPackageVersion) {
AndroidVersion v = ((IPackageVersion) this).getVersion();
@@ -688,14 +687,12 @@ public abstract class Package implements IDescription, Comparable<Package> { v.isPreview() ? 1 : 0
));
}
- sb.append("|r:"); //$NON-NLS-1$
-
// Append revision number
-
+ sb.append("|r:"); //$NON-NLS-1$
sb.append(String.format("%1$04d", getRevision())); //$NON-NLS-1$
- sb.append('|');
+ sb.append('|');
return sb.toString();
}
diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SystemImagePackage.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SystemImagePackage.java index 3e8363c..877a1d1 100755 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SystemImagePackage.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SystemImagePackage.java @@ -351,4 +351,21 @@ public class SystemImagePackage extends Package }
return true;
}
+
+ /**
+ * For sys img packages, we want to add abi to the sorting key
+ * <em>before<em/> the revision number.
+ * <p/>
+ * {@inheritDoc}
+ */
+ @Override
+ protected String comparisonKey() {
+ String s = super.comparisonKey();
+ int pos = s.indexOf("|r:"); //$NON-NLS-1$
+ assert pos > 0;
+ s = s.substring(0, pos) +
+ "|abi:" + getAbiDisplayName() + //$NON-NLS-1$
+ s.substring(pos);
+ return s;
+ }
}
diff --git a/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/LocalSdkParserTest.java b/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/LocalSdkParserTest.java index aa503cd..017d17c 100755 --- a/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/LocalSdkParserTest.java +++ b/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/LocalSdkParserTest.java @@ -70,8 +70,8 @@ public class LocalSdkParserTest extends SdkManagerTestCase { assertEquals( "[SDK Platform Android 0.0, API 0, revision 1, " + - "ARM EABI System Image, Android API 0, revision 0, " + "ARM EABI v7a System Image, Android API 0, revision 0, " + + "ARM EABI System Image, Android API 0, revision 0, " + "Sources for Android SDK, API 0, revision 0]", Arrays.toString(parser.parseSdk(sdkman.getLocation(), sdkman, monitor))); @@ -84,8 +84,8 @@ public class LocalSdkParserTest extends SdkManagerTestCase { assertEquals( "[SDK Platform Android 0.0, API 0, revision 1, " + - "ARM EABI System Image, Android API 0, revision 0, " + "ARM EABI v7a System Image, Android API 0, revision 0, " + + "ARM EABI System Image, Android API 0, revision 0, " + "Sources for Android SDK, API 0, revision 0, " + "Broken Intel x86 Atom System Image, API 0]", Arrays.toString(parser.parseSdk(sdkman.getLocation(), sdkman, monitor))); diff --git a/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/SdkRepoSourceTest.java b/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/SdkRepoSourceTest.java index 10330bc..564dfed 100755 --- a/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/SdkRepoSourceTest.java +++ b/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/SdkRepoSourceTest.java @@ -667,8 +667,8 @@ public class SdkRepoSourceTest extends TestCase { }
assertEquals(
"[42 armeabi, " +
- "2 x86, " +
- "2 armeabi-v7a]",
+ "2 armeabi-v7a, " +
+ "2 x86]",
Arrays.toString(sysImgVersionAbi.toArray()));
// Check the source packages
diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/sdkman2/PackagesDiffLogic.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/sdkman2/PackagesDiffLogic.java index 5b6a157..beef56f 100755 --- a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/sdkman2/PackagesDiffLogic.java +++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/sdkman2/PackagesDiffLogic.java @@ -26,21 +26,18 @@ import com.android.sdklib.internal.repository.PlatformToolPackage; import com.android.sdklib.internal.repository.SdkSource; import com.android.sdklib.internal.repository.SystemImagePackage; import com.android.sdklib.internal.repository.ToolPackage; -import com.android.sdklib.internal.repository.Package.UpdateInfo; -import com.android.sdklib.repository.SdkRepoConstants; import com.android.sdklib.util.SparseArray; import com.android.sdkuilib.internal.repository.UpdaterData; import com.android.sdkuilib.internal.repository.sdkman2.PkgItem.PkgState; -import java.net.URL; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -247,7 +244,10 @@ class PackagesDiffLogic { */ abstract class UpdateOp { private final Set<SdkSource> mVisitedSources = new HashSet<SdkSource>(); - protected final List<PkgCategory> mCategories = new ArrayList<PkgCategory>(); + private final List<PkgCategory> mCategories = new ArrayList<PkgCategory>(); + private final Set<PkgCategory> mCatsToRemove = new HashSet<PkgCategory>(); + private final Set<PkgItem> mItemsToRemove = new HashSet<PkgItem>(); + private final Map<Package, PkgItem> mUpdatesToRemove = new HashMap<Package, PkgItem>(); /** Removes all internal state. */ public void clear() { @@ -276,18 +276,23 @@ class PackagesDiffLogic { * items and/or adjust the category name. */ public abstract void postCategoryItemsChanged(); - /** Add the new package or merge it as an update or does nothing if this package - * is already part of the category items. - * Returns true if the category item list has changed. */ - public abstract boolean mergeNewPackage(Package newPackage, PkgCategory cat); - public void updateStart() { mVisitedSources.clear(); // Note that default categories are created after the unused ones so that // the callback can decide whether they should be marked as unused or not. + mCatsToRemove.clear(); + mItemsToRemove.clear(); + mUpdatesToRemove.clear(); for (PkgCategory cat : mCategories) { - cat.setUnused(true); + mCatsToRemove.add(cat); + List<PkgItem> items = cat.getItems(); + mItemsToRemove.addAll(items); + for (PkgItem item : items) { + if (item.hasUpdatePkg()) { + mUpdatesToRemove.put(item.getUpdatePkg(), item); + } + } } addDefaultCategories(); @@ -307,34 +312,57 @@ class PackagesDiffLogic { public boolean updateEnd() { boolean hasChanged = false; - // Remove unused categories + // Remove unused categories & items at the end of the update synchronized (mCategories) { - for (Iterator<PkgCategory> catIt = mCategories.iterator(); catIt.hasNext(); ) { - PkgCategory cat = catIt.next(); - if (cat.isUnused()) { - catIt.remove(); + for (PkgCategory unusedCat : mCatsToRemove) { + if (mCategories.remove(unusedCat)) { hasChanged = true; - continue; } + } + } - // Remove all *remote* items which obsolete source we have not been visited. - // This detects packages which have disappeared from a remote source during an - // update and removes from the current list. - // Locally installed item are never removed. - for (Iterator<PkgItem> itemIt = cat.getItems().iterator(); - itemIt.hasNext(); ) { - PkgItem item = itemIt.next(); - if (item.getState() == PkgState.NEW && - !mVisitedSources.contains(item.getSource())) { - itemIt.remove(); - hasChanged = true; - } + for (PkgCategory cat : mCategories) { + for (Iterator<PkgItem> itemIt = cat.getItems().iterator(); itemIt.hasNext(); ) { + PkgItem item = itemIt.next(); + if (mItemsToRemove.contains(item)) { + itemIt.remove(); + } else if (item.hasUpdatePkg() && + mUpdatesToRemove.containsKey(item.getUpdatePkg())) { + item.removeUpdate(); } } } + + mCatsToRemove.clear(); + mItemsToRemove.clear(); + mUpdatesToRemove.clear(); + return hasChanged; } + public boolean isKeep(PkgItem item) { + return !mItemsToRemove.contains(item); + } + + public void keep(Package pkg) { + mUpdatesToRemove.remove(pkg); + } + + public void keep(PkgItem item) { + mItemsToRemove.remove(item); + } + + public void keep(PkgCategory cat) { + mCatsToRemove.remove(cat); + } + + public void dontKeep(PkgItem item) { + mItemsToRemove.add(item); + } + + public void dontKeep(PkgCategory cat) { + mCatsToRemove.add(cat); + } } private final UpdateOpApi mOpApi = new UpdateOpApi(); @@ -389,80 +417,49 @@ class PackagesDiffLogic { return displayIsSortByApi ? apiListChanged : sourceListChanged; } + /** Process all local packages. Returns true if something changed. */ private boolean processLocals(UpdateOp op, Package[] packages) { boolean hasChanged = false; - Set<Package> newPackages = new HashSet<Package>(Arrays.asList(packages)); - Set<Package> unusedPackages = new HashSet<Package>(newPackages); - - assert newPackages.size() == packages.length; - - // Upgrade NEW items to INSTALLED for any local package we already know about. - // We can't just change the state of the NEW item to INSTALLED, we also need its - // installed package/archive information and so we swap them in-place in the items list. - - for (PkgCategory cat : op.getCategories()) { - List<PkgItem> items = cat.getItems(); - for (int i = 0; i < items.size(); i++) { - PkgItem item = items.get(i); - - if (item.hasUpdatePkg()) { - Package newPkg = setContainsLocalPackage(newPackages, item.getUpdatePkg()); - if (newPkg != null) { - // This item has an update package that is now installed. - PkgItem installed = new PkgItem(newPkg, PkgState.INSTALLED); - removePackageFromSet(unusedPackages, newPkg); - item.removeUpdate(); - items.add(installed); - cat.setUnused(false); - hasChanged = true; - } - } - - Package newPkg = setContainsLocalPackage(newPackages, item.getMainPackage()); - if (newPkg != null) { - removePackageFromSet(unusedPackages, newPkg); - cat.setUnused(false); - if (item.getState() == PkgState.NEW) { - // This item has a main package that is now installed. - replace(items, i, new PkgItem(newPkg, PkgState.INSTALLED)); - hasChanged = true; + List<PkgCategory> cats = op.getCategories(); + Set<PkgItem> keep = new HashSet<PkgItem>(); + + // For all locally installed packages, check they are either listed + // as installed or create new installed items for them. + + nextPkg: for (Package localPkg : packages) { + // Check to see if we already have the exact same package + // (type & revision) marked as installed. + for (PkgCategory cat : cats) { + for (PkgItem currItem : cat.getItems()) { + if (currItem.getState() == PkgState.INSTALLED && + currItem.isSameMainPackageAs(localPkg)) { + // This package is already listed as installed. + op.keep(currItem); + op.keep(cat); + keep.add(currItem); + continue nextPkg; } } } - } - // Remove INSTALLED items if their package isn't listed anymore in locals - for (PkgCategory cat : op.getCategories()) { - List<PkgItem> items = cat.getItems(); - for (int i = 0; i < items.size(); i++) { - PkgItem item = items.get(i); - - if (item.getState() == PkgState.INSTALLED) { - Package newPkg = setContainsLocalPackage(newPackages, item.getMainPackage()); - if (newPkg == null) { - items.remove(i--); - hasChanged = true; - } - } - } + // If not found, create a new installed package item + keep.add(addNewItem(op, localPkg, PkgState.INSTALLED)); + hasChanged = true; } - // Create new 'installed' items for any local package we haven't processed yet - for (Package newPackage : unusedPackages) { - Object catKey = op.getCategoryKey(newPackage); - PkgCategory cat = findCurrentCategory(op.getCategories(), catKey); + // Remove installed items that we don't want to keep anymore. They would normally be + // cleanup up in UpdateOp.updateEnd(); however it's easier to remove them before we + // run processSource() to avoid merging updates in items that would be removed later. - if (cat == null) { - // This is a new category. Create it and add it to the list. - cat = op.createCategory(catKey); - op.getCategories().add(cat); - op.sortCategoryList(); + for (PkgCategory cat : cats) { + for (Iterator<PkgItem> itemIt = cat.getItems().iterator(); itemIt.hasNext(); ) { + PkgItem item = itemIt.next(); + if (item.getState() == PkgState.INSTALLED && !keep.contains(item)) { + itemIt.remove(); + hasChanged = true; + } } - - cat.getItems().add(new PkgItem(newPackage, PkgState.INSTALLED)); - cat.setUnused(false); - hasChanged = true; } if (hasChanged) { @@ -472,175 +469,81 @@ class PackagesDiffLogic { return hasChanged; } - /** - * Replaces the item at {@code index} in {@code list} with the new {@code obj} element. - * This uses {@link ArrayList#set(int, Object)} if possible, remove+add otherwise. - * - * @return The old item at the same index position. - * @throws IndexOutOfBoundsException if index out of range (index < 0 || index >= size()). - */ - private <T> T replace(List<T> list, int index, T obj) { - if (list instanceof ArrayList<?>) { - return ((ArrayList<T>) list).set(index, obj); - } else { - T old = list.remove(index); - list.add(index, obj); - return old; - } - } - - /** - * Checks whether the {@code newPackages} set contains a package that is the - * same as {@code pkgToFind}. - * This is based on Package being the same from an install point of view rather than - * pure object equality. - * @return The matching package from the {@code newPackages} set or null if not found. - */ - private Package setContainsLocalPackage(Collection<Package> newPackages, Package pkgToFind) { - // Most of the time, local packages don't have the exact same hash code - // as new ones since the objects are similar but not exactly the same, - // for example their installed OS path cannot match (by definition) so - // their hash code do not match when used with Set.contains(). - - for (Package newPkg : newPackages) { - // Two packages are the same if they are compatible types, - // do not update each other and have the same revision number. - if (pkgToFind.canBeUpdatedBy(newPkg) == UpdateInfo.NOT_UPDATE && - newPkg.getRevision() == pkgToFind.getRevision()) { - return newPkg; - } - } - - return null; - } - - /** - * Removes the given package from the set. - * This is based on Package being the same from an install point of view rather than - * pure object equality. - */ - private void removePackageFromSet(Collection<Package> packages, Package pkgToFind) { - // First try to remove the package based on its hash code. This can fail - // for a variety of reasons, as explained in setContainsLocalPackage(). - if (packages.remove(pkgToFind)) { - return; - } - - for (Package pkg : packages) { - // Two packages are the same if they are compatible types, - // or not updates of each other and have the same revision number. - if (pkgToFind.canBeUpdatedBy(pkg) == UpdateInfo.NOT_UPDATE && - pkg.getRevision() == pkgToFind.getRevision()) { - packages.remove(pkg); - // Implementation detail: we can get away with using Collection.remove() - // whilst in the for iterator because we return right away (otherwise the - // iterator would complain the collection just changed.) - return; - } - } - } - - /** - * Removes any package from the set that is equal or lesser than {@code pkgToFind}. - * This is based on Package being the same from an install point of view rather than - * pure object equality. - * </p> - * This is a slight variation on {@link #removePackageFromSet(Collection, Package)} - * where we remove from the set any package that is similar to {@code pkgToFind} - * and has either the same revision number or a <em>lesser</em> revision number. - * An example of this use-case is there's an installed local package in rev 5 - * (that is the pkgToFind) and there's a remote package in rev 3 (in the package list), - * in which case we 'forget' the rev 3 package even exists. - */ - private void removePackageOrLesserFromSet(Collection<Package> packages, Package pkgToFind) { - for (Iterator<Package> it = packages.iterator(); it.hasNext(); ) { - Package pkg = it.next(); - - // Two packages are the same if they are compatible types, - // or not updates of each other and have the same revision number. - if (pkgToFind.canBeUpdatedBy(pkg) == UpdateInfo.NOT_UPDATE && - pkg.getRevision() <= pkgToFind.getRevision()) { - it.remove(); - } - } - } - /** Process all remote packages. Returns true if something changed. */ private boolean processSource(UpdateOp op, SdkSource source, Package[] packages) { boolean hasChanged = false; - // Note: unusedPackages must respect the original packages order. It can't be a set. - List<Package> unusedPackages = new ArrayList<Package>(Arrays.asList(packages)); - Set<Package> newPackages = new HashSet<Package>(unusedPackages); - - assert source != null; - assert newPackages.size() == packages.length; - - // Remove any items or updates that are no longer in the source's packages - for (PkgCategory cat : op.getCategories()) { - List<PkgItem> items = cat.getItems(); - for (int i = 0; i < items.size(); i++) { - PkgItem item = items.get(i); - - // Does the source provide this kind of package? - // FIXME. This is a crude workaround for bug 5508174; the - // diff logic has a larger issue, this is merely a quick fix. - // The downside is that if a remote source stops offering a given - // package type (e.g. a specific addon), it will still show up as - // available until the sdk manager is restarted. - boolean foundSame = false; - for (Package pkg : packages) { - if (pkg.sameItemAs(item.getMainPackage())) { - foundSame = true; - break; - } - } - if (!foundSame) { - continue; - } - - // Try to prune current items that are no longer on the remote site. - // Installed items have been dealt with the local source, so only - // change new items here. - if (item.getState() == PkgState.NEW) { - Package newPkg = setContainsLocalPackage(newPackages, item.getMainPackage()); - if (newPkg == null) { - // This package is no longer part of the source. - items.remove(i--); - hasChanged = true; - continue; - } - } + List<PkgCategory> cats = op.getCategories(); + + nextPkg: for (Package newPkg : packages) { + for (PkgCategory cat : cats) { + for (PkgState state : PkgState.values()) { + for (Iterator<PkgItem> currItemIt = cat.getItems().iterator(); + currItemIt.hasNext(); ) { + PkgItem currItem = currItemIt.next(); + // We need to merge with installed items first. When installing + // the diff will have both the new and the installed item and we + // need to merge with the installed one before the new one. + if (currItem.getState() != state) { + continue; + } + // Only process current items if they represent the same item (but + // with a different revision number) than the new package. + Package mainPkg = currItem.getMainPackage(); + if (!mainPkg.sameItemAs(newPkg)) { + continue; + } - cat.setUnused(false); - removePackageOrLesserFromSet(unusedPackages, item.getMainPackage()); + // Check to see if we already have the exact same package + // (type & revision) marked as main or update package. + if (currItem.isSameMainPackageAs(newPkg)) { + op.keep(currItem); + op.keep(cat); + continue nextPkg; + } else if (currItem.hasUpdatePkg() && + currItem.isSameUpdatePackageAs(newPkg)) { + op.keep(currItem.getUpdatePkg()); + op.keep(cat); + continue nextPkg; + } - if (item.hasUpdatePkg()) { - Package newPkg = setContainsLocalPackage(newPackages, item.getUpdatePkg()); - if (newPkg != null) { - removePackageFromSet(unusedPackages, newPkg); - } else { - // This update is no longer part of the source - item.removeUpdate(); - hasChanged = true; + switch (currItem.getState()) { + case NEW: + if (newPkg.getRevision() < mainPkg.getRevision()) { + if (!op.isKeep(currItem)) { + // The new item has a lower revision than the current one, + // but the current one hasn't been marked as being kept so + // it's ok to downgrade it. + currItemIt.remove(); + addNewItem(op, newPkg, PkgState.NEW); + hasChanged = true; + } + } else if (newPkg.getRevision() > mainPkg.getRevision()) { + // We have a more recent new version, remove the current one + // and replace by a new one + currItemIt.remove(); + addNewItem(op, newPkg, PkgState.NEW); + hasChanged = true; + } + break; + case INSTALLED: + // if newPkg.revision <= mainPkg.revision: it's already installed, ignore. + if (newPkg.getRevision() > mainPkg.getRevision()) { + // This is a new update for the main package. + if (currItem.mergeUpdate(newPkg)) { + op.keep(currItem.getUpdatePkg()); + op.keep(cat); + hasChanged = true; + } + } + break; + } + continue nextPkg; } } } - } - - // Add any new unknown packages - for (Package newPackage : unusedPackages) { - Object catKey = op.getCategoryKey(newPackage); - PkgCategory cat = findCurrentCategory(op.getCategories(), catKey); - - if (cat == null) { - // This is a new category. Create it and add it to the list. - cat = op.createCategory(catKey); - op.getCategories().add(cat); - op.sortCategoryList(); - } - - // Add the new package or merge it as an update - hasChanged |= op.mergeNewPackage(newPackage, cat); + // If not found, create a new package item + addNewItem(op, newPkg, PkgState.NEW); + hasChanged = true; } if (hasChanged) { @@ -650,75 +553,25 @@ class PackagesDiffLogic { return hasChanged; } - private boolean isSourceCompatible(PkgItem currentItem, Package newPackage) { - assert currentItem != null; - assert newPackage != null; - - // Don't compare source of packages which are not the same (their revision # can differ) - Package currentPkg = currentItem.getMainPackage(); - if (!currentPkg.sameItemAs(newPackage)) { - return false; - } - - SdkSource currentSource = currentItem.getSource(); - SdkSource newItemSource = newPackage.getParentSource(); - - // Only process items matching the current source. - if (currentSource == newItemSource) { - // Object identity, so definitely the same source. Accept it. - return true; - - } else if (currentSource != null && currentSource.equals(newItemSource)) { - // Same source. Accept it. - return true; - - } else if (currentSource != null && newItemSource != null && - !currentSource.getClass().equals(newItemSource.getClass())) { - // Both sources don't have the same type (e.g. sdk repository versus add-on repository) - return false; - - } else if (currentSource == null && currentItem.getState() == PkgState.INSTALLED) { - // Accept it. - // If a locally installed item has no source, it probably has been - // manually installed. In this case just match any remote source. - return true; - - } else if (currentSource != null && currentSource.getUrl().startsWith("file://")) { - // Heuristic: Probably a manual local install. Accept it. - return true; - } - - // Reject the source mismatch. The idea is that if two remote repositories - // have similar packages, we don't want to merge them together and have - // one hide the other. This is a design error from the repository owners - // and we want the case to be blatant so that we can get it fixed. - - if (currentSource != null && newItemSource != null) { - try { - String str1 = rewriteUrl(currentSource.getUrl()); - String str2 = rewriteUrl(newItemSource.getUrl()); - - URL url1 = new URL(str1); - URL url2 = new URL(str2); + private PkgItem addNewItem(UpdateOp op, Package pkg, PkgState state) { + List<PkgCategory> cats = op.getCategories(); + Object catKey = op.getCategoryKey(pkg); + PkgCategory cat = findCurrentCategory(cats, catKey); - // Make an exception if both URLs have the same host name & domain name. - if (url1.sameFile(url2) || url1.getHost().equals(url2.getHost())) { - return true; - } - } catch (Exception ignore) { - // Ignore MalformedURLException or other exceptions + if (cat == null) { + // This is a new category. Create it and add it to the list. + cat = op.createCategory(catKey); + synchronized (cats) { + cats.add(cat); } + op.sortCategoryList(); } - return false; - } - - private String rewriteUrl(String url) { - if (url != null && url.startsWith(SdkRepoConstants.URL_GOOGLE_SDK_SITE)) { - url = url.replaceAll("repository-[0-9]+\\.xml^", //$NON-NLS-1$ - "repository.xml"); //$NON-NLS-1$ - } - return url; + PkgItem item = new PkgItem(pkg, state); + op.keep(item); + cat.getItems().add(item); + op.keep(cat); + return item; } private PkgCategory findCurrentCategory( @@ -756,13 +609,14 @@ class PackagesDiffLogic { boolean needTools = true; boolean needExtras = true; - for (PkgCategory cat : mCategories) { + List<PkgCategory> cats = getCategories(); + for (PkgCategory cat : cats) { if (cat.getKey().equals(PkgCategoryApi.KEY_TOOLS)) { // Mark them as no unused to prevent their removal in updateEnd(). - cat.setUnused(false); + keep(cat); needTools = false; } else if (cat.getKey().equals(PkgCategoryApi.KEY_EXTRA)) { - cat.setUnused(false); + keep(cat); needExtras = false; } } @@ -773,8 +627,8 @@ class PackagesDiffLogic { PkgCategoryApi.KEY_TOOLS, null, mUpdaterData.getImageFactory().getImageByName(PackagesPage.ICON_CAT_OTHER)); - synchronized (mCategories) { - mCategories.add(acat); + synchronized (cats) { + cats.add(acat); } } @@ -783,8 +637,8 @@ class PackagesDiffLogic { PkgCategoryApi.KEY_EXTRA, null, mUpdaterData.getImageFactory().getImageByName(PackagesPage.ICON_CAT_OTHER)); - synchronized (mCategories) { - mCategories.add(acat); + synchronized (cats) { + cats.add(acat); } } } @@ -823,35 +677,6 @@ class PackagesDiffLogic { } @Override - public boolean mergeNewPackage(Package newPackage, PkgCategory cat) { - // First check if the new package could be an update - // to an existing package - for (PkgItem item : cat.getItems()) { - if (!isSourceCompatible(item, newPackage)) { - continue; - } - - if (item.isSameMainPackageAs(newPackage)) { - // Seems like this isn't really a new item after all. - cat.setUnused(false); - // Return false since we're not changing anything. - return false; - } else if (item.mergeUpdate(newPackage)) { - // The new package is an update for the existing package - // and has been merged in the PkgItem as such. - cat.setUnused(false); - // Return true to indicate we changed something. - return true; - } - } - - // This is truly a new item. - cat.getItems().add(new PkgItem(newPackage, PkgState.NEW)); - cat.setUnused(false); - return true; // something has changed - } - - @Override public void sortCategoryList() { // Sort the categories list. // We always want categories in order tools..platforms..extras. @@ -859,8 +684,8 @@ class PackagesDiffLogic { // This order is achieved by having the category keys ordered as // needed for the sort to just do what we expect. - synchronized (mCategories) { - Collections.sort(mCategories, new Comparator<PkgCategory>() { + synchronized (getCategories()) { + Collections.sort(getCategories(), new Comparator<PkgCategory>() { public int compare(PkgCategory cat1, PkgCategory cat2) { assert cat1 instanceof PkgCategoryApi; assert cat2 instanceof PkgCategoryApi; @@ -875,7 +700,7 @@ class PackagesDiffLogic { @Override public void postCategoryItemsChanged() { // Sort the items - for (PkgCategory cat : mCategories) { + for (PkgCategory cat : getCategories()) { Collections.sort(cat.getItems()); // When sorting by API, we can't always get the platform name @@ -922,7 +747,8 @@ class PackagesDiffLogic { @Override public void addDefaultCategories() { - for (PkgCategory cat : mCategories) { + List<PkgCategory> cats = getCategories(); + for (PkgCategory cat : cats) { if (cat.getKey().equals(PkgCategorySource.UNKNOWN_SOURCE)) { // Already present. return; @@ -933,10 +759,10 @@ class PackagesDiffLogic { PkgCategorySource cat = new PkgCategorySource( PkgCategorySource.UNKNOWN_SOURCE, mUpdaterData); - // Mark it as unused so that it can be cleared in updateEnd() if not used. - cat.setUnused(true); - synchronized (mCategories) { - mCategories.add(cat); + // Mark it so that it can be cleared in updateEnd() if not used. + dontKeep(cat); + synchronized (cats) { + cats.add(cat); } } @@ -949,37 +775,12 @@ class PackagesDiffLogic { } @Override - public boolean mergeNewPackage(Package newPackage, PkgCategory cat) { - // First check if the new package could be an update - // to an existing package - for (PkgItem item : cat.getItems()) { - if (item.isSameMainPackageAs(newPackage)) { - // Seems like this isn't really a new item after all. - cat.setUnused(false); - // Return false since we're not changing anything. - return false; - } else if (item.mergeUpdate(newPackage)) { - // The new package is an update for the existing package - // and has been merged in the PkgItem as such. - cat.setUnused(false); - // Return true to indicate we changed something. - return true; - } - } - - // This is truly a new item. - cat.getItems().add(new PkgItem(newPackage, PkgState.NEW)); - cat.setUnused(false); - return true; // something has changed - } - - @Override public void sortCategoryList() { // Sort the sources in ascending source name order, // with the local packages always first. - synchronized (mCategories) { - Collections.sort(mCategories, new Comparator<PkgCategory>() { + synchronized (getCategories()) { + Collections.sort(getCategories(), new Comparator<PkgCategory>() { public int compare(PkgCategory cat1, PkgCategory cat2) { assert cat1 instanceof PkgCategorySource; assert cat2 instanceof PkgCategorySource; @@ -1005,7 +806,7 @@ class PackagesDiffLogic { @Override public void postCategoryItemsChanged() { // Sort the items - for (PkgCategory cat : mCategories) { + for (PkgCategory cat : getCategories()) { Collections.sort(cat.getItems()); } } diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/sdkman2/PkgCategory.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/sdkman2/PkgCategory.java index 5bfd689..b682f08 100755 --- a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/sdkman2/PkgCategory.java +++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/sdkman2/PkgCategory.java @@ -20,12 +20,10 @@ import java.util.ArrayList; import java.util.List; abstract class PkgCategory { - private final Object mKey; - private final Object mIconRef; - private final List<PkgItem> mItems = new ArrayList<PkgItem>(); - private String mLabel; - /** Transient flag used during incremental updates. */ - private boolean mUnused; + private final Object mKey; + private final Object mIconRef; + private final List<PkgItem> mItems = new ArrayList<PkgItem>(); + private String mLabel; public PkgCategory(Object key, String label, Object iconRef) { mKey = key; @@ -53,14 +51,6 @@ abstract class PkgCategory { return mItems; } - public void setUnused(boolean unused) { - mUnused = unused; - } - - public boolean isUnused() { - return mUnused; - } - @Override public String toString() { return String.format("%s <key=%s, label=%s, #items=%d>", diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/sdkman2/PkgItem.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/sdkman2/PkgItem.java index d5eb0a3..0f9dbc3 100755 --- a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/sdkman2/PkgItem.java +++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/sdkman2/PkgItem.java @@ -40,6 +40,9 @@ public class PkgItem implements Comparable<PkgItem> { * a given remote package and the local repository. */ public enum PkgState { + // Implementation detail: order matters. Installed items must be dealt with before + // new items and the order of PkgState.values() matters. + /** * Package is locally installed and may or may not have an update. */ @@ -156,6 +159,18 @@ public class PkgItem implements Comparable<PkgItem> { } /** + * Checks whether the update packages are of the same type and are + * not an update of each other. + */ + public boolean isSameUpdatePackageAs(Package pkg) { + if (mUpdatePkg != null && mUpdatePkg.canBeUpdatedBy(pkg) == UpdateInfo.NOT_UPDATE) { + // package revision numbers must match + return mUpdatePkg.getRevision() == pkg.getRevision(); + } + return false; + } + + /** * 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. diff --git a/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/sdkman2/PackagesDiffLogicTest.java b/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/sdkman2/PackagesDiffLogicTest.java index c378fbb..c2b320d 100755 --- a/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/sdkman2/PackagesDiffLogicTest.java +++ b/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/sdkman2/PackagesDiffLogicTest.java @@ -379,6 +379,8 @@ public class PackagesDiffLogicTest extends TestCase { m.updateStart(); MockPlatformPackage p1; MockPlatformPackage p2; + @SuppressWarnings("unused") // keep p3 for clarity + MockPlatformPackage p3; assertTrue(m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] { new MockToolPackage(src1, 10, 3), @@ -386,8 +388,9 @@ public class PackagesDiffLogicTest extends TestCase { new MockExtraPackage(src1, "android", "usb_driver", 4, 3), // second update p1 = new MockPlatformPackage(src1, 1, 2, 3), // API 1 - new MockPlatformPackage(src1, 3, 6, 3), + p3 = new MockPlatformPackage(src1, 3, 6, 3), new MockAddonPackage(src2, "addon A", p1, 5), + new MockAddonPackage(src2, "addon D", p1, 10), })); assertTrue(m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] { new MockToolPackage(src1, 10, 3), @@ -400,10 +403,15 @@ public class PackagesDiffLogicTest extends TestCase { assertTrue(m.updateSourcePackages(true /*sortByApi*/, src2, new Package[] { new MockAddonPackage(src2, "addon C", p2, 9), new MockAddonPackage(src2, "addon A", p1, 6), + // the rev 7+8 will be ignored since there's a rev 9 coming after new MockAddonPackage(src2, "addon B", p2, 7), - // the rev 8 update will be ignored since there's a rev 9 coming after new MockAddonPackage(src2, "addon B", p2, 8), new MockAddonPackage(src2, "addon B", p2, 9), + // 11+12 should be ignored updates, 13 will update 10 + new MockAddonPackage(src2, "addon D", p1, 10), + new MockAddonPackage(src2, "addon D", p1, 12), // note: 12 listed before 11 + new MockAddonPackage(src2, "addon D", p1, 11), + new MockAddonPackage(src2, "addon D", p1, 13), })); assertFalse(m.updateEnd(true /*sortByApi*/)); @@ -415,11 +423,12 @@ public class PackagesDiffLogicTest extends TestCase { "-- <INSTALLED, pkg:SDK Platform Android android-3, API 3, revision 6>\n" + "PkgCategoryApi <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 B by vendor 2, Android API 2, revision 9>\n" + "-- <NEW, pkg:addon C by vendor 2, Android API 2, revision 9>\n" + - "PkgCategoryApi <API=API 1, label=Android android-1 (API 1), #items=2>\n" + + "PkgCategoryApi <API=API 1, label=Android android-1 (API 1), #items=3>\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" + + "-- <INSTALLED, pkg:addon D by vendor 1, Android API 1, revision 10, updated by:addon D by vendor 1, Android API 1, revision 13>\n" + "PkgCategoryApi <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", @@ -435,8 +444,9 @@ public class PackagesDiffLogicTest extends TestCase { new MockExtraPackage(src1, "android", "usb_driver", 4, 3), // second update p1 = new MockPlatformPackage(src1, 1, 2, 3), - new MockPlatformPackage(src1, 3, 6, 3), + p3 = new MockPlatformPackage(src1, 3, 6, 3), new MockAddonPackage(src2, "addon A", p1, 5), + new MockAddonPackage(src2, "addon D", p1, 10), })); assertFalse(m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] { new MockToolPackage(src1, 10, 3), @@ -449,12 +459,15 @@ public class PackagesDiffLogicTest extends TestCase { assertTrue(m.updateSourcePackages(true /*sortByApi*/, src2, new Package[] { new MockAddonPackage(src2, "addon C", p2, 9), new MockAddonPackage(src2, "addon A", p1, 6), + // the rev 7+8 will be ignored since there's a rev 9 coming after new MockAddonPackage(src2, "addon B", p2, 7), - // the rev 8 update will be ignored since there's a rev 9 coming after - // however as a side effect it makes the update method return true as it - // incorporated the update. new MockAddonPackage(src2, "addon B", p2, 8), new MockAddonPackage(src2, "addon B", p2, 9), + // 11+12 should be ignored updates, 13 will update 10 + new MockAddonPackage(src2, "addon D", p1, 10), + new MockAddonPackage(src2, "addon D", p1, 12), // note: 12 listed before 11 + new MockAddonPackage(src2, "addon D", p1, 11), + new MockAddonPackage(src2, "addon D", p1, 13), })); assertFalse(m.updateEnd(true /*sortByApi*/)); @@ -466,11 +479,12 @@ public class PackagesDiffLogicTest extends TestCase { "-- <INSTALLED, pkg:SDK Platform Android android-3, API 3, revision 6>\n" + "PkgCategoryApi <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 B by vendor 2, Android API 2, revision 9>\n" + "-- <NEW, pkg:addon C by vendor 2, Android API 2, revision 9>\n" + - "PkgCategoryApi <API=API 1, label=Android android-1 (API 1), #items=2>\n" + + "PkgCategoryApi <API=API 1, label=Android android-1 (API 1), #items=3>\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" + + "-- <INSTALLED, pkg:addon D by vendor 1, Android API 1, revision 10, updated by:addon D by vendor 1, Android API 1, revision 13>\n" + "PkgCategoryApi <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", @@ -708,6 +722,8 @@ public class PackagesDiffLogicTest extends TestCase { m.updateStart(); MockPlatformPackage p1; MockPlatformPackage p2; + @SuppressWarnings("unused") // keep p3 for clarity + MockPlatformPackage p3; assertTrue(m.updateSourcePackages(false /*sortByApi*/, null /*locals*/, new Package[] { new MockToolPackage(src1, 10, 3), @@ -715,8 +731,10 @@ public class PackagesDiffLogicTest extends TestCase { new MockExtraPackage(src1, "android", "usb_driver", 4, 3), // second update p1 = new MockPlatformPackage(src1, 1, 2, 3), // API 1 + p3 = new MockPlatformPackage(src1, 3, 6, 3), new MockPlatformPackage(src1, 3, 6, 3), // API 3 new MockAddonPackage(src2, "addon A", p1, 5), + new MockAddonPackage(src2, "addon D", p1, 10), })); assertTrue(m.updateSourcePackages(false /*sortByApi*/, src1, new Package[] { new MockToolPackage(src1, 10, 3), @@ -729,10 +747,15 @@ public class PackagesDiffLogicTest extends TestCase { assertTrue(m.updateSourcePackages(false /*sortByApi*/, src2, new Package[] { new MockAddonPackage(src2, "addon C", p2, 9), new MockAddonPackage(src2, "addon A", p1, 6), + // the rev 7+8 will be ignored since there's a rev 9 coming after new MockAddonPackage(src2, "addon B", p2, 7), - // the rev 8 update will be ignored since there's a rev 9 coming after new MockAddonPackage(src2, "addon B", p2, 8), new MockAddonPackage(src2, "addon B", p2, 9), + // 11+12 should be ignored updates, 13 will update 10 + new MockAddonPackage(src2, "addon D", p1, 10), + new MockAddonPackage(src2, "addon D", p1, 12), // note: 12 listed before 11 + new MockAddonPackage(src2, "addon D", p1, 11), + new MockAddonPackage(src2, "addon D", p1, 13), })); assertTrue(m.updateEnd(false /*sortByApi*/)); @@ -745,10 +768,11 @@ public class PackagesDiffLogicTest extends TestCase { "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 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" + - "PkgCategorySource <source=repo2 (2.example.com), #items=3>\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" + + "PkgCategorySource <source=repo2 (2.example.com), #items=4>\n" + + "-- <NEW, pkg: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 A by vendor 1, Android API 1, revision 5, updated by:addon A by vendor 1, Android API 1, revision 6>\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" + + "-- <INSTALLED, pkg:addon D by vendor 1, Android API 1, revision 10, updated by:addon D by vendor 1, Android API 1, revision 13>\n", getTree(m, false /*displaySortByApi*/)); // Reloading the same thing should have no impact except for the update methods @@ -760,9 +784,11 @@ public class PackagesDiffLogicTest extends TestCase { new MockPlatformToolPackage(src1, 3), new MockExtraPackage(src1, "android", "usb_driver", 4, 3), // second update - p1 = new MockPlatformPackage(src1, 1, 2, 3), - new MockPlatformPackage(src1, 3, 6, 3), + p1 = new MockPlatformPackage(src1, 1, 2, 3), // API 1 + p3 = new MockPlatformPackage(src1, 3, 6, 3), + new MockPlatformPackage(src1, 3, 6, 3), // API 3 new MockAddonPackage(src2, "addon A", p1, 5), + new MockAddonPackage(src2, "addon D", p1, 10), })); assertFalse(m.updateSourcePackages(false /*sortByApi*/, src1, new Package[] { new MockToolPackage(src1, 10, 3), @@ -775,12 +801,15 @@ public class PackagesDiffLogicTest extends TestCase { assertTrue(m.updateSourcePackages(false /*sortByApi*/, src2, new Package[] { new MockAddonPackage(src2, "addon C", p2, 9), new MockAddonPackage(src2, "addon A", p1, 6), + // the rev 7+8 will be ignored since there's a rev 9 coming after new MockAddonPackage(src2, "addon B", p2, 7), - // the rev 8 update will be ignored since there's a rev 9 coming after - // however as a side effect it makes the update method return true as it - // incorporated the update. new MockAddonPackage(src2, "addon B", p2, 8), new MockAddonPackage(src2, "addon B", p2, 9), + // 11+12 should be ignored updates, 13 will update 10 + new MockAddonPackage(src2, "addon D", p1, 10), + new MockAddonPackage(src2, "addon D", p1, 12), // note: 12 listed before 11 + new MockAddonPackage(src2, "addon D", p1, 11), + new MockAddonPackage(src2, "addon D", p1, 13), })); assertTrue(m.updateEnd(false /*sortByApi*/)); @@ -793,10 +822,11 @@ public class PackagesDiffLogicTest extends TestCase { "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 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" + - "PkgCategorySource <source=repo2 (2.example.com), #items=3>\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" + + "PkgCategorySource <source=repo2 (2.example.com), #items=4>\n" + + "-- <NEW, pkg: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 A by vendor 1, Android API 1, revision 5, updated by:addon A by vendor 1, Android API 1, revision 6>\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" + + "-- <INSTALLED, pkg:addon D by vendor 1, Android API 1, revision 10, updated by:addon D by vendor 1, Android API 1, revision 13>\n", getTree(m, false /*displaySortByApi*/)); } @@ -1387,13 +1417,8 @@ public class PackagesDiffLogicTest extends TestCase { getTree(m, true /*displaySortByApi*/)); assertEquals( "PkgCategorySource <source=Local Packages (no.source), #items=1>\n" + - // FIXME: tools rev 3 is installed in one source, and there's a rev 4 update - // it a different source. We should still mark the rev 3 here as having an update. - // We typically don't want to hide the fact the update comes from a different - // source but here it's OK since the local package has no source defined. - "-- <INSTALLED, pkg:Android SDK Tools, revision 3>\n" + // ERROR: missing update 4 - "PkgCategorySource <source=repo1 (1.example.com), #items=3>\n" + - "-- <NEW, pkg:Android SDK Tools, revision 4>\n" + + "-- <INSTALLED, pkg:Android SDK Tools, revision 3, updated by:Android SDK Tools, revision 4>\n" + + "PkgCategorySource <source=repo1 (1.example.com), #items=2>\n" + "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3, updated by:Android SDK Platform-tools, revision 4>\n" + "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n" + "PkgCategorySource <source=repo2 (2.example.com), #items=2>\n" + |