OSDN Git Service

SDK Manager2: revamp package diff logic.
authorRaphael Moll <ralf@android.com>
Thu, 21 Jul 2011 22:53:43 +0000 (15:53 -0700)
committerRaphael Moll <ralf@android.com>
Mon, 25 Jul 2011 21:34:37 +0000 (14:34 -0700)
This revamps the diff algorithm used to merge
the local and remote packages found during a
repository load into the PkgItems displayed in
the tree viewer.

FYI all the Package and Archive sub-classes are getting
an equals() and hashCode() methods to make them behave
correctly when put it in a HashTable or Set. These are
the auto-generated stuff from Eclipse.

Change-Id: I1494a0ed44cd768eed252e3a81b9e74bf86d563c

14 files changed:
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonPackage.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Archive.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/DocPackage.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ExtraPackage.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/LayoutlibVersionMixin.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/MinToolsPackage.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Package.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/PlatformPackage.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ToolPackage.java
sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/PackageLoader.java
sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/PackagesPage.java
sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/MockEmptyPackage.java
sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/PackagesDiffLogicTest.java [new file with mode: 0755]
sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/PackagesPageLogicTest.java [deleted file]

index af5c237..12645af 100755 (executable)
@@ -32,6 +32,7 @@ import org.w3c.dom.Node;
 \r
 import java.io.File;\r
 import java.util.ArrayList;\r
+import java.util.Arrays;\r
 import java.util.Map;\r
 import java.util.Properties;\r
 \r
@@ -70,6 +71,44 @@ public class AddonPackage extends Package
         public String getDescription() {\r
             return mDescription;\r
         }\r
+\r
+        @Override\r
+        public int hashCode() {\r
+            final int prime = 31;\r
+            int result = 1;\r
+            result = prime * result + ((mDescription == null) ? 0 : mDescription.hashCode());\r
+            result = prime * result + ((mName == null) ? 0 : mName.hashCode());\r
+            return result;\r
+        }\r
+\r
+        @Override\r
+        public boolean equals(Object obj) {\r
+            if (this == obj) {\r
+                return true;\r
+            }\r
+            if (obj == null) {\r
+                return false;\r
+            }\r
+            if (!(obj instanceof Lib)) {\r
+                return false;\r
+            }\r
+            Lib other = (Lib) obj;\r
+            if (mDescription == null) {\r
+                if (other.mDescription != null) {\r
+                    return false;\r
+                }\r
+            } else if (!mDescription.equals(other.mDescription)) {\r
+                return false;\r
+            }\r
+            if (mName == null) {\r
+                if (other.mName != null) {\r
+                    return false;\r
+                }\r
+            } else if (!mName.equals(other.mName)) {\r
+                return false;\r
+            }\r
+            return true;\r
+        }\r
     }\r
 \r
     private final Lib[] mLibs;\r
@@ -389,4 +428,62 @@ public class AddonPackage extends Package
 \r
         return false;\r
     }\r
+\r
+    @Override\r
+    public int hashCode() {\r
+        final int prime = 31;\r
+        int result = super.hashCode();\r
+        result = prime * result + ((mLayoutlibVersion == null) ? 0 : mLayoutlibVersion.hashCode());\r
+        result = prime * result + Arrays.hashCode(mLibs);\r
+        result = prime * result + ((mName == null) ? 0 : mName.hashCode());\r
+        result = prime * result + ((mVendor == null) ? 0 : mVendor.hashCode());\r
+        result = prime * result + ((mVersion == null) ? 0 : mVersion.hashCode());\r
+        return result;\r
+    }\r
+\r
+    @Override\r
+    public boolean equals(Object obj) {\r
+        if (this == obj) {\r
+            return true;\r
+        }\r
+        if (!super.equals(obj)) {\r
+            return false;\r
+        }\r
+        if (!(obj instanceof AddonPackage)) {\r
+            return false;\r
+        }\r
+        AddonPackage other = (AddonPackage) obj;\r
+        if (mLayoutlibVersion == null) {\r
+            if (other.mLayoutlibVersion != null) {\r
+                return false;\r
+            }\r
+        } else if (!mLayoutlibVersion.equals(other.mLayoutlibVersion)) {\r
+            return false;\r
+        }\r
+        if (!Arrays.equals(mLibs, other.mLibs)) {\r
+            return false;\r
+        }\r
+        if (mName == null) {\r
+            if (other.mName != null) {\r
+                return false;\r
+            }\r
+        } else if (!mName.equals(other.mName)) {\r
+            return false;\r
+        }\r
+        if (mVendor == null) {\r
+            if (other.mVendor != null) {\r
+                return false;\r
+            }\r
+        } else if (!mVendor.equals(other.mVendor)) {\r
+            return false;\r
+        }\r
+        if (mVersion == null) {\r
+            if (other.mVersion != null) {\r
+                return false;\r
+            }\r
+        } else if (!mVersion.equals(other.mVersion)) {\r
+            return false;\r
+        }\r
+        return true;\r
+    }\r
 }\r
index 4a21851..fb28a9d 100755 (executable)
@@ -384,4 +384,92 @@ public class Archive implements IDescription, Comparable<Archive> {
         }\r
         return 0;\r
     }\r
+\r
+    /**\r
+     * Note: An {@link Archive}'s hash code does NOT depend on the parent {@link Package} hash code.\r
+     * <p/>\r
+     * {@inheritDoc}\r
+     */\r
+    @Override\r
+    public int hashCode() {\r
+        final int prime = 31;\r
+        int result = 1;\r
+        result = prime * result + ((mArch == null) ? 0 : mArch.hashCode());\r
+        result = prime * result + ((mChecksum == null) ? 0 : mChecksum.hashCode());\r
+        result = prime * result + ((mChecksumType == null) ? 0 : mChecksumType.hashCode());\r
+        result = prime * result + (mIsLocal ? 1231 : 1237);\r
+        result = prime * result + ((mLocalOsPath == null) ? 0 : mLocalOsPath.hashCode());\r
+        result = prime * result + ((mOs == null) ? 0 : mOs.hashCode());\r
+        result = prime * result + (int) (mSize ^ (mSize >>> 32));\r
+        result = prime * result + ((mUrl == null) ? 0 : mUrl.hashCode());\r
+        return result;\r
+    }\r
+\r
+    /**\r
+     * Note: An {@link Archive}'s equality does NOT depend on the parent {@link Package} equality.\r
+     * <p/>\r
+     * {@inheritDoc}\r
+     */\r
+    @Override\r
+    public boolean equals(Object obj) {\r
+        if (this == obj) {\r
+            return true;\r
+        }\r
+        if (obj == null) {\r
+            return false;\r
+        }\r
+        if (!(obj instanceof Archive)) {\r
+            return false;\r
+        }\r
+        Archive other = (Archive) obj;\r
+        if (mArch == null) {\r
+            if (other.mArch != null) {\r
+                return false;\r
+            }\r
+        } else if (!mArch.equals(other.mArch)) {\r
+            return false;\r
+        }\r
+        if (mChecksum == null) {\r
+            if (other.mChecksum != null) {\r
+                return false;\r
+            }\r
+        } else if (!mChecksum.equals(other.mChecksum)) {\r
+            return false;\r
+        }\r
+        if (mChecksumType == null) {\r
+            if (other.mChecksumType != null) {\r
+                return false;\r
+            }\r
+        } else if (!mChecksumType.equals(other.mChecksumType)) {\r
+            return false;\r
+        }\r
+        if (mIsLocal != other.mIsLocal) {\r
+            return false;\r
+        }\r
+        if (mLocalOsPath == null) {\r
+            if (other.mLocalOsPath != null) {\r
+                return false;\r
+            }\r
+        } else if (!mLocalOsPath.equals(other.mLocalOsPath)) {\r
+            return false;\r
+        }\r
+        if (mOs == null) {\r
+            if (other.mOs != null) {\r
+                return false;\r
+            }\r
+        } else if (!mOs.equals(other.mOs)) {\r
+            return false;\r
+        }\r
+        if (mSize != other.mSize) {\r
+            return false;\r
+        }\r
+        if (mUrl == null) {\r
+            if (other.mUrl != null) {\r
+                return false;\r
+            }\r
+        } else if (!mUrl.equals(other.mUrl)) {\r
+            return false;\r
+        }\r
+        return true;\r
+    }\r
 }\r
index 5171454..b598f7d 100755 (executable)
@@ -253,4 +253,34 @@ public class DocPackage extends Package implements IPackageVersion {
         // not an upgrade but not incompatible either.\r
         return UpdateInfo.NOT_UPDATE;\r
     }\r
+\r
+    @Override\r
+    public int hashCode() {\r
+        final int prime = 31;\r
+        int result = super.hashCode();\r
+        result = prime * result + ((mVersion == null) ? 0 : mVersion.hashCode());\r
+        return result;\r
+    }\r
+\r
+    @Override\r
+    public boolean equals(Object obj) {\r
+        if (this == obj) {\r
+            return true;\r
+        }\r
+        if (!super.equals(obj)) {\r
+            return false;\r
+        }\r
+        if (!(obj instanceof DocPackage)) {\r
+            return false;\r
+        }\r
+        DocPackage other = (DocPackage) obj;\r
+        if (mVersion == null) {\r
+            if (other.mVersion != null) {\r
+                return false;\r
+            }\r
+        } else if (!mVersion.equals(other.mVersion)) {\r
+            return false;\r
+        }\r
+        return true;\r
+    }\r
 }\r
index ab074ce..ac8dd09 100755 (executable)
@@ -29,6 +29,7 @@ import org.w3c.dom.Node;
 \r
 import java.io.File;\r
 import java.util.ArrayList;\r
+import java.util.Arrays;\r
 import java.util.Map;\r
 import java.util.Properties;\r
 import java.util.regex.Pattern;\r
@@ -564,4 +565,50 @@ public class ExtraPackage extends MinToolsPackage
 \r
         return null;\r
     }\r
+\r
+    @Override\r
+    public int hashCode() {\r
+        final int prime = 31;\r
+        int result = super.hashCode();\r
+        result = prime * result + mMinApiLevel;\r
+        result = prime * result + ((mPath == null) ? 0 : mPath.hashCode());\r
+        result = prime * result + Arrays.hashCode(mProjectFiles);\r
+        result = prime * result + ((mVendor == null) ? 0 : mVendor.hashCode());\r
+        return result;\r
+    }\r
+\r
+    @Override\r
+    public boolean equals(Object obj) {\r
+        if (this == obj) {\r
+            return true;\r
+        }\r
+        if (!super.equals(obj)) {\r
+            return false;\r
+        }\r
+        if (!(obj instanceof ExtraPackage)) {\r
+            return false;\r
+        }\r
+        ExtraPackage other = (ExtraPackage) obj;\r
+        if (mMinApiLevel != other.mMinApiLevel) {\r
+            return false;\r
+        }\r
+        if (mPath == null) {\r
+            if (other.mPath != null) {\r
+                return false;\r
+            }\r
+        } else if (!mPath.equals(other.mPath)) {\r
+            return false;\r
+        }\r
+        if (!Arrays.equals(mProjectFiles, other.mProjectFiles)) {\r
+            return false;\r
+        }\r
+        if (mVendor == null) {\r
+            if (other.mVendor != null) {\r
+                return false;\r
+            }\r
+        } else if (!mVendor.equals(other.mVendor)) {\r
+            return false;\r
+        }\r
+        return true;\r
+    }\r
 }\r
index 88b778d..32d3ef3 100755 (executable)
@@ -97,4 +97,34 @@ public class LayoutlibVersionMixin implements ILayoutlibVersion {
     public Pair<Integer, Integer> getLayoutlibVersion() {\r
         return mLayoutlibVersion;\r
     }\r
+\r
+    @Override\r
+    public int hashCode() {\r
+        final int prime = 31;\r
+        int result = 1;\r
+        result = prime * result + ((mLayoutlibVersion == null) ? 0 : mLayoutlibVersion.hashCode());\r
+        return result;\r
+    }\r
+\r
+    @Override\r
+    public boolean equals(Object obj) {\r
+        if (this == obj) {\r
+            return true;\r
+        }\r
+        if (obj == null) {\r
+            return false;\r
+        }\r
+        if (!(obj instanceof LayoutlibVersionMixin)) {\r
+            return false;\r
+        }\r
+        LayoutlibVersionMixin other = (LayoutlibVersionMixin) obj;\r
+        if (mLayoutlibVersion == null) {\r
+            if (other.mLayoutlibVersion != null) {\r
+                return false;\r
+            }\r
+        } else if (!mLayoutlibVersion.equals(other.mLayoutlibVersion)) {\r
+            return false;\r
+        }\r
+        return true;\r
+    }\r
 }\r
index 661a73c..56f6247 100755 (executable)
@@ -98,4 +98,30 @@ public abstract class MinToolsPackage extends Package implements IMinToolsDepend
             props.setProperty(PROP_MIN_TOOLS_REV, Integer.toString(getMinToolsRevision()));\r
         }\r
     }\r
+\r
+    @Override\r
+    public int hashCode() {\r
+        final int prime = 31;\r
+        int result = super.hashCode();\r
+        result = prime * result + mMinToolsRevision;\r
+        return result;\r
+    }\r
+\r
+    @Override\r
+    public boolean equals(Object obj) {\r
+        if (this == obj) {\r
+            return true;\r
+        }\r
+        if (!super.equals(obj)) {\r
+            return false;\r
+        }\r
+        if (!(obj instanceof MinToolsPackage)) {\r
+            return false;\r
+        }\r
+        MinToolsPackage other = (MinToolsPackage) obj;\r
+        if (mMinToolsRevision != other.mMinToolsRevision) {\r
+            return false;\r
+        }\r
+        return true;\r
+    }\r
 }\r
index a9c5996..fe083d1 100755 (executable)
@@ -28,6 +28,7 @@ import org.w3c.dom.Node;
 \r
 import java.io.File;\r
 import java.util.ArrayList;\r
+import java.util.Arrays;\r
 import java.util.Map;\r
 import java.util.Properties;\r
 \r
@@ -450,6 +451,15 @@ public abstract class Package implements IDescription, Comparable<Package> {
     }\r
 \r
     /**\r
+     * A package is local (that is 'installed locally') if it contains a single\r
+     * archive that is local. If not local, it's a remote package, only available\r
+     * on a remote source for download and installation.\r
+     */\r
+    public boolean isLocal() {\r
+        return mArchives.length == 1 && mArchives[0].isLocal();\r
+    }\r
+\r
+    /**\r
      * Computes a potential installation folder if an archive of this package were\r
      * to be installed right away in the given SDK root.\r
      * <p/>\r
@@ -651,4 +661,49 @@ public abstract class Package implements IDescription, Comparable<Package> {
         return sb.toString();\r
     }\r
 \r
+    @Override\r
+    public int hashCode() {\r
+        final int prime = 31;\r
+        int result = 1;\r
+        result = prime * result + Arrays.hashCode(mArchives);\r
+        result = prime * result + ((mObsolete == null) ? 0 : mObsolete.hashCode());\r
+        result = prime * result + mRevision;\r
+        result = prime * result + ((mSource == null) ? 0 : mSource.hashCode());\r
+        return result;\r
+    }\r
+\r
+    @Override\r
+    public boolean equals(Object obj) {\r
+        if (this == obj) {\r
+            return true;\r
+        }\r
+        if (obj == null) {\r
+            return false;\r
+        }\r
+        if (!(obj instanceof Package)) {\r
+            return false;\r
+        }\r
+        Package other = (Package) obj;\r
+        if (!Arrays.equals(mArchives, other.mArchives)) {\r
+            return false;\r
+        }\r
+        if (mObsolete == null) {\r
+            if (other.mObsolete != null) {\r
+                return false;\r
+            }\r
+        } else if (!mObsolete.equals(other.mObsolete)) {\r
+            return false;\r
+        }\r
+        if (mRevision != other.mRevision) {\r
+            return false;\r
+        }\r
+        if (mSource == null) {\r
+            if (other.mSource != null) {\r
+                return false;\r
+            }\r
+        } else if (!mSource.equals(other.mSource)) {\r
+            return false;\r
+        }\r
+        return true;\r
+    }\r
 }\r
index 74b8cc7..6035ef5 100755 (executable)
@@ -261,4 +261,51 @@ public class PlatformPackage extends MinToolsPackage implements IPackageVersion,
 \r
         return false;\r
     }\r
+\r
+    @Override\r
+    public int hashCode() {\r
+        final int prime = 31;\r
+        int result = super.hashCode();\r
+        result = prime * result +\r
+                    ((mLayoutlibVersion == null) ? 0 : mLayoutlibVersion.hashCode());\r
+        result = prime * result + ((mVersion == null) ? 0 : mVersion.hashCode());\r
+        result = prime * result + ((mVersionName == null) ? 0 : mVersionName.hashCode());\r
+        return result;\r
+    }\r
+\r
+    @Override\r
+    public boolean equals(Object obj) {\r
+        if (this == obj) {\r
+            return true;\r
+        }\r
+        if (!super.equals(obj)) {\r
+            return false;\r
+        }\r
+        if (!(obj instanceof PlatformPackage)) {\r
+            return false;\r
+        }\r
+        PlatformPackage other = (PlatformPackage) obj;\r
+        if (mLayoutlibVersion == null) {\r
+            if (other.mLayoutlibVersion != null) {\r
+                return false;\r
+            }\r
+        } else if (!mLayoutlibVersion.equals(other.mLayoutlibVersion)) {\r
+            return false;\r
+        }\r
+        if (mVersion == null) {\r
+            if (other.mVersion != null) {\r
+                return false;\r
+            }\r
+        } else if (!mVersion.equals(other.mVersion)) {\r
+            return false;\r
+        }\r
+        if (mVersionName == null) {\r
+            if (other.mVersionName != null) {\r
+                return false;\r
+            }\r
+        } else if (!mVersionName.equals(other.mVersionName)) {\r
+            return false;\r
+        }\r
+        return true;\r
+    }\r
 }\r
index 69039ea..bac1253 100755 (executable)
@@ -352,4 +352,30 @@ public class ToolPackage extends Package implements IMinPlatformToolsDependency
         // get the return code from the process\r
         return process.waitFor();\r
     }\r
+\r
+    @Override\r
+    public int hashCode() {\r
+        final int prime = 31;\r
+        int result = super.hashCode();\r
+        result = prime * result + mMinPlatformToolsRevision;\r
+        return result;\r
+    }\r
+\r
+    @Override\r
+    public boolean equals(Object obj) {\r
+        if (this == obj) {\r
+            return true;\r
+        }\r
+        if (!super.equals(obj)) {\r
+            return false;\r
+        }\r
+        if (!(obj instanceof ToolPackage)) {\r
+            return false;\r
+        }\r
+        ToolPackage other = (ToolPackage) obj;\r
+        if (mMinPlatformToolsRevision != other.mMinPlatformToolsRevision) {\r
+            return false;\r
+        }\r
+        return true;\r
+    }\r
 }\r
index 2df670b..925f088 100755 (executable)
@@ -20,7 +20,6 @@ import com.android.sdklib.internal.repository.Archive;
 import com.android.sdklib.internal.repository.IPackageVersion;
 import com.android.sdklib.internal.repository.ITask;
 import com.android.sdklib.internal.repository.ITaskMonitor;
-import com.android.sdklib.internal.repository.LocalSdkParser;
 import com.android.sdklib.internal.repository.Package;
 import com.android.sdklib.internal.repository.SdkSource;
 import com.android.sdklib.internal.repository.Package.UpdateInfo;
@@ -44,10 +43,9 @@ class PackageLoader {
      * Interface for the callback called by
      * {@link PackageLoader#loadPackages(ISourceLoadedCallback)}.
      * <p/>
-     * After processing each source, the package loader calls {@link #onSourceLoaded(List)}
-     * with the list of package items found in that source. The client should process that
-     * list as it want, typically by accumulating the package items in a list of its own.
-     * By returning true from {@link #onSourceLoaded(List)}, the client tells the loader to
+     * After processing each source, the package loader calls {@link #onUpdateSource}
+     * with the list of packages found in that source.
+     * By returning true from {@link #onUpdateSource}, the client tells the loader to
      * continue and process the next source. By returning false, it tells to stop loading.
      * <p/>
      * The {@link #onLoadCompleted()} method is guaranteed to be called at the end, no
@@ -57,20 +55,19 @@ class PackageLoader {
     public interface ISourceLoadedCallback {
         /**
          * After processing each source, the package loader calls this method with the
-         * list of package items found in that source. The client should process that
-         * list as it want, typically by accumulating the package items in a list of its own.
-         * By returning true from {@link #onSourceLoaded(List)}, the client tells the loader to
-         * continue and process the next source. By returning false, it tells to stop loading.
+         * list of packages found in that source.
+         * By returning true from {@link #onUpdateSource}, the client tells
+         * the loader to continue and process the next source.
+         * By returning false, it tells to stop loading.
          * <p/>
-         * <em>Important</em>: This method is called from a sub-thread, so clients who try
-         * to access any UI widgets must wrap their calls into {@link Display#syncExec(Runnable)}
-         * or {@link Display#asyncExec(Runnable)}.
+         * <em>Important</em>: This method is called from a sub-thread, so clients which
+         * try to access any UI widgets must wrap their calls into
+         * {@link Display#syncExec(Runnable)} or {@link Display#asyncExec(Runnable)}.
          *
-         * @param pkgItems All the package items loaded from the last processed source.
-         *  This is a copy and the client can hold to this list or modify it in any way.
+         * @param packages All the packages loaded from the source. Never null.
          * @return True if the load operation should continue, false if it should stop.
          */
-        public boolean onSourceLoaded(List<PkgItem> pkgItems);
+        public boolean onUpdateSource(SdkSource source, Package[] packages);
 
         /**
          * This method is guaranteed to be called at the end, no matter how the
@@ -141,14 +138,15 @@ class PackageLoader {
             }
 
             // get local packages and offer them to the callback
-            List<PkgItem> localPkgItems =  loadLocalPackages();
-            if (!localPkgItems.isEmpty()) {
-                if (!sourceLoadedCallback.onSourceLoaded(localPkgItems)) {
-                    return;
-                }
+            Package[] localPkgs = mUpdaterData.getInstalledPackages();
+            if (localPkgs == null) {
+                localPkgs = new Package[0];
+            }
+            if (!sourceLoadedCallback.onUpdateSource(null, localPkgs)) {
+                return;
             }
 
-            final int[] numPackages = { localPkgItems.size() };
+            final int[] numPackages = { localPkgs == null ? 0 : localPkgs.length };
 
             // get remote packages
             final boolean forceHttp = mUpdaterData.getSettingsController().getForceHttp();
@@ -167,17 +165,11 @@ class PackageLoader {
                                 continue;
                             }
 
-                            List<PkgItem> sourcePkgItems = new ArrayList<PkgItem>();
-                            for(Package pkg : pkgs) {
-                                PkgItem pi = new PkgItem(pkg, PkgState.NEW);
-                                sourcePkgItems.add(pi);
-                            }
-
-                            numPackages[0] += sourcePkgItems.size();
+                            numPackages[0] += pkgs.length;
 
                             // Notify the callback a new source has finished loading.
                             // If the callback requests so, stop right away.
-                            if (!sourceLoadedCallback.onSourceLoaded(sourcePkgItems)) {
+                            if (!sourceLoadedCallback.onUpdateSource(source, pkgs)) {
                                 return;
                             }
                         }
@@ -196,26 +188,6 @@ class PackageLoader {
     }
 
     /**
-     * Internal method that returns all installed packages from the {@link LocalSdkParser}
-     * associated with the {@link UpdaterData}.
-     * <p/>
-     * Note that the {@link LocalSdkParser} maintains a cache, so callers need to clear
-     * it if they know they changed the local installation.
-     *
-     * @return A new list of {@link PkgItem}. May be empty but never null.
-     */
-    private List<PkgItem> loadLocalPackages() {
-        List<PkgItem> pkgItems = new ArrayList<PkgItem>();
-
-        for (Package pkg : mUpdaterData.getInstalledPackages()) {
-            PkgItem pi = new PkgItem(pkg, PkgState.INSTALLED);
-            pkgItems.add(pi);
-        }
-
-        return pkgItems;
-    }
-
-    /**
      * Load packages, source by source using {@link #loadPackages(ISourceLoadedCallback)},
      * and executes the given {@link IAutoInstallTask} on the current package list.
      * That is for each package known, the install task is queried to find if
@@ -245,59 +217,44 @@ class PackageLoader {
     public void loadPackagesWithInstallTask(final IAutoInstallTask installTask) {
 
         loadPackages(new ISourceLoadedCallback() {
-            public boolean onSourceLoaded(List<PkgItem> pkgItems) {
-                for (PkgItem item : pkgItems) {
-                    Package acceptedPkg = null;
-                    switch(item.getState()) {
-                    case NEW:
-                        if (installTask.acceptPackage(item.getMainPackage())) {
-                            acceptedPkg = item.getMainPackage();
-                        }
-                        if (item.hasUpdatePkg() && installTask.acceptPackage(item.getUpdatePkg())) {
-                            acceptedPkg = item.getUpdatePkg();
-                        }
-                        break;
-                    case INSTALLED:
-                        if (installTask.acceptPackage(item.getMainPackage())) {
+            public boolean onUpdateSource(SdkSource source, Package[] packages) {
+                for (Package pkg : packages) {
+                    if (pkg.isLocal()) {
+                        // This is a local (aka installed) package
+                        if (installTask.acceptPackage(pkg)) {
                             // If the caller is accepting an installed package,
                             // return a success and give the package's install path
-                            acceptedPkg = item.getMainPackage();
-                            Archive[] a = acceptedPkg.getArchives();
+                            Archive[] a = pkg.getArchives();
                             // an installed package should have one local compatible archive
                             if (a.length == 1 && a[0].isCompatible()) {
                                 installTask.setResult(
-                                        acceptedPkg,
+                                        pkg,
                                         true /*success*/,
                                         new File(a[0].getLocalOsPath()));
-
-                                // return false to tell loadPackages() that we don't
-                                // need to continue processing any more sources.
-                                return false;
                             }
+                            // return false to tell loadPackages() that we don't
+                            // need to continue processing any more sources.
+                            return false;
                         }
-                    }
 
-                    if (acceptedPkg != null) {
-                        // Try to install this package if it has one compatible archive.
-                        Archive archiveToInstall = null;
+                    } else {
+                        // This is a remote package
+                        if (installTask.acceptPackage(pkg)) {
+                            // The caller is accepting this remote package. Let's try to install it.
 
-                        for (Archive a2 : acceptedPkg.getArchives()) {
-                            if (a2.isCompatible()) {
-                                archiveToInstall = a2;
-                                break;
+                            for (Archive archive : pkg.getArchives()) {
+                                if (archive.isCompatible()) {
+                                    installArchive(archive);
+                                    break;
+                                }
                             }
+                            // return false to tell loadPackages() that we don't
+                            // need to continue processing any more sources.
+                            return false;
                         }
-
-                        if (archiveToInstall != null) {
-                            installArchive(archiveToInstall);
-                        }
-
-                        // return false to tell loadPackages() that we don't
-                        // need to continue processing any more sources.
-                        return false;
                     }
-
                 }
+
                 // Tell loadPackages() to process the next source.
                 return true;
             }
@@ -342,11 +299,10 @@ class PackageLoader {
 
                     // The local package list has changed, make sure to refresh it
                     mUpdaterData.getLocalSdkParser().clearPackages();
-                    final List<PkgItem> localPkgItems = loadLocalPackages();
+                    final Package[] localPkgs = mUpdaterData.getInstalledPackages();
 
                     // Try to locate the installed package in the new package list
-                    for (PkgItem localItem : localPkgItems) {
-                        Package localPkg = localItem.getMainPackage();
+                    for (Package localPkg : localPkgs) {
                         if (localPkg.canBeUpdatedBy(packageToInstall) == UpdateInfo.NOT_UPDATE) {
                             Archive[] localArchive = localPkg.getArchives();
                             if (localArchive.length == 1 && localArchive[0].isCompatible()) {
@@ -419,7 +375,7 @@ class PackageLoader {
         }
 
         public boolean hasUpdatePkg() {
-            return mState == PkgState.INSTALLED && mUpdatePkg != null;
+            return mUpdatePkg != null;
         }
 
         public String getName() {
index 36dcd49..fd5f3c9 100755 (executable)
@@ -21,16 +21,19 @@ import com.android.annotations.VisibleForTesting.Visibility;
 import com.android.sdklib.IAndroidTarget;
 import com.android.sdklib.internal.repository.Archive;
 import com.android.sdklib.internal.repository.IDescription;
+import com.android.sdklib.internal.repository.IPackageVersion;
 import com.android.sdklib.internal.repository.ITask;
 import com.android.sdklib.internal.repository.ITaskMonitor;
 import com.android.sdklib.internal.repository.Package;
 import com.android.sdklib.internal.repository.PlatformPackage;
 import com.android.sdklib.internal.repository.PlatformToolPackage;
+import com.android.sdklib.internal.repository.SdkRepoSource;
 import com.android.sdklib.internal.repository.SdkSource;
 import com.android.sdklib.internal.repository.ToolPackage;
 import com.android.sdkuilib.internal.repository.PackageLoader.ISourceLoadedCallback;
 import com.android.sdkuilib.internal.repository.PackageLoader.PkgItem;
 import com.android.sdkuilib.internal.repository.PackageLoader.PkgState;
+import com.android.sdkuilib.internal.repository.PackagesPage.PackagesDiffLogic.UpdateOp;
 import com.android.sdkuilib.internal.repository.icons.ImageFactory;
 import com.android.sdkuilib.repository.ISdkChangeListener;
 import com.android.sdkuilib.ui.GridDataBuilder;
@@ -68,6 +71,7 @@ import org.eclipse.swt.widgets.TreeColumn;
 
 import java.io.File;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
@@ -125,7 +129,7 @@ public class PackagesPage extends UpdaterPage
 
     private final Map<MenuAction, MenuItem> mMenuActions = new HashMap<MenuAction, MenuItem>();
 
-    private final PackagesPageLogic mLogic;
+    private final PackagesDiffLogic mDiffLogic;
     private boolean mDisplayArchives = false;
 
     private Text mTextSdkOsPath;
@@ -152,19 +156,14 @@ public class PackagesPage extends UpdaterPage
     public PackagesPage(Composite parent, int swtStyle, UpdaterData updaterData) {
         super(parent, swtStyle);
 
-        mLogic = new PackagesPageLogic(updaterData) {
-            @Override
-            boolean keepItem(PkgItem item) {
-                return PackagesPage.this.keepItem(item);
-            }
-        };
+        mDiffLogic = new PackagesDiffLogic(updaterData);
 
         createContents(this);
         postCreate();  //$hide$
     }
 
     public void onPageSelected() {
-        if (mLogic.mAllPkgItems.isEmpty()) {
+        if (mDiffLogic.mCurrentCategories == null || mDiffLogic.mCurrentCategories.isEmpty()) {
             // Initialize the package list the first time the page is shown.
             loadPackages();
         }
@@ -202,7 +201,7 @@ public class PackagesPage extends UpdaterPage
         mTree.setHeaderVisible(true);
         GridDataBuilder.create(mTree).fill().grab();
 
-        // column name icon is set in sortPackages() depending on the current filter type
+        // column name icon is set when loading depending on the current filter type
         // (e.g. API level or source)
         mColumnName = new TreeViewerColumn(mTreeViewer, SWT.NONE);
         mTreeColumnName = mColumnName.getColumn();
@@ -244,7 +243,7 @@ public class PackagesPage extends UpdaterPage
         mCheckFilterNew.addSelectionListener(new SelectionAdapter() {
             @Override
             public void widgetSelected(SelectionEvent e) {
-                sortPackages(true /*updateButtons*/);
+                loadPackages();
             }
         });
         mCheckFilterNew.setSelection(true);
@@ -254,7 +253,7 @@ public class PackagesPage extends UpdaterPage
         mCheckFilterInstalled.addSelectionListener(new SelectionAdapter() {
             @Override
             public void widgetSelected(SelectionEvent e) {
-                sortPackages(true /*updateButtons*/);
+                loadPackages();
             }
         });
         mCheckFilterInstalled.setSelection(true);
@@ -266,7 +265,7 @@ public class PackagesPage extends UpdaterPage
         mCheckFilterObsolete.addSelectionListener(new SelectionAdapter() {
             @Override
             public void widgetSelected(SelectionEvent e) {
-                sortPackages(true /*updateButtons*/);
+                loadPackages();
             }
         });
         mCheckFilterObsolete.setSelection(false);
@@ -304,9 +303,9 @@ public class PackagesPage extends UpdaterPage
         mCheckSortApi.addSelectionListener(new SelectionAdapter() {
             @Override
             public void widgetSelected(SelectionEvent e) {
-                sortPackages(true /*updateButtons*/);
+                loadPackages();
                 // Reset the expanded state when changing sort algorithm
-                expandInitial(mLogic.mCurrentCategories);
+                expandInitial(mDiffLogic.mCurrentCategories);
             }
         });
         mCheckSortApi.setText("API level");
@@ -318,9 +317,9 @@ public class PackagesPage extends UpdaterPage
         mCheckSortSource.addSelectionListener(new SelectionAdapter() {
             @Override
             public void widgetSelected(SelectionEvent e) {
-                sortPackages(true /*updateButtons*/);
+                loadPackages();
                 // Reset the expanded state when changing sort algorithm
-                expandInitial(mLogic.mCurrentCategories);
+                expandInitial(mDiffLogic.mCurrentCategories);
             }
         });
 
@@ -351,8 +350,8 @@ public class PackagesPage extends UpdaterPage
     }
 
     private Image getImage(String filename) {
-        if (mLogic.mUpdaterData != null) {
-            ImageFactory imgFactory = mLogic.mUpdaterData.getImageFactory();
+        if (mDiffLogic.mUpdaterData != null) {
+            ImageFactory imgFactory = mDiffLogic.mUpdaterData.getImageFactory();
             if (imgFactory != null) {
                 return imgFactory.getImageByName(filename);
             }
@@ -379,14 +378,14 @@ public class PackagesPage extends UpdaterPage
                     loadPackages();
                     break;
                 case SHOW_ADDON_SITES:
-                    AddonSitesDialog d = new AddonSitesDialog(getShell(), mLogic.mUpdaterData);
+                    AddonSitesDialog d = new AddonSitesDialog(getShell(), mDiffLogic.mUpdaterData);
                     if (d.open()) {
                         loadPackages();
                     }
                     break;
                 case TOGGLE_SHOW_ARCHIVES:
                     mDisplayArchives = !mDisplayArchives;
-                    sortPackages(true /*updateButtons*/);
+                    loadPackages();
                     break;
                 case TOGGLE_SHOW_INSTALLED_PKG:
                     button = mCheckFilterInstalled;
@@ -484,8 +483,8 @@ public class PackagesPage extends UpdaterPage
     }
 
     private void postCreate() {
-        if (mLogic.mUpdaterData != null) {
-            mTextSdkOsPath.setText(mLogic.mUpdaterData.getOsSdkRoot());
+        if (mDiffLogic.mUpdaterData != null) {
+            mTextSdkOsPath.setText(mDiffLogic.mUpdaterData.getOsSdkRoot());
         }
 
         mTreeViewer.setContentProvider(new PkgContentProvider());
@@ -512,55 +511,25 @@ public class PackagesPage extends UpdaterPage
     }
 
     private void loadPackages() {
-        if (mLogic.mUpdaterData == null) {
+        if (mDiffLogic.mUpdaterData == null) {
             return;
         }
 
-        final boolean firstLoad = mLogic.mAllPkgItems.isEmpty();
-
         // 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.
 
-        mLogic.mPackageLoader.loadPackages(new ISourceLoadedCallback() {
-            public boolean onSourceLoaded(List<PkgItem> newPkgItems) {
-                boolean somethingNew = false;
-
-                synchronized(mLogic.mAllPkgItems) {
-                    nextNewItem: for (PkgItem newItem : newPkgItems) {
-                        for (PkgItem existingItem : mLogic.mAllPkgItems) {
-                            if (existingItem.isSameItemAs(newItem)) {
-                                // This isn't a new package, we already have it.
-                                continue nextNewItem;
-                            }
-                        }
-                        mLogic.mAllPkgItems.add(newItem);
-                        somethingNew = true;
-                    }
-                }
+        final boolean useSortByApi = isSortByApi();
+        final UpdateOp op = mDiffLogic.updateStart(useSortByApi);
+        mDiffLogic.mPackageLoader.loadPackages(new ISourceLoadedCallback() {
+            boolean needsRefresh = mDiffLogic.isSortByApi() == useSortByApi;
 
-                if (somethingNew) {
-                    // Dynamically update the table while we load after each source.
-                    // Since the official Android source gets loaded first, it makes the
-                    // window look non-empty a lot sooner.
-                    if (!mGroupPackages.isDisposed()) {
-                        mGroupPackages.getDisplay().syncExec(new Runnable() {
-                            public void run() {
-                                sortPackages(true /* updateButtons */);
-
-                                if (!mGroupPackages.isDisposed()) {
-                                    if (firstLoad) {
-                                        // set the initial expanded state
-                                        expandInitial(mLogic.mCurrentCategories);
-                                    }
-                                    updateButtonsState();
-                                    updateMenuCheckmarks();
-                                }
-                            }
-                        });
-                    }
+            public boolean onUpdateSource(SdkSource source, Package[] newPackages) {
+                if (mDiffLogic.updateSourcePackages(op, source, newPackages) || needsRefresh) {
+                    refreshViewerSync();
+                    needsRefresh = false;
                 }
 
                 // Return true to tell the loader to continue with the next source.
@@ -570,73 +539,45 @@ public class PackagesPage extends UpdaterPage
             }
 
             public void onLoadCompleted() {
-                if (firstLoad && !mGroupPackages.isDisposed()) {
-                    updateButtonsState();
-                    updateMenuCheckmarks();
+                if (mDiffLogic.updateEnd(op) || needsRefresh) {
+                    refreshViewerSync();
+                    needsRefresh = false;
                 }
             }
         });
     }
 
-    private void sortPackages(boolean updateButtons) {
-        if (isSortByApi()) {
-            sortByApiLevel();
-        } else {
-            sortBySource();
-        }
-        if (updateButtons) {
-            updateButtonsState();
-            updateMenuCheckmarks();
-        }
-    }
-
-    private boolean isSortByApi() {
-        return mCheckSortApi != null && !mCheckSortApi.isDisposed() && mCheckSortApi.getSelection();
-    }
-
-    /**
-     * 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.
-     */
-    private void sortByApiLevel() {
+    private void refreshViewerSync() {
+        // Dynamically update the table while we load after each source.
+        // Since the official Android source gets loaded first, it makes the
+        // window look non-empty a lot sooner.
+        if (!mGroupPackages.isDisposed()) {
+            mGroupPackages.getDisplay().syncExec(new Runnable() {
+                public void run() {
+                    if (!mGroupPackages.isDisposed()) {
 
-        if (!mTreeColumnName.isDisposed()) {
-            mTreeColumnName.setImage(getImage(ICON_SORT_BY_API));
-        }
+                        if (mTreeViewer.getInput() != mDiffLogic.mCurrentCategories) {
+                            // set initial input
+                            mTreeViewer.setInput(mDiffLogic.mCurrentCategories);
+                        } else {
+                            // refresh existing, which preserves the expanded state, the selection
+                            // and the checked state.
+                            mTreeViewer.refresh();
+                        }
 
-        mLogic.sortByApiLevel();
+                        // set the initial expanded state
+                        expandInitial(mDiffLogic.mCurrentCategories);
 
-        if (mTreeViewer.getInput() != mLogic.mCurrentCategories) {
-            // set initial input
-            mTreeViewer.setInput(mLogic.mCurrentCategories);
-        } else {
-            // refresh existing, which preserves the expanded state, the selection
-            // and the checked state.
-            mTreeViewer.refresh();
+                        updateButtonsState();
+                        updateMenuCheckmarks();
+                    }
+                }
+            });
         }
     }
 
-    /**
-     * Recompute the tree by sorting all packages by source.
-     */
-    private void sortBySource() {
-
-        if (!mTreeColumnName.isDisposed()) {
-            mTreeColumnName.setImage(getImage(ICON_SORT_BY_SOURCE));
-        }
-
-        mLogic.sortBySource();
-
-        // We don't support in-place incremental updates so the table gets reset
-        // each time we load when sorted by source.
-        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();
-        }
+    private boolean isSortByApi() {
+        return mCheckSortApi != null && !mCheckSortApi.isDisposed() && mCheckSortApi.getSelection();
     }
 
     /**
@@ -820,8 +761,8 @@ public class PackagesPage extends UpdaterPage
 
     private void onSelectNewUpdates() {
         ITreeContentProvider provider = (ITreeContentProvider) mTreeViewer.getContentProvider();
-        synchronized(mLogic.mAllPkgItems) {
-            for (PkgCategory cat : mLogic.mCurrentCategories) {
+        synchronized(mDiffLogic.mCurrentCategories) {
+            for (PkgCategory cat : mDiffLogic.mCurrentCategories) {
                 boolean selected = false;
                 for (PkgItem item : cat.getItems()) {
                     if (item.getState() == PkgState.NEW || item.hasUpdatePkg()) {
@@ -895,34 +836,18 @@ public class PackagesPage extends UpdaterPage
             }
         }
 
-        if (mLogic.mUpdaterData != null) {
+        if (mDiffLogic.mUpdaterData != null) {
             try {
                 beginOperationPending();
 
-                mLogic.mUpdaterData.updateOrInstallAll_WithGUI(
+                mDiffLogic.mUpdaterData.updateOrInstallAll_WithGUI(
                     archives,
                     mCheckFilterObsolete.getSelection() /* includeObsoletes */);
             } finally {
                 endOperationPending();
 
-                // 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(mLogic.mAllPkgItems) {
-                    for (Archive a : archives) {
-                        for (Iterator<PkgItem> it = mLogic.mAllPkgItems.iterator();
-                                 it.hasNext(); ) {
-                            PkgItem pi = it.next();
-                            if (pi.hasArchive(a)) {
-                                it.remove();
-                                break;
-                            }
-                        }
-                    }
-                }
-
                 // The local package list has changed, make sure to refresh it
-                mLogic.mUpdaterData.getLocalSdkParser().clearPackages();
+                mDiffLogic.mUpdaterData.getLocalSdkParser().clearPackages();
                 loadPackages();
             }
         }
@@ -970,7 +895,7 @@ public class PackagesPage extends UpdaterPage
                 try {
                     beginOperationPending();
 
-                    mLogic.mUpdaterData.getTaskFactory().start("Delete Package", new ITask() {
+                    mDiffLogic.mUpdaterData.getTaskFactory().start("Delete Package", new ITask() {
                         public void run(ITaskMonitor monitor) {
                             monitor.setProgressMax(archives.size() + 1);
                             for (Entry<Archive, PkgItem> entry : archives.entrySet()) {
@@ -980,13 +905,9 @@ public class PackagesPage extends UpdaterPage
                                         a.getParentPackage().getShortDescription(),
                                         a.getLocalOsPath());
 
-                                // Delete the actual package and its internal representation
+                                // Delete the actual package
                                 a.deleteLocal();
 
-                                synchronized(mLogic.mAllPkgItems) {
-                                    mLogic.mAllPkgItems.remove(entry.getValue());
-                                }
-
                                 monitor.incProgress(1);
                                 if (monitor.isCancelRequested()) {
                                     break;
@@ -1001,7 +922,7 @@ public class PackagesPage extends UpdaterPage
                     endOperationPending();
 
                     // The local package list has changed, make sure to refresh it
-                    mLogic.mUpdaterData.getLocalSdkParser().clearPackages();
+                    mDiffLogic.mUpdaterData.getLocalSdkParser().clearPackages();
                     loadPackages();
                 }
             }
@@ -1027,7 +948,7 @@ public class PackagesPage extends UpdaterPage
                 if (element instanceof PkgCategory) {
                     return ((PkgCategory) element).getLabel();
                 } else if (element instanceof PkgItem) {
-                    return getPkgItemname((PkgItem) element);
+                    return getPkgItemName((PkgItem) element);
                 } else if (element instanceof IDescription) {
                     return ((IDescription) element).getShortDescription();
                 }
@@ -1081,13 +1002,14 @@ public class PackagesPage extends UpdaterPage
             return "";
         }
 
-        private String getPkgItemname(PkgItem item) {
+        private String getPkgItemName(PkgItem item) {
             String name = item.getName().trim();
 
             if (isSortByApi()) {
                 // When sorting by API, the package name might contains the API number
                 // or the platform name at the end. If we find it, cut it out since it's
                 // redundant.
+                // TODO deal with obsolete packages
 
                 PkgApiCategory cat = (PkgApiCategory) findCategoryForItem(item);
                 String apiLabel = cat.getApiLabel();
@@ -1105,7 +1027,7 @@ public class PackagesPage extends UpdaterPage
         }
 
         private PkgCategory findCategoryForItem(PkgItem item) {
-            for (PkgCategory cat : mLogic.mCurrentCategories) {
+            for (PkgCategory cat : mDiffLogic.mCurrentCategories) {
                 for (PkgItem i : cat.getItems()) {
                     if (i == item) {
                         return cat;
@@ -1118,7 +1040,7 @@ public class PackagesPage extends UpdaterPage
 
         @Override
         public Image getImage(Object element) {
-            ImageFactory imgFactory = mLogic.mUpdaterData.getImageFactory();
+            ImageFactory imgFactory = mDiffLogic.mUpdaterData.getImageFactory();
 
             if (imgFactory != null) {
                 if (mColumn == mColumnName) {
@@ -1248,6 +1170,8 @@ public class PackagesPage extends UpdaterPage
         private final Object mIconRef;
         private final List<PkgItem> mItems = new ArrayList<PkgItem>();
         private String mLabel;
+        /** Transient flag used during incremental updates. */
+        private boolean mUnused;
 
         public PkgCategory(Object key, String label, Object iconRef) {
             mKey = key;
@@ -1275,6 +1199,14 @@ public class PackagesPage extends UpdaterPage
             return mItems;
         }
 
+        public void setUnused(boolean unused) {
+            mUnused = unused;
+        }
+
+        public boolean isUnused() {
+            return mUnused;
+        }
+
         @Override
         public String toString() {
             return String.format("%s <key=%08x, label=%s, #items=%d>",
@@ -1389,9 +1321,10 @@ public class PackagesPage extends UpdaterPage
 
         /**
          * A special {@link SdkSource} object that represents the locally installed
-         * items, or more exactly a lack of remote source. Value is {@code null}.
+         * items, or more exactly a lack of remote source.
          */
-        public final static SdkSource UNKNOWN_SOURCE = null;
+        public final static SdkSource UNKNOWN_SOURCE =
+            new SdkRepoSource("http://no.source", "Local Packages");
         private final SdkSource mSource;
 
         public PkgSourceCategory(SdkSource source, UpdaterData updaterData) {
@@ -1408,7 +1341,7 @@ public class PackagesPage extends UpdaterPage
         public String toString() {
             return String.format("%s <source=%s, #items=%d>",
                     this.getClass().getSimpleName(),
-                    mSource == UNKNOWN_SOURCE ? "Local" : mSource.toString(),
+                    mSource.toString(),
                     getItems().size());
         }
 
@@ -1437,477 +1370,516 @@ public class PackagesPage extends UpdaterPage
 
 
     /**
-     * Helper class that separate the logic of package management from the UI
+     * Helper class that separates the logic of package management from the UI
      * so that we can test it using head-less unit tests.
      */
-    static abstract class PackagesPageLogic {
+    static class PackagesDiffLogic {
         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) {
+        public PackagesDiffLogic(UpdaterData updaterData) {
             mUpdaterData = updaterData;
             mPackageLoader = new PackageLoader(updaterData);
         }
 
         /**
-         * Private interface used by {@link PackagesPageLogic#sort(ISortOperation)}.
-         * The sort() method only focuses on the incremental update part of the sort.
-         * The operation interface tells what to sort, how to extract keys from items
-         * to bucket them in categeories, how to create categories, what categories are
-         * by default and finally how to sort these categories.
+         * An update operation, customized to either sort by API or sort by source.
          */
-        private interface ISortOperation {
-            /** The list of categories to update in-place. */
-            List<PkgCategory> getCategories();
-
-            /** Create all the default categories (e.g. local installed packages, tools. etc.) */
-            void addDefaultCategories(
-                    List<PkgCategory> currentCategories,
-                    Map<Object, PkgCategory> categoryKeyMap,
-                    Set<Object> unusedCategoryKey,
-                    ImageFactory imgFactory);
-
-            /** Extracts the category key from a given item. */
-            public Object getCategoryKey(PkgItem item);
-
-            /** Creates a new category object for the given key. */
-            PkgCategory createCategory(Object catKey, ImageFactory imgFactory);
-
-            /**
-             * Process a new item and merge it into the existing categories,
-             * return true if the item was already in the category items list.
-             *
-             * @return True if the {@code newItem} was found in the category's item list.
-             *   When returning true, the method should remove the package(s) from
-             *   {@code unusedPackages}.
-             *   If the method return false, the caller will add {@code newItem} to the
-             *   category's items list.
-             */
-            boolean mergeNewItem(
-                    PkgItem newItem,
-                    PkgCategory cat,
-                    List<PkgCategory> cats,
-                    Set<Package> unusedPackages);
-
-            /**
-             * Post process items/categories after an item from mAllPkgItems as been filtered.
-             * Used by the API sort to see if we can infer the category name from a platform
-             * package when we don't have this info in the package manager.
-             */
-            void postProcessItem(Object catKey, PkgCategory category, PkgItem item);
-
-            /** Final step, sort the list of categories. */
-            void sortCategoryList(List<PkgCategory> categoryList);
+        abstract class UpdateOp {
+            public final Set<SdkSource> mVisitedSources = new HashSet<SdkSource>();
+
+            /** Retrieve the category key for the given package, either local or remote. */
+            public abstract Object getCategoryKey(Package pkg);
+            /** Modified {@code currentCategories} to add default categories. */
+            public abstract void addDefaultCategories(List<PkgCategory> currentCategories);
+            /** Creates the category for the given key and returns it. */
+            public abstract PkgCategory createCategory(Object catKey);
+            /** Sorts the category list (but not the items within the categories.) */
+            public abstract void sortCategoryList(List<PkgCategory> categoryList);
+            /** Called after items of a given category have changed. Used to sort the
+             * items and/or adjust the category name. */
+            public abstract void postCategoryItemsChanged(List<PkgCategory> categoryList);
+            /** 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 boolean isSortByApi() {
+            return mCurrentCategories == mApiCategories;
+        }
+
+        public UpdateOp updateStart(boolean sortByApi) {
+            mCurrentCategories = sortByApi ? mApiCategories : mSourceCategories;
+
+            UpdateOp info = sortByApi ? (new UpdateOpApi()) : (new UpdateOpSource());
+
+            // 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.
+            for (PkgCategory cat : mCurrentCategories) {
+                cat.setUnused(true);
+            }
+
+            info.addDefaultCategories(mCurrentCategories);
+
+            return info;
         }
 
-        /**
-         * Recompute the tree by sorting all the {@link PkgItem}s into the category buckets.
-         * This does an update in-place of the mCurrentCategories list so that the table
-         * can preserve its state (checked / expanded / selected) properly.
-         * <p/>
-         * Since this is shared between both the per-API and the per-source sort, care must
-         * be taken to either not change the displayed PkgItem or make the change compatible
-         * with both displays. Otherwise it looks odd when the displayed items change
-         * when changing the sorting mode in the UI.
-         * FIXME currently there's an issue about that wrt items that have available updates.
-         *
-         * @param op The actual details of the sort, which allows us to
-         *   reuse the same method for both sorting by API or by SdkSource.
-         * @see ISortOperation
-         */
-        private void sort(ISortOperation op) {
-            ImageFactory imgFactory = mUpdaterData.getImageFactory();
-
-            List<PkgCategory> cats = mCurrentCategories = op.getCategories();
-
-            // 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<Object, PkgCategory> categoryKeyMap = new HashMap<Object, PkgCategory>();
-            final Set<Object> unusedCategoryKey = new HashSet<Object>();
-            final Set<Package> unusedPackages =   new HashSet<Package>();
-
-            // Get existing categories and packages
-            for (PkgCategory cat : cats) {
-                categoryKeyMap.put(cat.getKey(), cat);
-                unusedCategoryKey.add(cat.getKey());
-
-                for (PkgItem pi : cat.getItems()) {
-                    unusedPackages.add(pi.getMainPackage());
-                    if (pi.hasUpdatePkg()) {
-                        unusedPackages.add(pi.getUpdatePkg());
+        public boolean updateSourcePackages(UpdateOp op, SdkSource source, Package[] newPackages) {
+            if (newPackages.length > 0) {
+                op.mVisitedSources.add(source);
+            }
+            if (source == null) {
+                return processLocals(op, newPackages);
+            } else {
+                return processSource(op, source, newPackages);
+            }
+        }
+
+        public boolean updateEnd(UpdateOp op) {
+            boolean hasChanged = false;
+
+            // Remove unused categories
+            for (Iterator<PkgCategory> catIt = mCurrentCategories.iterator(); catIt.hasNext(); ) {
+                PkgCategory cat = catIt.next();
+                if (cat.isUnused()) {
+                    catIt.remove();
+                    hasChanged  = true;
+                    continue;
+                }
+
+                // Remove all items which source we have not been visited. They are obsolete.
+                for (Iterator<PkgItem> itemIt = cat.getItems().iterator(); itemIt.hasNext(); ) {
+                    PkgItem item = itemIt.next();
+                    if (!op.mVisitedSources.contains(item.getSource())) {
+                        itemIt.remove();
+                        hasChanged  = true;
                     }
                 }
             }
+            return hasChanged;
+        }
 
-            op.addDefaultCategories(cats, categoryKeyMap, unusedCategoryKey, imgFactory);
+        /** Process all local packages. Returns true if something changed.
+         * @param op */
+        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);
 
-            // 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;
-                    }
+            assert newPackages.size() == packages.length;
 
-                    Object catKey = op.getCategoryKey(newItem);
-                    PkgCategory cat = categoryKeyMap.get(catKey);
+            // Upgrade 'new' items to 'installed' for any local package we already know about
+            for (PkgCategory cat : mCurrentCategories) {
+                List<PkgItem> items = cat.getItems();
+                for (int i = 0; i < items.size(); i++) {
+                    PkgItem item = items.get(i);
 
-                    if (cat == null) {
-                        // This is a new category. Create it and add it to the list.
-                        cat = op.createCategory(catKey, imgFactory);
-                        // It should not matter where we add to the list since we'll sort
-                        // the categories at the end.
-                        cats.add(cat);
-                        categoryKeyMap.put(cat.getKey(), cat);
-                    } else {
-                        // Remove the category key from the unused list.
-                        unusedCategoryKey.remove(catKey);
+                    if (item.hasUpdatePkg() && newPackages.contains(item.getUpdatePkg())) {
+                        // This item has an update package that is now installed.
+                        PkgItem installed = new PkgItem(item.getUpdatePkg(), PkgState.INSTALLED);
+                        unusedPackages.remove(item.getUpdatePkg());
+                        item.removeUpdate();
+                        items.add(installed);
+                        cat.setUnused(false);
+                        hasChanged = true;
                     }
 
-                    // Check whether the item is already present or merge it if it's an update
-                    boolean found = op.mergeNewItem(newItem, cat, cats, unusedPackages);
-
-                    if (!found) {
-                        cat.getItems().add(newItem);
-                        unusedPackages.remove(newItem.getMainPackage());
-                        unusedPackages.remove(newItem.getUpdatePkg());
+                    if (newPackages.contains(item.getMainPackage())) {
+                        unusedPackages.remove(item.getMainPackage());
+                        if (item.getState() == PkgState.NEW) {
+                            // This item has a main package that is now installed.
+                            item.setState(PkgState.INSTALLED);
+                            cat.setUnused(false);
+                            hasChanged = true;
+                        }
                     }
+                }
+            }
 
-                    op.postProcessItem(catKey, cat, newItem);
+            // Downgrade 'installed' items to 'new' if their package isn't listed anymore
+            for (PkgCategory cat : mCurrentCategories) {
+                for (PkgItem item : cat.getItems()) {
+                    if (item.getState() == PkgState.INSTALLED &&
+                            !newPackages.contains(item.getMainPackage())) {
+                        item.setState(PkgState.NEW);
+                        hasChanged = true;
+                    }
                 }
             }
 
-            // Now go through all the remaining categories used for the tree and clear unused items.
-            for (Iterator<PkgCategory> iterCat = cats.iterator(); iterCat.hasNext(); ) {
-                PkgCategory cat = iterCat.next();
+            // 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(mCurrentCategories, catKey);
 
-                // Remove any unused categories.
-                if (unusedCategoryKey.contains(cat.getKey())) {
-                    iterCat.remove();
-                    continue;
+                if (cat == null) {
+                    // This is a new category. Create it and add it to the list.
+                    cat = op.createCategory(catKey);
+                    mCurrentCategories.add(cat);
+                    op.sortCategoryList(mCurrentCategories);
                 }
 
-                // Remove any unused items in the category.
-                for (Iterator<PkgItem> iterItem = cat.getItems().iterator(); iterItem.hasNext(); ) {
-                    PkgItem item = iterItem.next();
+                cat.getItems().add(new PkgItem(newPackage, PkgState.INSTALLED));
+                cat.setUnused(false);
+                hasChanged = true;
+            }
 
-                    if (unusedPackages.contains(item.getMainPackage())) {
-                        iterItem.remove();
-                    } else if (item.hasUpdatePkg() &&
-                            unusedPackages.contains(item.getUpdatePkg())) {
-                        item.removeUpdate();
+            if (hasChanged) {
+                op.postCategoryItemsChanged(mCurrentCategories);
+            }
+
+            return hasChanged;
+        }
+
+        /** Process all remote packages. Returns true if something changed.
+         * @param op */
+        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 newPackages.size() == packages.length;
+
+            // Remove any items or updates that are no longer in the source's packages
+            for (PkgCategory cat : mCurrentCategories) {
+                List<PkgItem> items = cat.getItems();
+                for (int i = 0; i < items.size(); i++) {
+                    PkgItem item = items.get(i);
+                    SdkSource itemSource = item.getSource();
+
+                    // Only process items matching the current source
+                    if (!(itemSource == source || (source != null && source.equals(itemSource)))) {
+                        continue;
+                    }
+                    // Installed items have been dealt with the local source,
+                    // so only change new items here
+                    if (item.getState() == PkgState.NEW &&
+                            !newPackages.contains(item.getMainPackage())) {
+                        // This package is no longer part of the source.
+                        items.remove(i--);
+                        hasChanged = true;
+                        continue;
+                    }
+
+                    cat.setUnused(false);
+                    unusedPackages.remove(item.getMainPackage());
+
+                    if (item.hasUpdatePkg()) {
+                        if (newPackages.contains(item.getUpdatePkg())) {
+                            unusedPackages.remove(item.getUpdatePkg());
+                        } else {
+                            // This update is no longer part of the source
+                            item.removeUpdate();
+                            hasChanged = true;
+                        }
                     }
                 }
+            }
 
-                // Sort the items
-                Collections.sort(cat.getItems());
+            // Add any new unknown packages
+            for (Package newPackage : unusedPackages) {
+                Object catKey = op.getCategoryKey(newPackage);
+                PkgCategory cat = findCurrentCategory(mCurrentCategories, catKey);
+
+                if (cat == null) {
+                    // This is a new category. Create it and add it to the list.
+                    cat = op.createCategory(catKey);
+                    mCurrentCategories.add(cat);
+                    op.sortCategoryList(mCurrentCategories);
+                }
+
+                // Add the new package or merge it as an update
+                hasChanged |= op.mergeNewPackage(newPackage, cat);
             }
 
-            op.sortCategoryList(cats);
+            if (hasChanged) {
+                op.postCategoryItemsChanged(mCurrentCategories);
+            }
+
+            return hasChanged;
         }
 
+        private PkgCategory findCurrentCategory(
+                List<PkgCategory> currentCategories,
+                Object categoryKey) {
+            for (PkgCategory cat : currentCategories) {
+                if (cat.getKey().equals(categoryKey)) {
+                    return cat;
+                }
+            }
+            return null;
+        }
 
         /**
-         * Recompute the tree by sorting all the packages by API.
+         * {@link UpdateOp} describing the Sort-by-API operation.
          */
-        void sortByApiLevel() {
-            sort(new ISortOperation() {
-                public List<PkgCategory> getCategories() {
-                    return mApiCategories;
-                }
+        private class UpdateOpApi extends UpdateOp {
+            @Override
+            public Object getCategoryKey(Package pkg) {
+                // Sort by API
 
-                public void addDefaultCategories(
-                        List<PkgCategory> currentCategories,
-                        Map<Object, PkgCategory> categoryKeyMap,
-                        Set<Object> unusedCategoryKey,
-                        ImageFactory imgFactory) {
-                    // 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));
-                        currentCategories.add(acat);
-                        categoryKeyMap.put(acat.getKey(), acat);
-                        unusedCategoryKey.add(acat.getKey());
-                    }
+                if (pkg instanceof IPackageVersion) {
+                    return ((IPackageVersion) pkg).getVersion().getApiLevel();
+
+                } else if (pkg instanceof ToolPackage || pkg instanceof PlatformToolPackage) {
+                    return PkgApiCategory.KEY_TOOLS;
 
-                    if (!unusedCategoryKey.contains(PkgApiCategory.KEY_EXTRA)) {
-                        PkgApiCategory acat = new PkgApiCategory(
-                                PkgApiCategory.KEY_EXTRA,
-                                null,
-                                imgFactory.getImageByName(ICON_CAT_OTHER));
-                        currentCategories.add(acat);
-                        categoryKeyMap.put(acat.getKey(), acat);
-                        unusedCategoryKey.add(acat.getKey());
+                } else {
+                    return PkgApiCategory.KEY_EXTRA;
+                }
+            }
+
+            @Override
+            public void addDefaultCategories(List<PkgCategory> currentCategories) {
+                boolean needTools = true;
+                boolean needExtras = true;
+
+                for (PkgCategory cat : currentCategories) {
+                    if (cat.getKey().equals(PkgApiCategory.KEY_TOOLS)) {
+                        // Mark them as no unused to prevent their removal in updateEnd().
+                        cat.setUnused(false);
+                        needTools = false;
+                    } else if (cat.getKey().equals(PkgApiCategory.KEY_EXTRA)) {
+                        cat.setUnused(false);
+                        needExtras = false;
                     }
                 }
 
-                public Object getCategoryKey(PkgItem item) {
-                    // Get the category for this item.
-                    int apiKey = item.getApi();
+                // Always add the tools & extras categories, even if empty (unlikely anyway)
+                if (needTools) {
+                    PkgApiCategory acat = new PkgApiCategory(
+                            PkgApiCategory.KEY_TOOLS,
+                            null,
+                            mUpdaterData.getImageFactory().getImageByName(ICON_CAT_OTHER));
+                    currentCategories.add(acat);
+                }
 
-                    if (apiKey < 1) {
-                        Package p = item.getMainPackage();
-                        if (p instanceof ToolPackage || p instanceof PlatformToolPackage) {
-                            apiKey = PkgApiCategory.KEY_TOOLS;
-                        } else {
-                            apiKey = PkgApiCategory.KEY_EXTRA;
-                        }
-                    }
-                    return apiKey;
+                if (needExtras) {
+                    PkgApiCategory acat = new PkgApiCategory(
+                            PkgApiCategory.KEY_EXTRA,
+                            null,
+                            mUpdaterData.getImageFactory().getImageByName(ICON_CAT_OTHER));
+                    currentCategories.add(acat);
                 }
+            }
 
-                public PkgCategory createCategory(
-                        Object catKey,
-                        ImageFactory imgFactory) {
-
-                    PkgCategory cat = null;
-
-                    assert catKey instanceof Integer;
-                    int apiKey = ((Integer) catKey).intValue();
-
-                    // 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 && apiKey != PkgApiCategory.KEY_TOOLS) {
-                        for (IAndroidTarget target :
-                                mUpdaterData.getSdkManager().getTargets()) {
-                            if (target.isPlatform() &&
-                                    target.getVersion().getApiLevel() == apiKey) {
-                                platformName = target.getVersionName();
-                                break;
-                            }
+            @Override
+            public PkgCategory createCategory(Object catKey) {
+                // Create API category.
+                PkgCategory cat = null;
+
+                assert catKey instanceof Integer;
+                int apiKey = ((Integer) catKey).intValue();
+
+                // 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 && apiKey != PkgApiCategory.KEY_TOOLS) {
+                    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));
+                cat = new PkgApiCategory(
+                        apiKey,
+                        platformName,
+                        mUpdaterData.getImageFactory().getImageByName(ICON_CAT_PLATFORM));
 
-                    return cat;
-                }
+                return cat;
+            }
 
-                public boolean mergeNewItem(
-                        PkgItem newItem,
-                        PkgCategory cat,
-                        List<PkgCategory> cats,
-                        Set<Package> unusedPackages) {
-
-                    // Behavior for a merge when sorting by API:
-                    // - New items can only be merged with their own category.
-                    // - Normally we expect installed items to be processed first (before new
-                    //   item which will update them), by design (since the local list is always
-                    //   processed first.) If for any reason this isn't the case, we'll show a
-                    //   duplicate right now. That means if we're processing an installed item,
-                    //   we won't try to merge it.
-
-                    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());
-                            return true;
-                        } else if (newItem.getState() == PkgState.NEW && pi.mergeUpdate(p)) {
-                            // The new package is an update for the existing package.
-                            unusedPackages.remove(pi.getMainPackage());
-                            unusedPackages.remove(pi.getUpdatePkg());
-                            return true;
-                        }
+            @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;
                     }
-                    return false;
                 }
 
-                public void postProcessItem(
-                        Object catKey,
-                        PkgCategory category,
-                        PkgItem item) {
-                    assert catKey instanceof Integer;
-                    int apiKey = ((Integer) catKey).intValue();
+                // This is truly a new item.
+                cat.getItems().add(new PkgItem(newPackage, PkgState.NEW));
+                cat.setUnused(false);
+                return true; // something has changed
+            }
 
-                    assert category instanceof PkgApiCategory;
-                    PkgApiCategory cat = (PkgApiCategory) category;
+            @Override
+            public void sortCategoryList(List<PkgCategory> categoryList) {
+                // Sort the categories list.
+                // We always want categories in order tools..platforms..extras.
+                // For platform, we compare in descending order (o2-o1).
+                // This order is achieved by having the category keys ordered as
+                // needed for the sort to just do what we expect.
+
+                Collections.sort(categoryList, new Comparator<PkgCategory>() {
+                    public int compare(PkgCategory cat1, PkgCategory cat2) {
+                        assert cat1 instanceof PkgApiCategory;
+                        assert cat2 instanceof PkgApiCategory;
+                        int api1 = ((Integer) cat1.getKey()).intValue();
+                        int api2 = ((Integer) cat2.getKey()).intValue();
+                        return api2 - api1;
+                    }
+                });
+            }
 
-                    if (apiKey != -1 && cat.getPlatformName() == null) {
+            @Override
+            public void postCategoryItemsChanged(List<PkgCategory> categoryList) {
+                // Sort the items
+                for (PkgCategory cat : mCurrentCategories) {
+                    Collections.sort(cat.getItems());
+
+                    // When sorting by API, we can't always get the platform name
+                    // from the package manager. In this case at the very end we
+                    // look for a potential platform package we can use to extract
+                    // the platform version name (e.g. '1.5') from the first suitable
+                    // platform package we can find.
+
+                    assert cat instanceof PkgApiCategory;
+                    PkgApiCategory pac = (PkgApiCategory) cat;
+                    if (pac.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.getMainPackage();
-                        if (p instanceof PlatformPackage) {
-                            String platformName = ((PlatformPackage) p).getVersionName();
-                            cat.setPlatformName(platformName);
+
+                        for (PkgItem item : cat.getItems()) {
+                            Package p = item.getMainPackage();
+                            if (p instanceof PlatformPackage) {
+                                String platformName = ((PlatformPackage) p).getVersionName();
+                                if (platformName != null) {
+                                    pac.setPlatformName(platformName);
+                                    break;
+                                }
+                            }
                         }
                     }
                 }
 
-                public void sortCategoryList(List<PkgCategory> categoryList) {
-                    // Sort the categories list.
-                    // We always want categories in order tools..platforms..extras.
-                    // For platform, we compare in descending order (o2-o1).
-                    // This order is achieved by having the category keys ordered as
-                    // needed for the sort to just do what we expect.
-
-                    Collections.sort(categoryList, new Comparator<PkgCategory>() {
-                        public int compare(PkgCategory cat1, PkgCategory cat2) {
-                            assert cat1 instanceof PkgApiCategory;
-                            assert cat2 instanceof PkgApiCategory;
-                            int api1 = ((Integer) cat1.getKey()).intValue();
-                            int api2 = ((Integer) cat2.getKey()).intValue();
-                            return api2 - api1;
-                        }
-                    });
-                }
-            });
+            }
         }
 
         /**
-         * Recompute the tree by sorting all packages by source.
-         *
-         * Behavior for a merge when sorting by source:
-         * - Items are grouped under their source even if installed.
-         *   The 'local' source is only for installed items with no source.
+         * {@link UpdateOp} describing the Sort-by-Source operation.
          */
-        void sortBySource() {
-            sort(new ISortOperation() {
-                public List<PkgCategory> getCategories() {
-                    return mSourceCategories;
+        private class UpdateOpSource extends UpdateOp {
+            @Override
+            public Object getCategoryKey(Package pkg) {
+                // Sort by source
+                SdkSource source = pkg.getParentSource();
+                if (source == null) {
+                    return PkgSourceCategory.UNKNOWN_SOURCE;
                 }
+                return source;
+            }
 
-                public void addDefaultCategories(
-                        List<PkgCategory> currentCategories,
-                        Map<Object, PkgCategory> categoryKeyMap,
-                        Set<Object> unusedCategoryKey,
-                        ImageFactory imgFactory) {
-
-                    // Always add the local categories, even if empty (unlikely anyway)
-                    if (!unusedCategoryKey.contains(PkgSourceCategory.UNKNOWN_SOURCE)) {
-                        PkgSourceCategory cat = new PkgSourceCategory(
-                                PkgSourceCategory.UNKNOWN_SOURCE,
-                                mUpdaterData);
-                        currentCategories.add(cat);
-                        categoryKeyMap.put(cat.getKey(), cat);
-                        unusedCategoryKey.add(cat.getKey());
+            @Override
+            public void addDefaultCategories(List<PkgCategory> currentCategories) {
+                for (PkgCategory cat : currentCategories) {
+                    if (cat.getKey().equals(PkgSourceCategory.UNKNOWN_SOURCE)) {
+                        // Already present.
+                        return;
                     }
                 }
 
-                public Object getCategoryKey(PkgItem item) {
-                    return item.getSource();
-                }
-
-                public PkgCategory createCategory(
-                        Object catKey,
-                        ImageFactory imgFactory) {
+                // Always add the local categories, even if empty (unlikely anyway)
+                PkgSourceCategory cat = new PkgSourceCategory(
+                        PkgSourceCategory.UNKNOWN_SOURCE,
+                        mUpdaterData);
+                // Mark it as unused so that it can be cleared in updateEnd() if not used.
+                cat.setUnused(true);
+                currentCategories.add(cat);
+            }
 
-                    assert catKey instanceof SdkSource;
+            @Override
+            public PkgCategory createCategory(Object catKey) {
+                assert catKey instanceof SdkSource;
+                PkgCategory cat = new PkgSourceCategory((SdkSource) catKey, mUpdaterData);
+                return cat;
 
-                    PkgCategory cat = new PkgSourceCategory(
-                            (SdkSource) catKey,
-                            mUpdaterData);
+            }
 
-                    return cat;
+            @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;
+                    }
                 }
 
-                public boolean mergeNewItem(
-                        PkgItem newItem,
-                        PkgCategory cat,
-                        List<PkgCategory> cats,
-                        Set<Package> unusedPackages) {
-
-                    for (PkgItem pi : cat.getItems()) {
-                        Package p = newItem.getMainPackage();
-                        if (pi.isSameItemAs(newItem)) {
-                            // It's the same item, keep it.
-                            unusedPackages.remove(pi.getMainPackage());
-                            return true;
-                        } else if (pi.isSameMainPackageAs(p)) {
-                            // It's not exactly the same item but the main package is the same.
-                            // This happens when trying to merge an item which state has changed
-                            // or its update list has changed.
-
-                            if (newItem.getState() == PkgState.INSTALLED &&
-                                    pi.getState() == PkgState.NEW) {
-                                // In source-list mode, installed items 'hide' new items.
-                                // In this case, return false so that the caller add the newItem
-                                // to the category.
-                                return false;
-                            }
+                // This is truly a new item.
+                cat.getItems().add(new PkgItem(newPackage, PkgState.NEW));
+                cat.setUnused(false);
+                return true; // something has changed
+            }
 
-                            unusedPackages.remove(pi.getMainPackage());
-                            return true;
-                        } else if (newItem.getState() == PkgState.NEW && pi.mergeUpdate(p)) {
-                            // The new package is an update for the existing package.
-                            unusedPackages.remove(pi.getMainPackage());
-                            unusedPackages.remove(pi.getUpdatePkg());
-                            return true;
+            @Override
+            public void sortCategoryList(List<PkgCategory> categoryList) {
+                // Sort the sources in ascending source name order,
+                // with the local packages always first.
+
+                Collections.sort(categoryList, new Comparator<PkgCategory>() {
+                    public int compare(PkgCategory cat1, PkgCategory cat2) {
+                        assert cat1 instanceof PkgSourceCategory;
+                        assert cat2 instanceof PkgSourceCategory;
+
+                        SdkSource src1 = ((PkgSourceCategory) cat1).getSource();
+                        SdkSource src2 = ((PkgSourceCategory) cat2).getSource();
+
+                        if (src1 == src2) {
+                            return 0;
+                        } else if (src1 == PkgSourceCategory.UNKNOWN_SOURCE) {
+                            return -1;
+                        } else if (src2 == PkgSourceCategory.UNKNOWN_SOURCE) {
+                            return 1;
                         }
+                        assert src1 != null; // true because LOCAL_SOURCE==null
+                        assert src2 != null;
+                        return src1.toString().compareTo(src2.toString());
                     }
+                });
+            }
 
-                    return false;
-                }
-
-                public void postProcessItem(
-                        Object catKey,
-                        PkgCategory category,
-                        PkgItem item) {
-                    // pass
-                }
-
-                public void sortCategoryList(List<PkgCategory> categoryList) {
-
-                    // Sort the sources in ascending source name order,
-                    // with the local packages always first.
-
-                    Collections.sort(categoryList, new Comparator<PkgCategory>() {
-                        public int compare(PkgCategory cat1, PkgCategory cat2) {
-                            assert cat1 instanceof PkgSourceCategory;
-                            assert cat2 instanceof PkgSourceCategory;
-
-                            SdkSource src1 = ((PkgSourceCategory) cat1).getSource();
-                            SdkSource src2 = ((PkgSourceCategory) cat2).getSource();
-
-                            if (src1 == src2) {
-                                return 0;
-                            } else if (src1 == PkgSourceCategory.UNKNOWN_SOURCE) {
-                                return -1;
-                            } else if (src2 == PkgSourceCategory.UNKNOWN_SOURCE) {
-                                return 1;
-                            }
-                            assert src1 != null; // true because LOCAL_SOURCE==null
-                            assert src2 != null;
-                            return src1.toString().compareTo(src2.toString());
-                        }
-                    });
+            @Override
+            public void postCategoryItemsChanged(List<PkgCategory> categoryList) {
+                // Sort the items
+                for (PkgCategory cat : mCurrentCategories) {
+                    Collections.sort(cat.getItems());
                 }
-            });
+            }
         }
-
-        /**
-         * Used by {@link #sort(ISortOperation)} to determine if a given item from
-         * the input {@link #mAllPkgItems} should be displayed or not. This is what
-         * allows us to filter items in our out of the tree displayed depending on
-         * user flags, without actually reloading anything.
-         */
-        abstract boolean keepItem(PkgItem item);
     }
 
 
index 8fb4f1c..964c30a 100755 (executable)
@@ -138,4 +138,33 @@ class MockEmptyPackage extends Package {
             mTestHandle.equals(((MockEmptyPackage) pkg).mTestHandle);
     }
 
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = super.hashCode();
+        result = prime * result + ((mTestHandle == null) ? 0 : mTestHandle.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!super.equals(obj)) {
+            return false;
+        }
+        if (!(obj instanceof MockEmptyPackage)) {
+            return false;
+        }
+        MockEmptyPackage other = (MockEmptyPackage) obj;
+        if (mTestHandle == null) {
+            if (other.mTestHandle != null) {
+                return false;
+            }
+        } else if (!mTestHandle.equals(other.mTestHandle)) {
+            return false;
+        }
+        return true;
+    }
 }
diff --git a/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/PackagesDiffLogicTest.java b/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/PackagesDiffLogicTest.java
new file mode 100755 (executable)
index 0000000..3be5d4f
--- /dev/null
@@ -0,0 +1,789 @@
+/*
+ * 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.sdklib.internal.repository.Package;
+import com.android.sdklib.internal.repository.SdkRepoSource;
+import com.android.sdklib.internal.repository.SdkSource;
+import com.android.sdkuilib.internal.repository.PackageLoader.PkgItem;
+import com.android.sdkuilib.internal.repository.PackagesPage.PackagesDiffLogic;
+import com.android.sdkuilib.internal.repository.PackagesPage.PkgCategory;
+import com.android.sdkuilib.internal.repository.PackagesPage.PackagesDiffLogic.UpdateOp;
+
+import junit.framework.TestCase;
+
+public class PackagesDiffLogicTest extends TestCase {
+
+    private PackagesDiffLogic m;
+    private MockUpdaterData u;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        u = new MockUpdaterData();
+        m = new PackagesDiffLogic(u);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    // ----
+    //
+    // Test Details Note: the way load is implemented in PackageLoader, the
+    // loader processes each source and then for each source the packages are added
+    // to a list and the sorting algorithm is called with that list. Thus for
+    // one load, many calls to the sortByX/Y happen, with the list progressively
+    // being populated.
+    // However when the user switches sorting algorithm, the package list is not
+    // reloaded and is processed at once.
+
+    public void testSortByApi_Empty() {
+        UpdateOp op = m.updateStart(true /*sortByApi*/);
+        assertFalse(m.updateSourcePackages(op, null /*locals*/, new Package[0]));
+        assertFalse(m.updateEnd(op));
+
+        assertSame(m.mCurrentCategories, m.mApiCategories);
+
+        // We also keep these 2 categories even if they contain nothing
+        assertEquals(
+                "PkgApiCategory <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgApiCategory <API=EXTRAS, label=Extras, #items=0>\n",
+               getTree(m));
+    }
+
+    public void testSortByApi_AddSamePackage() {
+        SdkSource src1 = new SdkRepoSource("http://repo.com/url", "repo1");
+
+        UpdateOp op = m.updateStart(true /*sortByApi*/);
+        // First insert local packages
+        assertTrue(m.updateSourcePackages(op, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "some pkg", 1)
+        }));
+
+        assertEquals(
+                "PkgApiCategory <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgApiCategory <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'some pkg' rev=1>\n",
+               getTree(m));
+
+        // Insert the next source
+        // Same package as the one installed, so we don't display it
+        assertFalse(m.updateSourcePackages(op, src1, new Package[] {
+                new MockEmptyPackage(src1, "some pkg", 1)
+        }));
+
+        assertFalse(m.updateEnd(op));
+
+        assertEquals(
+                "PkgApiCategory <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgApiCategory <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'some pkg' rev=1>\n",
+               getTree(m));
+    }
+
+    public void testSortByApi_AddOtherPackage() {
+        SdkSource src1 = new SdkRepoSource("http://repo.com/url", "repo1");
+
+        UpdateOp op = m.updateStart(true /*sortByApi*/);
+        // First insert local packages
+        assertTrue(m.updateSourcePackages(op, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "some pkg", 1)
+        }));
+
+        assertEquals(
+                "PkgApiCategory <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgApiCategory <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'some pkg' rev=1>\n",
+               getTree(m));
+
+        // Insert the next source
+        // Not the same package as the one installed, so we'll display it
+        assertTrue(m.updateSourcePackages(op, src1, new Package[] {
+                new MockEmptyPackage(src1, "other pkg", 1)
+        }));
+
+        assertFalse(m.updateEnd(op));
+
+        assertEquals(
+                "PkgApiCategory <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgApiCategory <API=EXTRAS, label=Extras, #items=2>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'some pkg' rev=1>\n" +
+                "-- <NEW, pkg:MockEmptyPackage 'other pkg' rev=1>\n",
+               getTree(m));
+    }
+
+    public void testSortByApi_Update1() {
+        SdkSource src1 = new SdkRepoSource("http://repo.com/url", "repo1");
+
+        // Typical case: user has a locally installed package in revision 1
+        // The display list after sort should show that installed package.
+        UpdateOp op = m.updateStart(true /*sortByApi*/);
+        // First insert local packages
+        assertTrue(m.updateSourcePackages(op, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1)
+        }));
+
+        assertEquals(
+                "PkgApiCategory <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgApiCategory <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1>\n",
+               getTree(m));
+
+        assertTrue(m.updateSourcePackages(op, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 4),
+                new MockEmptyPackage(src1, "type1", 2)
+        }));
+
+        assertFalse(m.updateEnd(op));
+
+        assertEquals(
+                "PkgApiCategory <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgApiCategory <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=4>\n",
+               getTree(m));
+    }
+
+    public void testSortByApi_Reload() {
+        SdkSource src1 = new SdkRepoSource("http://repo.com/url", "repo1");
+
+        // First load reveals a package local package and its update
+        UpdateOp op1 = m.updateStart(true /*sortByApi*/);
+        // First insert local packages
+        assertTrue(m.updateSourcePackages(op1, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1)
+        }));
+        assertTrue(m.updateSourcePackages(op1, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 2)
+        }));
+
+        assertFalse(m.updateEnd(op1));
+
+        assertEquals(
+                "PkgApiCategory <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgApiCategory <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n",
+               getTree(m));
+
+        // Now simulate a reload that clears the package list and create similar
+        // objects but not the same references. The only difference is that updateXyz
+        // returns false since they don't change anything.
+
+        UpdateOp op2 = m.updateStart(true /*sortByApi*/);
+        // First insert local packages
+        assertFalse(m.updateSourcePackages(op2, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1)
+        }));
+        assertFalse(m.updateSourcePackages(op2, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 2)
+        }));
+
+        assertFalse(m.updateEnd(op2));
+
+        assertEquals(
+                "PkgApiCategory <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgApiCategory <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n",
+               getTree(m));
+    }
+
+    public void testSortByApi_InstallPackage() {
+        SdkSource src1 = new SdkRepoSource("http://repo.com/url", "repo1");
+
+        // First load reveals a new package
+        UpdateOp op1 = m.updateStart(true /*sortByApi*/);
+        // No local packages at first
+        assertFalse(m.updateSourcePackages(op1, null /*locals*/, new Package[0]));
+        assertTrue(m.updateSourcePackages(op1, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1)
+        }));
+
+        assertFalse(m.updateEnd(op1));
+
+        assertEquals(
+                "PkgApiCategory <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgApiCategory <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <NEW, pkg:MockEmptyPackage 'type1' rev=1>\n",
+               getTree(m));
+
+        // Install it.
+        UpdateOp op2 = m.updateStart(true /*sortByApi*/);
+        // local packages
+        assertTrue(m.updateSourcePackages(op2, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1)
+        }));
+        assertFalse(m.updateSourcePackages(op2, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1)
+        }));
+
+        assertFalse(m.updateEnd(op2));
+
+        assertEquals(
+                "PkgApiCategory <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgApiCategory <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1>\n",
+               getTree(m));
+
+        // Load reveals an update
+        UpdateOp op3 = m.updateStart(true /*sortByApi*/);
+        // local packages
+        assertFalse(m.updateSourcePackages(op3, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1)
+        }));
+        assertTrue(m.updateSourcePackages(op3, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 2)
+        }));
+
+        assertFalse(m.updateEnd(op3));
+
+        assertEquals(
+                "PkgApiCategory <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgApiCategory <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n",
+               getTree(m));
+    }
+
+    public void testSortByApi_DeletePackage() {
+        SdkSource src1 = new SdkRepoSource("http://repo.com/url", "repo1");
+
+        // We have an installed package
+        UpdateOp op2 = m.updateStart(true /*sortByApi*/);
+        // local packages
+        assertTrue(m.updateSourcePackages(op2, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1)
+        }));
+        assertTrue(m.updateSourcePackages(op2, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 2)
+        }));
+
+        assertFalse(m.updateEnd(op2));
+
+        assertEquals(
+                "PkgApiCategory <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgApiCategory <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n",
+               getTree(m));
+
+        // User now deletes the installed package.
+        UpdateOp op1 = m.updateStart(true /*sortByApi*/);
+        // No local packages
+        assertTrue(m.updateSourcePackages(op1, null /*locals*/, new Package[0]));
+        assertTrue(m.updateSourcePackages(op1, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1)
+        }));
+
+        assertFalse(m.updateEnd(op1));
+
+        assertEquals(
+                "PkgApiCategory <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgApiCategory <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <NEW, pkg:MockEmptyPackage 'type1' rev=1>\n",
+               getTree(m));
+    }
+
+    public void testSortByApi_CompleteUpdate() {
+        SdkSource src1 = new SdkRepoSource("http://repo.com/url1", "repo1");
+        SdkSource src2 = new SdkRepoSource("http://repo.com/url2", "repo2");
+
+        // 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.
+
+        // First update has the typical tools and a couple extras
+        UpdateOp op1 = m.updateStart(true /*sortByApi*/);
+
+        assertTrue(m.updateSourcePackages(op1, null /*locals*/, new Package[] {
+                new MockToolPackage(src1, 10, 3),
+                new MockPlatformToolPackage(src1, 3),
+                new MockExtraPackage(src1, "android", "usb_driver", 4, 3),
+        }));
+        assertTrue(m.updateSourcePackages(op1, src1, new Package[] {
+                new MockToolPackage(src1, 10, 3),
+                new MockPlatformToolPackage(src1, 3),
+                new MockExtraPackage(src1, "carrier", "custom_rom", 1, 0),
+                new MockExtraPackage(src1, "android", "usb_driver", 5, 3),
+        }));
+        assertFalse(m.updateEnd(op1));
+
+        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));
+
+        // Next update adds platforms and addon, sorted in a category based on their API level
+        UpdateOp op2 = m.updateStart(true /*sortByApi*/);
+        MockPlatformPackage p1;
+        MockPlatformPackage p2;
+
+        assertTrue(m.updateSourcePackages(op2, null /*locals*/, new Package[] {
+                new MockToolPackage(src1, 10, 3),
+                new MockPlatformToolPackage(src1, 3),
+                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),
+                new MockAddonPackage(src2, "addon A", p1, 5),
+        }));
+        assertTrue(m.updateSourcePackages(op2, src1, new Package[] {
+                new MockToolPackage(src1, 10, 3),
+                new MockPlatformToolPackage(src1, 3),
+                new MockExtraPackage(src1, "carrier", "custom_rom", 1, 0),
+                new MockExtraPackage(src1, "android", "usb_driver", 5, 3),
+                // second update
+                p2 = new MockPlatformPackage(src1, 2, 4, 3),    // API 2
+        }));
+        assertTrue(m.updateSourcePackages(op2, src2, new Package[] {
+                new MockAddonPackage(src2, "addon C", p2, 9),
+                new MockAddonPackage(src2, "addon A", p1, 6),
+                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),
+        }));
+        assertFalse(m.updateEnd(op2));
+
+        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));
+
+        // Reloading the same thing should have no impact except for the update methods
+        // returning false when they don't change the current list.
+        UpdateOp op3 = m.updateStart(true /*sortByApi*/);
+
+        assertFalse(m.updateSourcePackages(op3, null /*locals*/, new Package[] {
+                new MockToolPackage(src1, 10, 3),
+                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),
+                new MockAddonPackage(src2, "addon A", p1, 5),
+        }));
+        assertFalse(m.updateSourcePackages(op3, src1, new Package[] {
+                new MockToolPackage(src1, 10, 3),
+                new MockPlatformToolPackage(src1, 3),
+                new MockExtraPackage(src1, "carrier", "custom_rom", 1, 0),
+                new MockExtraPackage(src1, "android", "usb_driver", 5, 3),
+                // second update
+                p2 = new MockPlatformPackage(src1, 2, 4, 3),
+        }));
+        assertTrue(m.updateSourcePackages(op3, src2, new Package[] {
+                new MockAddonPackage(src2, "addon C", p2, 9),
+                new MockAddonPackage(src2, "addon A", p1, 6),
+                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),
+        }));
+        assertFalse(m.updateEnd(op3));
+
+        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));
+    }
+
+    // ----
+
+    public void testSortBySource_Empty() {
+        UpdateOp op = m.updateStart(false /*sortByApi*/);
+        assertFalse(m.updateSourcePackages(op, null /*locals*/, new Package[0]));
+        // UpdateEnd returns true since it removed the synthetic "unknown source" category
+        assertTrue(m.updateEnd(op));
+
+        assertSame(m.mCurrentCategories, m.mSourceCategories);
+        assertTrue(m.mApiCategories.isEmpty());
+
+        assertEquals(
+                "",
+               getTree(m));
+    }
+
+    public void testSortBySource_AddPackages() {
+        // Since we're sorting by source, items are grouped under their source
+        // even if installed. The 'local' source is only for installed items for
+        // which we don't know the source.
+        SdkSource src1 = new SdkRepoSource("http://repo.com/url", "repo1");
+
+        UpdateOp op = m.updateStart(false /*sortByApi*/);
+        assertTrue(m.updateSourcePackages(op, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "known source", 2),
+                new MockEmptyPackage(null, "unknown source", 3),
+        }));
+
+        assertEquals(
+                "PkgSourceCategory <source=Local Packages (no.source), #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'unknown source' rev=3>\n" +
+                "PkgSourceCategory <source=repo1 (repo.com), #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'known source' rev=2>\n",
+               getTree(m));
+
+        assertTrue(m.updateSourcePackages(op, src1, new Package[] {
+                new MockEmptyPackage(src1, "new", 1),
+        }));
+
+        assertFalse(m.updateEnd(op));
+
+        assertEquals(
+                "PkgSourceCategory <source=Local Packages (no.source), #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'unknown source' rev=3>\n" +
+                "PkgSourceCategory <source=repo1 (repo.com), #items=2>\n" +
+                "-- <NEW, pkg:MockEmptyPackage 'new' rev=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'known source' rev=2>\n",
+               getTree(m));
+    }
+
+    public void testSortBySource_Update1() {
+
+        // Typical case: user has a locally installed package in revision 1
+        // The display list after sort should show that instaled package.
+        SdkSource src1 = new SdkRepoSource("http://repo.com/url", "repo1");
+        UpdateOp op = m.updateStart(false /*sortByApi*/);
+        assertTrue(m.updateSourcePackages(op, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1),
+        }));
+
+        assertEquals(
+                "PkgSourceCategory <source=Local Packages (no.source), #items=0>\n" +
+                "PkgSourceCategory <source=repo1 (repo.com), #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1>\n",
+               getTree(m));
+
+        // Edge case: the source reveals an update in revision 2. It is ignored since
+        // we already have a package in rev 4.
+
+        assertTrue(m.updateSourcePackages(op, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 4),
+                new MockEmptyPackage(src1, "type1", 2),
+        }));
+
+        assertTrue(m.updateEnd(op));
+
+        assertEquals(
+                "PkgSourceCategory <source=repo1 (repo.com), #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=4>\n",
+               getTree(m));
+    }
+
+    public void testSortBySource_Reload() {
+
+        // First load reveals a package local package and its update
+        SdkSource src1 = new SdkRepoSource("http://repo.com/url", "repo1");
+        UpdateOp op1 = m.updateStart(false /*sortByApi*/);
+        assertTrue(m.updateSourcePackages(op1, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1),
+        }));
+        assertTrue(m.updateSourcePackages(op1, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 2),
+        }));
+        assertTrue(m.updateEnd(op1));
+
+        assertEquals(
+                "PkgSourceCategory <source=repo1 (repo.com), #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n",
+               getTree(m));
+
+        // Now simulate a reload that clears the package list and creates similar
+        // objects but not the same references. Update methods return false since
+        // they don't change anything.
+        UpdateOp op2 = m.updateStart(false /*sortByApi*/);
+        assertFalse(m.updateSourcePackages(op2, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1),
+        }));
+        assertFalse(m.updateSourcePackages(op2, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 2),
+        }));
+        assertTrue(m.updateEnd(op2));
+
+        assertEquals(
+                "PkgSourceCategory <source=repo1 (repo.com), #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n",
+               getTree(m));
+    }
+
+    public void testSortBySource_InstallPackage() {
+
+        // First load reveals a new package
+        SdkSource src1 = new SdkRepoSource("http://repo.com/url", "repo1");
+        UpdateOp op1 = m.updateStart(false /*sortByApi*/);
+        // no local package
+        assertFalse(m.updateSourcePackages(op1, null /*locals*/, new Package[0]));
+        assertTrue(m.updateSourcePackages(op1, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1),
+        }));
+        assertTrue(m.updateEnd(op1));
+
+        assertEquals(
+                "PkgSourceCategory <source=repo1 (repo.com), #items=1>\n" +
+                "-- <NEW, pkg:MockEmptyPackage 'type1' rev=1>\n",
+               getTree(m));
+
+
+        // Install it. The display only shows the installed one, 'hiding' the remote package
+        UpdateOp op2 = m.updateStart(false /*sortByApi*/);
+        assertTrue(m.updateSourcePackages(op2, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1),
+        }));
+        assertFalse(m.updateSourcePackages(op2, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1),
+        }));
+        assertTrue(m.updateEnd(op2));
+
+        assertEquals(
+                "PkgSourceCategory <source=repo1 (repo.com), #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1>\n",
+               getTree(m));
+
+        // Now we have an update
+        UpdateOp op3 = m.updateStart(false /*sortByApi*/);
+        assertFalse(m.updateSourcePackages(op3, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1),
+        }));
+        assertTrue(m.updateSourcePackages(op3, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 2),
+        }));
+        assertTrue(m.updateEnd(op3));
+
+        assertEquals(
+                "PkgSourceCategory <source=repo1 (repo.com), #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n",
+               getTree(m));
+    }
+
+    public void testSortBySource_DeletePackage() {
+        SdkSource src1 = new SdkRepoSource("http://repo.com/url", "repo1");
+
+        // Start with an installed package and its matching remote package
+        UpdateOp op2 = m.updateStart(false /*sortByApi*/);
+        assertTrue(m.updateSourcePackages(op2, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1),
+        }));
+        assertFalse(m.updateSourcePackages(op2, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1),
+        }));
+        assertTrue(m.updateEnd(op2));
+
+        assertEquals(
+                "PkgSourceCategory <source=repo1 (repo.com), #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1>\n",
+               getTree(m));
+
+        // User now deletes the installed package.
+        UpdateOp op1 = m.updateStart(false /*sortByApi*/);
+        // no local package
+        assertTrue(m.updateSourcePackages(op1, null /*locals*/, new Package[0]));
+        assertFalse(m.updateSourcePackages(op1, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1),
+        }));
+        assertTrue(m.updateEnd(op1));
+
+        assertEquals(
+                "PkgSourceCategory <source=repo1 (repo.com), #items=1>\n" +
+                "-- <NEW, pkg:MockEmptyPackage 'type1' rev=1>\n",
+               getTree(m));
+    }
+
+    public void testSortBySource_CompleteUpdate() {
+        SdkSource src1 = new SdkRepoSource("http://repo.com/url1", "repo1");
+        SdkSource src2 = new SdkRepoSource("http://repo.com/url2", "repo2");
+
+        // First update has the typical tools and a couple extras
+        UpdateOp op1 = m.updateStart(false /*sortByApi*/);
+
+        assertTrue(m.updateSourcePackages(op1, null /*locals*/, new Package[] {
+                new MockToolPackage(src1, 10, 3),
+                new MockPlatformToolPackage(src1, 3),
+                new MockExtraPackage(src1, "android", "usb_driver", 4, 3),
+        }));
+        assertTrue(m.updateSourcePackages(op1, src1, new Package[] {
+                new MockToolPackage(src1, 10, 3),
+                new MockPlatformToolPackage(src1, 3),
+                new MockExtraPackage(src1, "carrier", "custom_rom", 1, 0),
+                new MockExtraPackage(src1, "android", "usb_driver", 5, 3),
+        }));
+        assertTrue(m.updateEnd(op1));
+
+        assertEquals(
+                "PkgSourceCategory <source=repo1 (repo.com), #items=4>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 10>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3>\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));
+
+        // Next update adds platforms and addon, sorted in a category based on their API level
+        UpdateOp op2 = m.updateStart(false /*sortByApi*/);
+        MockPlatformPackage p1;
+        MockPlatformPackage p2;
+
+        assertTrue(m.updateSourcePackages(op2, null /*locals*/, new Package[] {
+                new MockToolPackage(src1, 10, 3),
+                new MockPlatformToolPackage(src1, 3),
+                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),       // API 3
+                new MockAddonPackage(src2, "addon A", p1, 5),
+        }));
+        assertTrue(m.updateSourcePackages(op2, src1, new Package[] {
+                new MockToolPackage(src1, 10, 3),
+                new MockPlatformToolPackage(src1, 3),
+                new MockExtraPackage(src1, "carrier", "custom_rom", 1, 0),
+                new MockExtraPackage(src1, "android", "usb_driver", 5, 3),
+                // second update
+                p2 = new MockPlatformPackage(src1, 2, 4, 3),    // API 2
+        }));
+        assertTrue(m.updateSourcePackages(op2, src2, new Package[] {
+                new MockAddonPackage(src2, "addon C", p2, 9),
+                new MockAddonPackage(src2, "addon A", p1, 6),
+                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),
+        }));
+        assertTrue(m.updateEnd(op2));
+
+        assertEquals(
+                "PkgSourceCategory <source=repo1 (repo.com), #items=7>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 10>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-3, API 3, revision 6>\n" +
+                "-- <NEW, pkg:SDK Platform Android android-2, API 2, revision 4>\n" +
+                "-- <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" +
+                "PkgSourceCategory <source=repo2 (repo.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" +
+                "-- <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",
+               getTree(m));
+
+        // Reloading the same thing should have no impact except for the update methods
+        // returning false when they don't change the current list.
+        UpdateOp op3 = m.updateStart(false /*sortByApi*/);
+
+        assertFalse(m.updateSourcePackages(op3, null /*locals*/, new Package[] {
+                new MockToolPackage(src1, 10, 3),
+                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),
+                new MockAddonPackage(src2, "addon A", p1, 5),
+        }));
+        assertFalse(m.updateSourcePackages(op3, src1, new Package[] {
+                new MockToolPackage(src1, 10, 3),
+                new MockPlatformToolPackage(src1, 3),
+                new MockExtraPackage(src1, "carrier", "custom_rom", 1, 0),
+                new MockExtraPackage(src1, "android", "usb_driver", 5, 3),
+                // second update
+                p2 = new MockPlatformPackage(src1, 2, 4, 3),
+        }));
+        assertTrue(m.updateSourcePackages(op3, src2, new Package[] {
+                new MockAddonPackage(src2, "addon C", p2, 9),
+                new MockAddonPackage(src2, "addon A", p1, 6),
+                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),
+        }));
+        assertTrue(m.updateEnd(op3));
+
+        assertEquals(
+                "PkgSourceCategory <source=repo1 (repo.com), #items=7>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 10>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-3, API 3, revision 6>\n" +
+                "-- <NEW, pkg:SDK Platform Android android-2, API 2, revision 4>\n" +
+                "-- <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" +
+                "PkgSourceCategory <source=repo2 (repo.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" +
+                "-- <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",
+               getTree(m));
+    }
+
+    // ----
+
+    /**
+     * 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 &lt;description&gt;
+     *   -- &lt;PkgItem description&gt;
+     * </pre>
+     */
+    public String getTree(PackagesDiffLogic 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/PackagesPageLogicTest.java b/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/PackagesPageLogicTest.java
deleted file mode 100755 (executable)
index d3f30ac..0000000
+++ /dev/null
@@ -1,727 +0,0 @@
-/*
- * 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.sdklib.internal.repository.SdkRepoSource;
-import com.android.sdklib.internal.repository.SdkSource;
-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();
-    }
-
-    // ----
-    //
-    // Test Details Note: the way load is implemented in PackageLoader, the
-    // loader processes each source and then for each source the packages are added
-    // to a list and the sorting algorithm is called with that list. Thus for
-    // one load, many calls to the sortByX/Y happen, with the list progressively
-    // being populated.
-    // However when the user switches sorting algorithm, the package list is not
-    // reloaded and is processed at once.
-
-    public void testSortByApi_Empty() {
-        assertTrue(m.mAllPkgItems.isEmpty());
-        m.sortByApiLevel();
-        assertSame(m.mCurrentCategories, m.mApiCategories);
-        assertTrue(m.mApiCategories.isEmpty());
-    }
-
-    public void testSortByApi_SamePackage() {
-        assertTrue(m.mAllPkgItems.isEmpty());
-        SdkSource src1 = new SdkRepoSource("http://repo.com/url", "repo1");
-
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "some pkg", 1), PkgState.INSTALLED));
-        m.sortByApiLevel();
-
-        assertEquals(
-                "PkgApiCategory <API=EXTRAS, label=Extras, #items=1>\n" +
-                "-- <INSTALLED, pkg:MockEmptyPackage 'some pkg' rev=1>\n",
-               getTree(m));
-
-        // Same package as the one installed, so we don't display it
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "some pkg", 1), PkgState.NEW));
-        m.sortByApiLevel();
-
-        assertEquals(
-                "PkgApiCategory <API=EXTRAS, label=Extras, #items=1>\n" +
-                "-- <INSTALLED, pkg:MockEmptyPackage 'some pkg' rev=1>\n",
-               getTree(m));
-    }
-
-    public void testSortByApi_AddPackages() {
-        assertTrue(m.mAllPkgItems.isEmpty());
-        SdkSource src1 = new SdkRepoSource("http://repo.com/url", "repo1");
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "that pkg", 1), PkgState.INSTALLED));
-        m.sortByApiLevel();
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "this pkg", 1), PkgState.NEW));
-        m.sortByApiLevel();
-
-        assertEquals(
-                "PkgApiCategory <API=EXTRAS, label=Extras, #items=2>\n" +
-                "-- <INSTALLED, pkg:MockEmptyPackage 'that pkg' rev=1>\n" +
-                "-- <NEW, pkg:MockEmptyPackage 'this pkg' rev=1>\n",
-               getTree(m));
-    }
-
-    public void testSortByApi_Update1() {
-        assertTrue(m.mAllPkgItems.isEmpty());
-        SdkSource src1 = new SdkRepoSource("http://repo.com/url", "repo1");
-
-        // 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(src1, "type1", 1), PkgState.INSTALLED));
-        m.sortByApiLevel();
-
-        assertEquals(
-                "PkgApiCategory <API=EXTRAS, label=Extras, #items=1>\n" +
-                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' 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(src1, "type1", 4), PkgState.NEW));
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 2), PkgState.NEW));
-        m.sortByApiLevel();
-
-        assertEquals(
-                "PkgApiCategory <API=EXTRAS, label=Extras, #items=1>\n" +
-                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=4>\n",
-               getTree(m));
-    }
-
-    public void testSortByApi_Reload() {
-        assertTrue(m.mAllPkgItems.isEmpty());
-        SdkSource src1 = new SdkRepoSource("http://repo.com/url", "repo1");
-
-        // First load reveals a package local package and its update
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 1), PkgState.INSTALLED));
-        m.sortByApiLevel();
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 2), PkgState.NEW));
-        m.sortByApiLevel();
-
-        assertEquals(
-                "PkgApiCategory <API=EXTRAS, label=Extras, #items=1>\n" +
-                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n",
-               getTree(m));
-
-        // Now simulate a reload that clears the package list and create similar
-        // objects but not the same references.
-        m.mAllPkgItems.clear();
-        assertTrue(m.mAllPkgItems.isEmpty());
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 1), PkgState.INSTALLED));
-        m.sortByApiLevel();
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 2), PkgState.NEW));
-        m.sortByApiLevel();
-
-        assertEquals(
-                "PkgApiCategory <API=EXTRAS, label=Extras, #items=1>\n" +
-                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n",
-               getTree(m));
-    }
-
-    public void testSortByApi_InstallAfterNew() {
-        assertTrue(m.mAllPkgItems.isEmpty());
-        SdkSource src1 = new SdkRepoSource("http://repo.com/url", "repo1");
-
-        // We expect updates to appear AFTER the packages the installed items will update.
-        // (This is pretty much guaranteed since local packages are processed first.)
-        // The reverse order is not supported by the sorting algorithm and both will be shown.
-
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 2), PkgState.NEW));
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 1), PkgState.INSTALLED));
-        m.sortByApiLevel();
-
-        assertEquals(
-                "PkgApiCategory <API=EXTRAS, label=Extras, #items=2>\n" +
-                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1>\n" +
-                "-- <NEW, pkg:MockEmptyPackage 'type1' rev=2>\n",
-               getTree(m));
-    }
-
-    public void testSortByApi_InstallPackage() {
-        assertTrue(m.mAllPkgItems.isEmpty());
-        SdkSource src1 = new SdkRepoSource("http://repo.com/url", "repo1");
-
-        // First load reveals a new package
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 1), PkgState.NEW));
-        m.sortByApiLevel();
-
-        assertEquals(
-                "PkgApiCategory <API=EXTRAS, label=Extras, #items=1>\n" +
-                "-- <NEW, pkg:MockEmptyPackage 'type1' rev=1>\n",
-               getTree(m));
-
-        // Install it. Load reveals a package local package and its update
-        m.mAllPkgItems.clear();
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 1), PkgState.INSTALLED));
-        m.sortByApiLevel();
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 1), PkgState.NEW));
-        m.sortByApiLevel();
-
-        assertEquals(
-                "PkgApiCategory <API=EXTRAS, label=Extras, #items=1>\n" +
-                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1>\n",
-               getTree(m));
-
-        // Now we have an update
-        m.mAllPkgItems.clear();
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 1), PkgState.INSTALLED));
-        m.sortByApiLevel();
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 2), PkgState.NEW));
-        m.sortByApiLevel();
-
-        assertEquals(
-                "PkgApiCategory <API=EXTRAS, label=Extras, #items=1>\n" +
-                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n",
-               getTree(m));
-    }
-
-    public void testSortByApi_DeletePackage() {
-        assertTrue(m.mAllPkgItems.isEmpty());
-        SdkSource src1 = new SdkRepoSource("http://repo.com/url", "repo1");
-
-        // We have an installed package
-        m.mAllPkgItems.clear();
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 1), PkgState.INSTALLED));
-        m.sortByApiLevel();
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 1), PkgState.NEW));
-        m.sortByApiLevel();
-
-        assertEquals(
-                "PkgApiCategory <API=EXTRAS, label=Extras, #items=1>\n" +
-                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1>\n",
-               getTree(m));
-
-        // User now deletes the installed package.
-        m.mAllPkgItems.clear();
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 1), PkgState.NEW));
-        m.sortByApiLevel();
-
-        assertEquals(
-                "PkgApiCategory <API=EXTRAS, label=Extras, #items=1>\n" +
-                "-- <NEW, pkg:MockEmptyPackage 'type1' rev=1>\n",
-               getTree(m));
-    }
-
-    public void testSortByApi_CompleteUpdate() {
-        assertTrue(m.mAllPkgItems.isEmpty());
-        SdkSource src1 = new SdkRepoSource("http://repo.com/url", "repo1");
-
-        // 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));
-        // We'll typically see installed items twice, first as installed then as new packages
-        // coming from the source that delivered them. The new ones should be ignored.
-        m.mAllPkgItems.add(new PkgItem(new MockToolPackage(10, 3), PkgState.NEW));
-        m.mAllPkgItems.add(new PkgItem(new MockPlatformToolPackage(3), PkgState.NEW));
-
-        // Load a few extra packages
-        m.mAllPkgItems.add(new PkgItem(
-                new MockExtraPackage(src1, "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(src1, "android", "usb_driver", 4, 3), PkgState.INSTALLED));
-        m.mAllPkgItems.add(new PkgItem(
-                new MockExtraPackage(src1, "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(src1, 1, 2, 3), PkgState.INSTALLED));
-        m.mAllPkgItems.add(new PkgItem(p2 = new MockPlatformPackage(src1, 2, 4, 3), PkgState.NEW));
-        m.mAllPkgItems.add(new PkgItem(     new MockPlatformPackage(src1, 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(src1, "addon C", p2, 9), PkgState.NEW));
-        m.mAllPkgItems.add(new PkgItem(new MockAddonPackage(src1, "addon A", p1, 5), PkgState.INSTALLED));
-        m.mAllPkgItems.add(new PkgItem(new MockAddonPackage(src1, "addon A", p1, 6), PkgState.NEW));
-        m.mAllPkgItems.add(new PkgItem(new MockAddonPackage(src1, "addon B", p2, 7), PkgState.NEW));
-        // the rev 8 update will be ignored since there's a rev 9 coming after
-        m.mAllPkgItems.add(new PkgItem(new MockAddonPackage(src1, "addon B", p2, 8), PkgState.NEW));
-        m.mAllPkgItems.add(new PkgItem(new MockAddonPackage(src1, "addon B", p2, 9), 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=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));
-
-        // Now simulate a change of sorting algorithm: sort by source then by API again.
-
-        m.sortBySource();
-        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));
-}
-
-    // ----
-
-    public void testSortBySource_Empty() {
-        assertTrue(m.mAllPkgItems.isEmpty());
-        m.sortBySource();
-        assertSame(m.mCurrentCategories, m.mSourceCategories);
-        assertTrue(m.mApiCategories.isEmpty());
-    }
-
-
-    public void testSortBySource_AddPackages() {
-        assertTrue(m.mAllPkgItems.isEmpty());
-
-        // Since we're sorting by source, items are grouped under their source
-        // even if installed. The 'local' source is only for installed items for
-        // which we don't know the source.
-        SdkSource src1 = new SdkRepoSource("http://repo.com/url", "repo1");
-
-        m.mAllPkgItems.add(new PkgItem(
-                new MockEmptyPackage(src1, "new", 1), PkgState.NEW));
-        m.mAllPkgItems.add(new PkgItem(
-                new MockEmptyPackage(src1, "known source", 2), PkgState.INSTALLED));
-        m.mAllPkgItems.add(new PkgItem(
-                new MockEmptyPackage(null, "unknown source", 3), PkgState.INSTALLED));
-
-        m.sortBySource();
-
-        assertEquals(
-                "PkgSourceCategory <source=Local, #items=1>\n" +
-                "-- <INSTALLED, pkg:MockEmptyPackage 'unknown source' rev=3>\n" +
-                "PkgSourceCategory <source=repo1 (repo.com), #items=2>\n" +
-                "-- <NEW, pkg:MockEmptyPackage 'new' rev=1>\n" +
-                "-- <INSTALLED, pkg:MockEmptyPackage 'known source' rev=2>\n",
-               getTree(m));
-    }
-
-    public void testSortBySource_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.
-        SdkSource src1 = new SdkRepoSource("http://repo.com/url", "repo1");
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 1), PkgState.INSTALLED));
-
-        m.sortBySource();
-
-        assertEquals(
-                "PkgSourceCategory <source=repo1 (repo.com), #items=1>\n" +
-                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1>\n",
-               getTree(m));
-
-        // Edge case: the source reveals an update in revision 2. It is ignored since
-        // we already have a package in rev 4.
-
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 4), PkgState.NEW));
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 2), PkgState.NEW));
-
-        m.sortBySource();
-
-        assertEquals(
-                "PkgSourceCategory <source=repo1 (repo.com), #items=1>\n" +
-                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=4>\n",
-               getTree(m));
-    }
-
-    public void testSortBySource_Reload() {
-        assertTrue(m.mAllPkgItems.isEmpty());
-
-        // First load reveals a package local package and its update
-        SdkSource src1 = new SdkRepoSource("http://repo.com/url", "repo1");
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 1), PkgState.INSTALLED));
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 2), PkgState.NEW));
-
-        m.sortBySource();
-
-        assertEquals(
-                "PkgSourceCategory <source=repo1 (repo.com), #items=1>\n" +
-                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n",
-               getTree(m));
-
-        // Now simulate a reload that clears the package list and create similar
-        // objects but not the same references.
-        m.mAllPkgItems.clear();
-        assertTrue(m.mAllPkgItems.isEmpty());
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 1), PkgState.INSTALLED));
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 2), PkgState.NEW));
-
-        m.sortBySource();
-
-        assertEquals(
-                "PkgSourceCategory <source=repo1 (repo.com), #items=1>\n" +
-                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n",
-               getTree(m));
-    }
-
-    public void testSortBySource_InstallAfterNew() {
-        assertTrue(m.mAllPkgItems.isEmpty());
-
-        // We expect updates to appear AFTER the packages the installed items will update.
-        // (This is pretty much guaranteed since local packages are processed first.)
-        // The reverse order is not supported by the sorting algorithm and both will be shown.
-
-        SdkSource src1 = new SdkRepoSource("http://repo.com/url", "repo1");
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 2), PkgState.NEW));
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 1), PkgState.INSTALLED));
-
-        m.sortBySource();
-
-        assertEquals(
-                "PkgSourceCategory <source=repo1 (repo.com), #items=2>\n" +
-                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1>\n" +
-                "-- <NEW, pkg:MockEmptyPackage 'type1' rev=2>\n",
-               getTree(m));
-    }
-
-    public void testSortBySource_InstallPackage() {
-        assertTrue(m.mAllPkgItems.isEmpty());
-
-        // First load reveals a new package
-        SdkSource src1 = new SdkRepoSource("http://repo.com/url", "repo1");
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 1), PkgState.NEW));
-
-        m.sortBySource();
-
-        assertEquals(
-                "PkgSourceCategory <source=repo1 (repo.com), #items=1>\n" +
-                "-- <NEW, pkg:MockEmptyPackage 'type1' rev=1>\n",
-               getTree(m));
-
-        // Install it. The display only shows the installed one, 'hiding' the remote package
-        m.mAllPkgItems.clear();
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 1), PkgState.INSTALLED));
-        m.sortBySource();
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 1), PkgState.NEW));
-        m.sortBySource();
-
-        assertEquals(
-                "PkgSourceCategory <source=repo1 (repo.com), #items=1>\n" +
-                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1>\n",
-               getTree(m));
-
-        // Now we have an update
-        m.mAllPkgItems.clear();
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 1), PkgState.INSTALLED));
-        m.sortBySource();
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 2), PkgState.NEW));
-        m.sortBySource();
-
-        assertEquals(
-                "PkgSourceCategory <source=repo1 (repo.com), #items=1>\n" +
-                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n",
-               getTree(m));
-    }
-
-    public void testSortBySource_DeletePackage() {
-        assertTrue(m.mAllPkgItems.isEmpty());
-
-        // Start with an installed package and its matching remote package
-        SdkSource src1 = new SdkRepoSource("http://repo.com/url", "repo1");
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 1), PkgState.INSTALLED));
-        m.sortBySource();
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 1), PkgState.NEW));
-        m.sortBySource();
-
-        assertEquals(
-                "PkgSourceCategory <source=repo1 (repo.com), #items=1>\n" +
-                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1>\n",
-               getTree(m));
-
-        // User now deletes the installed package.
-        m.mAllPkgItems.clear();
-        m.mAllPkgItems.add(new PkgItem(new MockEmptyPackage(src1, "type1", 1), PkgState.NEW));
-        m.sortBySource();
-
-        assertEquals(
-                "PkgSourceCategory <source=repo1 (repo.com), #items=1>\n" +
-                "-- <NEW, pkg:MockEmptyPackage 'type1' rev=1>\n",
-               getTree(m));
-    }
-
-    public void testSortBySource_CompleteUpdate() {
-        assertTrue(m.mAllPkgItems.isEmpty());
-
-        // Typical case is to have these 2 tools
-        SdkSource src1 = new SdkRepoSource("http://repo.com/url1", "repo1");
-        m.mAllPkgItems.add(new PkgItem(new MockToolPackage(src1, 10, 3),     PkgState.INSTALLED));
-        m.mAllPkgItems.add(new PkgItem(new MockPlatformToolPackage(src1, 3), PkgState.INSTALLED));
-
-        // Load a few extra packages
-        m.mAllPkgItems.add(
-            new PkgItem(new MockExtraPackage(src1, "carrier", "custom_rom", 1, 0), PkgState.NEW));
-
-        // We call sortBySource() 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.sortBySource();
-
-        assertEquals(
-                "PkgSourceCategory <source=repo1 (repo.com), #items=3>\n" +
-                "-- <INSTALLED, pkg:Android SDK Tools, revision 10>\n" +
-                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3>\n" +
-                "-- <NEW, pkg:Carrier Custom Rom package, revision 1>\n",
-               getTree(m));
-
-        // Source 2 only provides the addon, which is already installed so the source
-        // should be empty.
-        SdkSource src2 = new SdkRepoSource("http://repo.com/url2", "repo2");
-        m.mAllPkgItems.add(new PkgItem(
-                new MockExtraPackage(src2, "android", "usb_driver", 4, 3), PkgState.INSTALLED));
-        m.mAllPkgItems.add(new PkgItem(
-                new MockExtraPackage(src2, "android", "usb_driver", 4, 3), PkgState.NEW));
-
-        m.sortBySource();
-
-        assertEquals(
-                "PkgSourceCategory <source=repo1 (repo.com), #items=3>\n" +
-                "-- <INSTALLED, pkg:Android SDK Tools, revision 10>\n" +
-                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3>\n" +
-                "-- <NEW, pkg:Carrier Custom Rom package, revision 1>\n" +
-                "PkgSourceCategory <source=repo2 (repo.com), #items=1>\n" +
-                "-- <INSTALLED, pkg:Android USB Driver package, revision 4>\n",
-               getTree(m));
-
-        // When an update is available, it is still merged with the installed item
-        m.mAllPkgItems.add(new PkgItem(
-                new MockExtraPackage(src2, "android", "usb_driver", 6, 4), PkgState.NEW));
-
-        m.sortBySource();
-
-        assertEquals(
-                "PkgSourceCategory <source=repo1 (repo.com), #items=3>\n" +
-                "-- <INSTALLED, pkg:Android SDK Tools, revision 10>\n" +
-                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3>\n" +
-                "-- <NEW, pkg:Carrier Custom Rom package, revision 1>\n" +
-                "PkgSourceCategory <source=repo2 (repo.com), #items=1>\n" +
-                "-- <INSTALLED, pkg:Android USB Driver package, revision 4, updated by:Android USB Driver package, revision 6>\n" ,
-               getTree(m));
-
-
-        // Now add a few Platforms
-
-        SdkSource src3 = new SdkRepoSource("http://repo.com/url3", "repo3");
-        MockPlatformPackage p1;
-        MockPlatformPackage p2;
-        m.mAllPkgItems.add(new PkgItem(
-                p1 = new MockPlatformPackage(src2, 1, 2, 3), PkgState.INSTALLED));
-        m.mAllPkgItems.add(new PkgItem(
-                p2 = new MockPlatformPackage(src3, 2, 4, 3), PkgState.NEW));
-        m.mAllPkgItems.add(new PkgItem(
-                     new MockPlatformPackage(src2, 3, 6, 3), PkgState.INSTALLED));
-
-        m.sortBySource();
-
-        assertEquals(
-                "PkgSourceCategory <source=repo1 (repo.com), #items=3>\n" +
-                "-- <INSTALLED, pkg:Android SDK Tools, revision 10>\n" +
-                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3>\n" +
-                "-- <NEW, pkg:Carrier Custom Rom package, revision 1>\n" +
-                "PkgSourceCategory <source=repo2 (repo.com), #items=3>\n" +
-                "-- <INSTALLED, pkg:SDK Platform Android android-3, API 3, revision 6>\n"+
-                "-- <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 6>\n" +
-                "PkgSourceCategory <source=repo3 (repo.com), #items=1>\n" +
-                "-- <NEW, pkg:SDK Platform Android android-2, API 2, revision 4>\n",
-               getTree(m));
-
-        // Add a bunch of add-ons and sort them.
-        // Note that for source 4, the order is BCA since we order first by decreasing API
-        // and then by increasing add-on name.
-        SdkSource src4 = new SdkRepoSource("http://repo.com/url4", "repo4");
-        m.mAllPkgItems.add(new PkgItem(
-                new MockAddonPackage(src4, "addon C", p2, 9), PkgState.NEW));
-        m.mAllPkgItems.add(new PkgItem(
-                new MockAddonPackage(src4, "addon A", p1, 5), PkgState.INSTALLED));
-        m.mAllPkgItems.add(new PkgItem(
-                new MockAddonPackage(src4, "addon A", p1, 6), PkgState.NEW));
-        m.mAllPkgItems.add(new PkgItem(
-                new MockAddonPackage(src4, "addon B", p2, 7), PkgState.NEW));
-        // the rev 8 update will be ignored since there's a rev 9 coming after
-        m.mAllPkgItems.add(new PkgItem(
-                new MockAddonPackage(src4, "addon B", p2, 8), PkgState.NEW));
-        m.mAllPkgItems.add(new PkgItem(
-                new MockAddonPackage(src4, "addon B", p2, 9), PkgState.NEW));
-
-        m.sortBySource();
-
-        assertEquals(
-                "PkgSourceCategory <source=repo1 (repo.com), #items=3>\n" +
-                "-- <INSTALLED, pkg:Android SDK Tools, revision 10>\n" +
-                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3>\n" +
-                "-- <NEW, pkg:Carrier Custom Rom package, revision 1>\n" +
-                "PkgSourceCategory <source=repo2 (repo.com), #items=3>\n" +
-                "-- <INSTALLED, pkg:SDK Platform Android android-3, API 3, revision 6>\n"+
-                "-- <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 6>\n" +
-                "PkgSourceCategory <source=repo3 (repo.com), #items=1>\n" +
-                "-- <NEW, pkg:SDK Platform Android android-2, API 2, revision 4>\n" +
-                "PkgSourceCategory <source=repo4 (repo.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" +
-                "-- <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",
-               getTree(m));
-
-        // Now simulate a change of sorting algorithm: sort by source then by API again.
-        m.sortByApiLevel();
-        m.sortBySource();
-
-        assertEquals(
-                "PkgSourceCategory <source=repo1 (repo.com), #items=3>\n" +
-                "-- <INSTALLED, pkg:Android SDK Tools, revision 10>\n" +
-                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3>\n" +
-                "-- <NEW, pkg:Carrier Custom Rom package, revision 1>\n" +
-                "PkgSourceCategory <source=repo2 (repo.com), #items=3>\n" +
-                "-- <INSTALLED, pkg:SDK Platform Android android-3, API 3, revision 6>\n"+
-                "-- <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 6>\n" +
-                "PkgSourceCategory <source=repo3 (repo.com), #items=1>\n" +
-                "-- <NEW, pkg:SDK Platform Android android-2, API 2, revision 4>\n" +
-                "PkgSourceCategory <source=repo4 (repo.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" +
-                "-- <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",
-               getTree(m));
-    }
-
-    // ----
-
-    /**
-     * 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 &lt;description&gt;
-     *   -- &lt;PkgItem description&gt;
-     * </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();
-    }
-}