am 4c99836d: am 48257f16: am 31f5e7f4: Merge "Adopting the external update api to update SDK components" into studio-1.3-dev automerge: 22ef8ec automerge: 068e2ee

* commit '4c99836d2f525e236697448cc2f14eb1984b9592':
  Adopting the external update api to update SDK components
diff --git a/android/src/com/android/tools/idea/avdmanager/SystemImageList.java b/android/src/com/android/tools/idea/avdmanager/SystemImageList.java
index 5b39ee7..05faf7a 100644
--- a/android/src/com/android/tools/idea/avdmanager/SystemImageList.java
+++ b/android/src/com/android/tools/idea/avdmanager/SystemImageList.java
@@ -283,7 +283,7 @@
   @Nullable
   private List<SystemImageDescription> getRemoteImages() {
     List<SystemImageDescription> items = Lists.newArrayList();
-    Set<RemotePkgInfo> infos = mySdkState.getUpdates().getNewPkgs();
+    Set<RemotePkgInfo> infos = mySdkState.getPackages().getNewPkgs();
 
     if (infos.isEmpty()) {
       return null;
diff --git a/android/src/com/android/tools/idea/sdk/SdkLifecycleListener.java b/android/src/com/android/tools/idea/sdk/SdkLifecycleListener.java
deleted file mode 100755
index ce2d598..0000000
--- a/android/src/com/android/tools/idea/sdk/SdkLifecycleListener.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (C) 2014 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.tools.idea.sdk;
-
-import com.android.annotations.NonNull;
-import com.intellij.util.messages.Topic;
-import org.jetbrains.android.sdk.AndroidSdkData;
-
-public interface SdkLifecycleListener {
-  Topic<SdkLifecycleListener> TOPIC = Topic.create("Android SDK lifecycle notifications", SdkLifecycleListener.class);
-
-  void localSdkLoaded (@NonNull AndroidSdkData sdkData);
-  void remoteSdkLoaded(@NonNull AndroidSdkData sdkData);
-  void updatesComputed(@NonNull AndroidSdkData sdkData);
-
-  abstract class Adapter implements SdkLifecycleListener {
-    @Override
-    public void localSdkLoaded(@NonNull AndroidSdkData sdkData) {}
-
-    @Override
-    public void remoteSdkLoaded(@NonNull AndroidSdkData sdkData) {}
-
-    @Override
-    public void updatesComputed(@NonNull AndroidSdkData sdkData) {}
-  }
-}
diff --git a/android/src/com/android/tools/idea/sdk/SdkPackages.java b/android/src/com/android/tools/idea/sdk/SdkPackages.java
new file mode 100755
index 0000000..95e60e8
--- /dev/null
+++ b/android/src/com/android/tools/idea/sdk/SdkPackages.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2015 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.tools.idea.sdk;
+
+import com.android.annotations.NonNull;
+import com.android.sdklib.repository.descriptors.IPkgDesc;
+import com.android.sdklib.repository.descriptors.PkgType;
+import com.android.sdklib.repository.local.LocalPkgInfo;
+import com.android.tools.idea.sdk.remote.RemotePkgInfo;
+import com.android.tools.idea.sdk.remote.UpdatablePkgInfo;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.common.collect.TreeMultimap;
+
+import java.util.Set;
+
+
+/**
+ * Store of current local and remote packages, in convenient forms.
+ */
+public final class SdkPackages {
+  private final Set<UpdatablePkgInfo> myUpdatedPkgs = Sets.newTreeSet();
+  private final Set<RemotePkgInfo> myNewPkgs = Sets.newTreeSet();
+  private final long myTimestampMs;
+  private Set<UpdatablePkgInfo> myConsolidatedPkgs = Sets.newTreeSet();
+  private LocalPkgInfo[] myLocalPkgInfos = new LocalPkgInfo[0];
+  private Multimap<PkgType, RemotePkgInfo> myRemotePkgInfos = TreeMultimap.create();
+
+  SdkPackages() {
+    myTimestampMs = System.currentTimeMillis();
+  }
+
+  public SdkPackages(LocalPkgInfo[] localPkgs, Multimap<PkgType, RemotePkgInfo> remotePkgs) {
+    this();
+    setLocalPkgInfos(localPkgs);
+    setRemotePkgInfos(remotePkgs);
+  }
+
+  /**
+   * Returns the timestamp (in {@link System#currentTimeMillis()} time) when this object was created.
+   */
+  public long getTimestampMs() {
+    return myTimestampMs;
+  }
+
+  /**
+   * Returns the set of packages that have local updates available.
+   * Use {@link LocalPkgInfo#getUpdate()} to retrieve the computed updated candidate.
+   *
+   * @return A non-null, possibly empty Set of update candidates.
+   */
+  @NonNull
+  public Set<UpdatablePkgInfo> getUpdatedPkgs() {
+    return myUpdatedPkgs;
+  }
+
+  /**
+   * Returns the set of new remote packages that are not locally present
+   * and that the user could install.
+   *
+   * @return A non-null, possibly empty Set of new install candidates.
+   */
+  @NonNull
+  public Set<RemotePkgInfo> getNewPkgs() {
+    return myNewPkgs;
+  }
+
+  /**
+   * Returns a set of {@link UpdatablePackageInfo}s representing all known local and remote packages. Remote packages corresponding
+   * to local packages will be represented by a single item containing both the local and remote info..
+   */
+  @NonNull
+  public Set<UpdatablePkgInfo> getConsolidatedPkgs() {
+    return myConsolidatedPkgs;
+  }
+
+  @NonNull
+  public LocalPkgInfo[] getLocalPkgInfos() {
+    return myLocalPkgInfos;
+  }
+
+  public Multimap<PkgType, RemotePkgInfo> getRemotePkgInfos() {
+    return myRemotePkgInfos;
+  }
+
+  void setLocalPkgInfos(LocalPkgInfo[] packages) {
+    myLocalPkgInfos = packages;
+    computeUpdates();
+  }
+
+  void setRemotePkgInfos(Multimap<PkgType, RemotePkgInfo> packages) {
+    myRemotePkgInfos = packages;
+    computeUpdates();
+  }
+
+  private void computeUpdates() {
+    Set<UpdatablePkgInfo> newConsolidatedPkgs = Sets.newTreeSet();
+    UpdatablePkgInfo[] updatablePkgInfos = new UpdatablePkgInfo[myLocalPkgInfos.length];
+    for (int i = 0; i < myLocalPkgInfos.length; i++) {
+      updatablePkgInfos[i] = new UpdatablePkgInfo(myLocalPkgInfos[i]);
+    }
+    Set<RemotePkgInfo> updates = Sets.newTreeSet();
+
+    // Find updates to locally installed packages
+    for (UpdatablePkgInfo info : updatablePkgInfos) {
+      RemotePkgInfo update = findUpdate(info);
+      if (update != null) {
+        info.setRemote(update);
+        myUpdatedPkgs.add(info);
+        updates.add(update);
+      }
+      newConsolidatedPkgs.add(info);
+    }
+
+    // Find new packages not yet installed
+    nextRemote: for (RemotePkgInfo remote : myRemotePkgInfos.values()) {
+      if (updates.contains(remote)) {
+        // if package is already a known update, it's not new.
+        continue nextRemote;
+      }
+      IPkgDesc remoteDesc = remote.getPkgDesc();
+      for (UpdatablePkgInfo info : updatablePkgInfos) {
+        IPkgDesc localDesc = info.getLocalInfo().getDesc();
+        if (remoteDesc.compareTo(localDesc) == 0 || remoteDesc.isUpdateFor(localDesc)) {
+          // if package is same as an installed or is an update for an installed
+          // one, then it's not new.
+          continue nextRemote;
+        }
+      }
+
+      myNewPkgs.add(remote);
+      newConsolidatedPkgs.add(new UpdatablePkgInfo(remote));
+    }
+    myConsolidatedPkgs = newConsolidatedPkgs;
+  }
+
+  private RemotePkgInfo findUpdate(@NonNull UpdatablePkgInfo info) {
+    RemotePkgInfo currUpdatePkg = null;
+    IPkgDesc currUpdateDesc = null;
+    IPkgDesc localDesc = info.getLocalInfo().getDesc();
+
+    for (RemotePkgInfo remote: myRemotePkgInfos.get(localDesc.getType())) {
+      IPkgDesc remoteDesc = remote.getPkgDesc();
+      if ((currUpdateDesc == null && remoteDesc.isUpdateFor(localDesc)) ||
+          (currUpdateDesc != null && remoteDesc.isUpdateFor(currUpdateDesc))) {
+        currUpdatePkg = remote;
+        currUpdateDesc = remoteDesc;
+      }
+    }
+
+    return currUpdatePkg;
+  }
+}
diff --git a/android/src/com/android/tools/idea/sdk/SdkState.java b/android/src/com/android/tools/idea/sdk/SdkState.java
index d232b75..2927d86 100755
--- a/android/src/com/android/tools/idea/sdk/SdkState.java
+++ b/android/src/com/android/tools/idea/sdk/SdkState.java
@@ -19,28 +19,20 @@
 import com.android.annotations.Nullable;
 import com.android.annotations.concurrency.GuardedBy;
 import com.android.sdklib.repository.descriptors.PkgType;
-import com.android.sdklib.repository.local.LocalPkgInfo;
 import com.android.tools.idea.sdk.remote.RemotePkgInfo;
 import com.android.tools.idea.sdk.remote.RemoteSdk;
-import com.android.tools.idea.sdk.remote.Update;
-import com.android.tools.idea.sdk.remote.UpdateResult;
 import com.android.tools.idea.sdk.remote.internal.sources.SdkSources;
 import com.android.utils.ILogger;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
 import com.intellij.openapi.application.ApplicationManager;
-import com.intellij.openapi.application.ModalityState;
 import com.intellij.openapi.application.ex.ApplicationEx;
 import com.intellij.openapi.application.ex.ApplicationManagerEx;
 import com.intellij.openapi.diagnostic.Logger;
-import com.intellij.openapi.progress.PerformInBackgroundOption;
-import com.intellij.openapi.progress.ProgressIndicator;
-import com.intellij.openapi.progress.ProgressManager;
-import com.intellij.openapi.progress.Task;
+import com.intellij.openapi.progress.*;
 import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator;
 import com.intellij.openapi.progress.util.ProgressWindow;
 import com.intellij.reference.SoftReference;
-import com.intellij.util.concurrency.FutureResult;
 import com.intellij.util.concurrency.Semaphore;
 import org.jetbrains.android.sdk.AndroidSdkData;
 
@@ -48,25 +40,19 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
-import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
 
 public class SdkState {
 
-  /** Default expiration delay is 24 hours. */
-  public final static long DEFAULT_EXPIRATION_PERIOD_MS = 24 * 3600 * 1000;
+  public final static long DEFAULT_EXPIRATION_PERIOD_MS = TimeUnit.DAYS.toMillis(1);
 
   private static final Logger LOG = Logger.getInstance("#com.android.tools.idea.sdk.SdkState");
 
-  @GuardedBy(value = "sSdkStates")
-  private static final Set<SoftReference<SdkState>> sSdkStates = new HashSet<SoftReference<SdkState>>();
+  @GuardedBy(value = "sSdkStates") private static final Set<SoftReference<SdkState>> sSdkStates = new HashSet<SoftReference<SdkState>>();
 
-  @Nullable
-  private final AndroidSdkData mySdkData;
+  @Nullable private final AndroidSdkData mySdkData;
   private final RemoteSdk myRemoteSdk;
-  private LocalPkgInfo[] myLocalPkgInfos = new LocalPkgInfo[0];
-  private SdkSources mySources;
-  private UpdateResult myUpdates;
-  private Multimap<PkgType, RemotePkgInfo> myRemotePkgs;
+  private SdkPackages myPackages = null;
 
   private long myLastRefreshMs;
   private LoadTask myTask;
@@ -78,6 +64,16 @@
     myRemoteSdk = new RemoteSdk(new LogWrapper(Logger.getInstance(SdkState.class)));
   }
 
+  /**
+   * This shouldn't be needed unless interacting with the internals of the remote sdk.
+   *
+   * @return
+   */
+  @NonNull
+  public RemoteSdk getRemoteSdk() {
+    return myRemoteSdk;
+  }
+
   @NonNull
   public static SdkState getInstance(@Nullable AndroidSdkData sdkData) {
     synchronized (sSdkStates) {
@@ -106,17 +102,9 @@
   }
 
   @NonNull
-  public LocalPkgInfo[] getLocalPkgInfos() {
-    return myLocalPkgInfos;
-  }
-
-  public Multimap<PkgType, RemotePkgInfo> getRemotePkgInfos() {
-    return myRemotePkgs;
-  }
-
-  @Nullable
-  public UpdateResult getUpdates() {
-    return myUpdates;
+  public SdkPackages getPackages() {
+    assert myPackages != null;
+    return myPackages;
   }
 
   public boolean loadAsync(long timeoutMs,
@@ -152,8 +140,12 @@
 
       myTask = new LoadTask(canBeCancelled, onLocalComplete, onSuccess, onError, forceRefresh, sync);
     }
-    ProgressWindow progress = new BackgroundableProcessIndicator(myTask);
-    myTask.setProgress(progress);
+    if (!ApplicationManager.getApplication().isDispatchThread()) {
+      // not dispatch thread, assume progress is being handled elsewhere. Just run the task.
+      myTask.run(new EmptyProgressIndicator());
+      return true;
+    }
+
     ProgressManager.getInstance().run(myTask);
 
     return true;
@@ -180,6 +172,10 @@
     onSuccesses.add(complete);
     onErrors.add(complete);
     boolean result = load(timeoutMs, canBeCancelled, onLocalCompletes, onSuccesses, onErrors, forceRefresh, true);
+    if (!ApplicationManager.getApplication().isDispatchThread()) {
+      // not dispatch thread, assume progress is being handled elsewhere. We don't have to wait since load() ran in-thread.
+      return result;
+    }
     ProgressManager pm = ProgressManager.getInstance();
     ProgressIndicator indicator = pm.getProgressIndicator();
     indicator = indicator == null ? new ProgressWindow(false, false, null) : indicator;
@@ -228,7 +224,8 @@
     public void error(@Nullable Throwable t, @Nullable String msgFormat, Object... args) {
       if (msgFormat == null && t != null) {
         myIndicator.setText2(t.toString());
-      } else if (msgFormat != null) {
+      }
+      else if (msgFormat != null) {
         myIndicator.setText2(String.format(msgFormat, args));
       }
     }
@@ -287,14 +284,11 @@
 
     @Override
     public void run(@NonNull ProgressIndicator indicator) {
-      assert myProgress != null;
       boolean success = false;
       try {
         IndicatorLogger logger = new IndicatorLogger(indicator);
 
-        ApplicationEx app = ApplicationManagerEx.getApplicationEx();
-        SdkLifecycleListener notifier = app.getMessageBus().syncPublisher(SdkLifecycleListener.TOPIC);
-
+        myPackages = new SdkPackages();
         if (mySdkData != null) {
           // fetch local sdk
           indicator.setText("Loading local SDK...");
@@ -302,8 +296,7 @@
           if (myForceRefresh) {
             mySdkData.getLocalSdk().clearLocalPkg(PkgType.PKG_ALL);
           }
-          myLocalPkgInfos = mySdkData.getLocalSdk().getPkgsInfos(PkgType.PKG_ALL);
-          notifier.localSdkLoaded(mySdkData);
+          myPackages.setLocalPkgInfos(mySdkData.getLocalSdk().getPkgsInfos(PkgType.PKG_ALL));
           indicator.setFraction(0.25);
         }
         if (indicator.isCanceled()) {
@@ -315,11 +308,10 @@
           }
           myOnLocalCompletes.clear();
         }
-
         // fetch sdk repository sources.
         indicator.setText("Find SDK Repository...");
         indicator.setText2("");
-        mySources = myRemoteSdk.fetchSources(myForceRefresh ? 0 : RemoteSdk.DEFAULT_EXPIRATION_PERIOD_MS, logger);
+        SdkSources sources = myRemoteSdk.fetchSources(myForceRefresh ? 0 : RemoteSdk.DEFAULT_EXPIRATION_PERIOD_MS, logger);
         indicator.setFraction(0.50);
 
         if (indicator.isCanceled()) {
@@ -328,18 +320,15 @@
         // fetch remote sdk
         indicator.setText("Check SDK Repository...");
         indicator.setText2("");
-        myRemotePkgs = myRemoteSdk.fetch(mySources, logger);
-        notifier.remoteSdkLoaded(mySdkData);
+        Multimap<PkgType, RemotePkgInfo> remotes = myRemoteSdk.fetch(sources, logger);
+        // compute updates
+        indicator.setText("Compute SDK updates...");
         indicator.setFraction(0.75);
-
+        myPackages.setRemotePkgInfos(remotes);
         if (indicator.isCanceled()) {
           return;
         }
-        // compute updates
-        indicator.setText("Compute SDK updates...");
         indicator.setText2("");
-        myUpdates = Update.computeUpdates(myLocalPkgInfos, myRemotePkgs);
-        notifier.updatesComputed(mySdkData);
         indicator.setFraction(1.0);
 
         if (indicator.isCanceled()) {
diff --git a/android/src/com/android/tools/idea/sdk/remote/UpdatablePkgInfo.java b/android/src/com/android/tools/idea/sdk/remote/UpdatablePkgInfo.java
index ff082db..e25aa70 100644
--- a/android/src/com/android/tools/idea/sdk/remote/UpdatablePkgInfo.java
+++ b/android/src/com/android/tools/idea/sdk/remote/UpdatablePkgInfo.java
@@ -16,38 +16,71 @@
 package com.android.tools.idea.sdk.remote;
 
 import com.android.annotations.NonNull;
+import com.android.sdklib.repository.descriptors.IPkgDesc;
 import com.android.sdklib.repository.local.LocalPkgInfo;
 import org.jetbrains.annotations.Nullable;
 
 /**
- * Created by jbakermalone on 4/3/15.
+ * Represents a (revisionless) package, either local, remote, or both. If both a local and remote package are specified,
+ * they should represent exactly the same package, excepting the revision. That is, the result of installing the remote package
+ * should be (a possibly updated version of) the local package.
  */
 public class UpdatablePkgInfo implements Comparable<UpdatablePkgInfo> {
-  private final LocalPkgInfo myLocalInfo;
-  private RemotePkgInfo myUpdate;
+  private LocalPkgInfo myLocalInfo;
+  private RemotePkgInfo myRemoteInfo;
 
-  public UpdatablePkgInfo(@NonNull LocalPkgInfo localPkg, @Nullable RemotePkgInfo update) {
+  public UpdatablePkgInfo(@NonNull LocalPkgInfo localInfo) {
+    init(localInfo, null);
+  }
+
+  public UpdatablePkgInfo(@NonNull RemotePkgInfo remoteInfo) {
+    init(null, remoteInfo);
+  }
+
+  public UpdatablePkgInfo(@NonNull LocalPkgInfo localInfo, @NonNull RemotePkgInfo remoteInfo) {
+    init(localInfo, remoteInfo);
+  }
+
+  private void init(@Nullable LocalPkgInfo localPkg, @Nullable RemotePkgInfo remotePkg) {
+    assert localPkg != null || remotePkg != null;
     myLocalInfo = localPkg;
-    myUpdate = update;
+    myRemoteInfo = remotePkg;
   }
 
-  public void setUpdate(@NonNull RemotePkgInfo update) {
-    assert myUpdate == null;
-    myUpdate = update;
+  public void setRemote(@NonNull RemotePkgInfo remote) {
+    assert myRemoteInfo == null;
+    myRemoteInfo = remote;
   }
 
-  @NonNull
+  @Nullable
   public LocalPkgInfo getLocalInfo() {
     return myLocalInfo;
   }
 
   @Nullable
-  public RemotePkgInfo getUpdate() {
-    return myUpdate;
+  public RemotePkgInfo getRemote() {
+    return myRemoteInfo;
+  }
+
+  public boolean hasRemote() {
+    return myRemoteInfo != null;
+  }
+
+  public boolean hasLocal() {
+    return myLocalInfo != null;
   }
 
   @Override
   public int compareTo(UpdatablePkgInfo o) {
-    return getLocalInfo().compareTo(o.getLocalInfo());
+    return getPkgDesc().compareTo(o.getPkgDesc());
+  }
+
+  public IPkgDesc getPkgDesc() {
+    return myLocalInfo == null ? myRemoteInfo.getPkgDesc() : myLocalInfo.getDesc();
+  }
+
+  public boolean isUpdate() {
+    return myLocalInfo != null && myRemoteInfo != null &&
+           myRemoteInfo.getPkgDesc().getPreciseRevision().compareTo(myLocalInfo.getDesc().getPreciseRevision()) > 0;
   }
 }
diff --git a/android/src/com/android/tools/idea/sdk/remote/Update.java b/android/src/com/android/tools/idea/sdk/remote/Update.java
deleted file mode 100755
index bc62130..0000000
--- a/android/src/com/android/tools/idea/sdk/remote/Update.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright (C) 2015 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.tools.idea.sdk.remote;
-
-import com.android.annotations.NonNull;
-import com.android.sdklib.repository.descriptors.IPkgDesc;
-import com.android.sdklib.repository.descriptors.PkgType;
-import com.android.sdklib.repository.local.LocalPkgInfo;
-import com.google.common.collect.Multimap;
-import com.google.common.collect.Sets;
-
-import java.util.Set;
-
-
-/**
- * Helper methods to compute updates available for local packages.
- */
-public abstract class Update {
-
-  public static UpdateResult computeUpdates(@NonNull LocalPkgInfo[] localPkgs,
-                                            @NonNull Multimap<PkgType, RemotePkgInfo> remotePkgs) {
-    UpdatablePkgInfo[] updatablePkgInfos = new UpdatablePkgInfo[localPkgs.length];
-    for (int i = 0; i < localPkgs.length; i++) {
-      updatablePkgInfos[i] = new UpdatablePkgInfo(localPkgs[i], null);
-    }
-    UpdateResult result = new UpdateResult();
-    Set<RemotePkgInfo> updates = Sets.newTreeSet();
-
-    // Find updates to locally installed packages
-    for (UpdatablePkgInfo info : updatablePkgInfos) {
-      RemotePkgInfo update = findUpdate(info, remotePkgs, result);
-      if (update != null) {
-        info.setUpdate(update);
-        updates.add(update);
-      }
-    }
-
-    // Find new packages not yet installed
-    nextRemote: for (RemotePkgInfo remote : remotePkgs.values()) {
-      if (updates.contains(remote)) {
-        // if package is already a known update, it's not new.
-        continue nextRemote;
-      }
-      IPkgDesc remoteDesc = remote.getPkgDesc();
-      for (UpdatablePkgInfo info : updatablePkgInfos) {
-        IPkgDesc localDesc = info.getLocalInfo().getDesc();
-        if (remoteDesc.compareTo(localDesc) == 0 || remoteDesc.isUpdateFor(localDesc)) {
-          // if package is same as an installed or is an update for an installed
-          // one, then it's not new.
-          continue nextRemote;
-        }
-      }
-
-      result.addNewPkgs(remote);
-    }
-
-    return result;
-  }
-
-  private static RemotePkgInfo findUpdate(@NonNull UpdatablePkgInfo info,
-                                          @NonNull Multimap<PkgType, RemotePkgInfo> remotePkgs,
-                                          @NonNull UpdateResult result) {
-    RemotePkgInfo currUpdatePkg = null;
-    IPkgDesc currUpdateDesc = null;
-    IPkgDesc localDesc = info.getLocalInfo().getDesc();
-
-    for (RemotePkgInfo remote: remotePkgs.get(localDesc.getType())) {
-      IPkgDesc remoteDesc = remote.getPkgDesc();
-      if ((currUpdateDesc == null && remoteDesc.isUpdateFor(localDesc)) ||
-          (currUpdateDesc != null && remoteDesc.isUpdateFor(currUpdateDesc))) {
-        currUpdatePkg = remote;
-        currUpdateDesc = remoteDesc;
-      }
-    }
-
-    if (currUpdatePkg != null) {
-      result.addUpdatedPkgs(info);
-    }
-
-    return currUpdatePkg;
-  }
-
-}
diff --git a/android/src/com/android/tools/idea/sdk/remote/UpdateResult.java b/android/src/com/android/tools/idea/sdk/remote/UpdateResult.java
deleted file mode 100755
index b6ab148..0000000
--- a/android/src/com/android/tools/idea/sdk/remote/UpdateResult.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (C) 2015 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.tools.idea.sdk.remote;
-
-import com.android.annotations.NonNull;
-import com.android.sdklib.repository.local.LocalPkgInfo;
-import com.google.common.collect.Sets;
-
-import java.util.Set;
-
-
-/**
- * Results from {@link Update#computeUpdates(LocalPkgInfo[], com.google.common.collect.Multimap)}.
- */
-public final class UpdateResult {
-  private final Set<UpdatablePkgInfo> mUpdatedPkgs = Sets.newTreeSet();
-  private final Set<RemotePkgInfo> mNewPkgs = Sets.newTreeSet();
-  private final long mTimestampMs;
-
-  public UpdateResult() {
-    mTimestampMs = System.currentTimeMillis();
-  }
-
-  /**
-   * Returns the timestamp (in {@link System#currentTimeMillis()} time) when this object was created.
-   */
-  public long getTimestampMs() {
-    return mTimestampMs;
-  }
-
-  /**
-   * Returns the set of packages that have local updates available.
-   * Use {@link LocalPkgInfo#getUpdate()} to retrieve the computed updated candidate.
-   *
-   * @return A non-null, possibly empty list of update candidates.
-   */
-  @NonNull
-  public Set<UpdatablePkgInfo> getUpdatedPkgs() {
-    return mUpdatedPkgs;
-  }
-
-  /**
-   * Returns the set of new remote packages that are not locally present
-   * and that the user could install.
-   *
-   * @return A non-null, possibly empty list of new install candidates.
-   */
-  @NonNull
-  public Set<RemotePkgInfo> getNewPkgs() {
-    return mNewPkgs;
-  }
-
-  /**
-   * Add a package to the set of packages with available updates.
-   *
-   * @param pkgInfo The {@link LocalPkgInfo} which has an available update.
-   */
-  void addUpdatedPkgs(@NonNull UpdatablePkgInfo pkgInfo) {
-    mUpdatedPkgs.add(pkgInfo);
-  }
-
-  /**
-   * Add a package to the set of new remote packages that are not locally present
-   * and that the user could install.
-   *
-   * @param pkgInfo The {@link RemotePkgInfo} which has an available update.
-   */
-  void addNewPkgs(@NonNull RemotePkgInfo pkgInfo) {
-    mNewPkgs.add(pkgInfo);
-  }
-}
diff --git a/android/src/com/android/tools/idea/sdk/remote/internal/archives/Archive.java b/android/src/com/android/tools/idea/sdk/remote/internal/archives/Archive.java
index 0d34b5b..2519105 100755
--- a/android/src/com/android/tools/idea/sdk/remote/internal/archives/Archive.java
+++ b/android/src/com/android/tools/idea/sdk/remote/internal/archives/Archive.java
@@ -56,7 +56,7 @@
    * @param checksum   The expected checksum string of the archive. Currently only the
    *                   {@link ChecksumType#SHA1} format is supported.
    */
-  public Archive(@Nullable RemotePkgInfo pkg, @Nullable ArchFilter archFilter, @Nullable String url, long size, @NonNull String checksum) {
+  public Archive(@NonNull RemotePkgInfo pkg, @Nullable ArchFilter archFilter, @Nullable String url, long size, @NonNull String checksum) {
     mPackage = pkg;
     mArchFilter = archFilter != null ? archFilter : new ArchFilter(null);
     mUrl = url == null ? null : url.trim();
@@ -76,7 +76,7 @@
    * Returns the package that created and owns this archive.
    * It should generally not be null.
    */
-  @Nullable
+  @NonNull
   public RemotePkgInfo getParentPackage() {
     return mPackage;
   }
diff --git a/android/src/com/android/tools/idea/sdk/remote/internal/sources/SdkSources.java b/android/src/com/android/tools/idea/sdk/remote/internal/sources/SdkSources.java
index 34a9401..7f90887 100755
--- a/android/src/com/android/tools/idea/sdk/remote/internal/sources/SdkSources.java
+++ b/android/src/com/android/tools/idea/sdk/remote/internal/sources/SdkSources.java
@@ -16,20 +16,19 @@
 
 package com.android.tools.idea.sdk.remote.internal.sources;
 
+import com.android.annotations.concurrency.GuardedBy;
 import com.android.prefs.AndroidLocation;
 import com.android.prefs.AndroidLocation.AndroidLocationException;
 import com.android.sdklib.repository.SdkSysImgConstants;
 import com.android.utils.ILogger;
+import com.google.common.collect.Lists;
 
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.EnumMap;
-import java.util.Iterator;
+import java.util.*;
 import java.util.Map.Entry;
-import java.util.Properties;
 
 /**
  * A list of sdk-repository and sdk-addon sources, sorted by {@link SdkSourceCategory}.
@@ -39,10 +38,12 @@
   private static final String KEY_COUNT = "count";
 
   private static final String KEY_SRC = "src";
+  private static final String KEY_DISPLAY = "disp";
 
   private static final String SRC_FILENAME = "repositories.cfg"; //$NON-NLS-1$
 
-  private final EnumMap<SdkSourceCategory, ArrayList<SdkSource>> mSources =
+  @GuardedBy("itself")
+  private final EnumMap<SdkSourceCategory, ArrayList<SdkSource>> mySources =
     new EnumMap<SdkSourceCategory, ArrayList<SdkSource>>(SdkSourceCategory.class);
 
   public SdkSources() {
@@ -57,11 +58,11 @@
    * at the end, not for every single addition.
    */
   public void add(SdkSourceCategory category, SdkSource source) {
-    synchronized (mSources) {
-      ArrayList<SdkSource> list = mSources.get(category);
+    synchronized (mySources) {
+      ArrayList<SdkSource> list = mySources.get(category);
       if (list == null) {
         list = new ArrayList<SdkSource>();
-        mSources.put(category, list);
+        mySources.put(category, list);
       }
 
       list.add(source);
@@ -69,14 +70,28 @@
   }
 
   /**
+   * Replaces the current collection of sources corresponding to a particular category with the given collection.
+   * <p/>
+   * Implementation detail: {@link SdkSources} doesn't invoke {@link #notifyChangeListeners()}
+   * directly. Callers who use {@code set()} are responsible for notifying the listeners once
+   * they are done modifying the sources list. The intent is to notify the listeners only once
+   * at the end, not for every single addition.
+   */
+  public void set(SdkSourceCategory category, Collection<SdkSource> sources) {
+    synchronized (mySources) {
+      mySources.put(category, Lists.newArrayList(sources));
+    }
+  }
+
+  /**
    * Removes a source from the Sources list.
    * <p/>
    * Callers who remove entries are responsible for notifying the listeners using
    * {@link #notifyChangeListeners()} once they are done modifying the sources list.
    */
   public void remove(SdkSource source) {
-    synchronized (mSources) {
-      Iterator<Entry<SdkSourceCategory, ArrayList<SdkSource>>> it = mSources.entrySet().iterator();
+    synchronized (mySources) {
+      Iterator<Entry<SdkSourceCategory, ArrayList<SdkSource>>> it = mySources.entrySet().iterator();
       while (it.hasNext()) {
         Entry<SdkSourceCategory, ArrayList<SdkSource>> entry = it.next();
         ArrayList<SdkSource> list = entry.getValue();
@@ -98,8 +113,8 @@
    * {@link #notifyChangeListeners()} once they are done modifying the sources list.
    */
   public void removeAll(SdkSourceCategory category) {
-    synchronized (mSources) {
-      mSources.remove(category);
+    synchronized (mySources) {
+      mySources.remove(category);
     }
   }
 
@@ -117,8 +132,8 @@
         cats.add(cat);
       }
       else {
-        synchronized (mSources) {
-          ArrayList<SdkSource> list = mSources.get(cat);
+        synchronized (mySources) {
+          ArrayList<SdkSource> list = mySources.get(cat);
           if (list != null && !list.isEmpty()) {
             cats.add(cat);
           }
@@ -134,8 +149,8 @@
    * Might return an empty array, but never returns null.
    */
   public SdkSource[] getSources(SdkSourceCategory category) {
-    synchronized (mSources) {
-      ArrayList<SdkSource> list = mSources.get(category);
+    synchronized (mySources) {
+      ArrayList<SdkSource> list = mySources.get(category);
       if (list == null) {
         return new SdkSource[0];
       }
@@ -149,8 +164,8 @@
    * Returns true if there are sources for the given category.
    */
   public boolean hasSources(SdkSourceCategory category) {
-    synchronized (mSources) {
-      ArrayList<SdkSource> list = mSources.get(category);
+    synchronized (mySources) {
+      ArrayList<SdkSource> list = mySources.get(category);
       return list != null && !list.isEmpty();
     }
   }
@@ -159,17 +174,17 @@
    * Returns an array of the sources across all categories. This is never null.
    */
   public SdkSource[] getAllSources() {
-    synchronized (mSources) {
+    synchronized (mySources) {
       int n = 0;
 
-      for (ArrayList<SdkSource> list : mSources.values()) {
+      for (ArrayList<SdkSource> list : mySources.values()) {
         n += list.size();
       }
 
       SdkSource[] sources = new SdkSource[n];
 
       int i = 0;
-      for (ArrayList<SdkSource> list : mSources.values()) {
+      for (ArrayList<SdkSource> list : mySources.values()) {
         for (SdkSource source : list) {
           sources[i++] = source;
         }
@@ -186,8 +201,8 @@
    * the remote package list.
    */
   public void clearAllPackages() {
-    synchronized (mSources) {
-      for (ArrayList<SdkSource> list : mSources.values()) {
+    synchronized (mySources) {
+      for (ArrayList<SdkSource> list : mySources.values()) {
         for (SdkSource source : list) {
           source.clearPackages();
         }
@@ -205,8 +220,8 @@
    */
   public SdkSourceCategory getCategory(SdkSource source) {
     if (source != null) {
-      synchronized (mSources) {
-        for (Entry<SdkSourceCategory, ArrayList<SdkSource>> entry : mSources.entrySet()) {
+      synchronized (mySources) {
+        for (Entry<SdkSourceCategory, ArrayList<SdkSource>> entry : mySources.entrySet()) {
           if (entry.getValue().contains(source)) {
             return entry.getKey();
           }
@@ -227,8 +242,8 @@
    * The search is O(N), which should be acceptable on the expectedly small source list.
    */
   public boolean hasSourceUrl(SdkSource source) {
-    synchronized (mSources) {
-      for (ArrayList<SdkSource> list : mSources.values()) {
+    synchronized (mySources) {
+      for (ArrayList<SdkSource> list : mySources.values()) {
         for (SdkSource s : list) {
           if (s.equals(source)) {
             return true;
@@ -250,8 +265,8 @@
    * The search is O(N), which should be acceptable on the expectedly small source list.
    */
   public boolean hasSourceUrl(SdkSourceCategory category, SdkSource source) {
-    synchronized (mSources) {
-      ArrayList<SdkSource> list = mSources.get(category);
+    synchronized (mySources) {
+      ArrayList<SdkSource> list = mySources.get(category);
       if (list != null) {
         for (SdkSource s : list) {
           if (s.equals(source)) {
@@ -276,7 +291,7 @@
     // In most cases we do these operation from the UI thread so it's not really
     // that necessary. This is more a protection in case of someone calls this
     // from a worker thread by mistake.
-    synchronized (mSources) {
+    synchronized (mySources) {
       // Remove all existing user sources
       removeAll(SdkSourceCategory.USER_ADDONS);
 
@@ -295,6 +310,7 @@
 
           for (int i = 0; i < count; i++) {
             String url = props.getProperty(String.format("%s%02d", KEY_SRC, i));  //$NON-NLS-1$
+            String disp = props.getProperty(String.format("%s%02d", KEY_DISPLAY, i));  //$NON-NLS-1$
             if (url != null) {
               // FIXME: this code originally only dealt with add-on XML sources.
               // Now we'd like it to deal with system-image sources too, but we
@@ -307,10 +323,10 @@
               // the URI has been fetched.
               SdkSource s;
               if (url.endsWith(SdkSysImgConstants.URL_DEFAULT_FILENAME)) {
-                s = new SdkSysImgSource(url, null/*uiName*/);
+                s = new SdkSysImgSource(url, disp);
               }
               else {
-                s = new SdkAddonSource(url, null/*uiName*/);
+                s = new SdkAddonSource(url, disp);
               }
               if (!hasSourceUrl(s)) {
                 add(SdkSourceCategory.USER_ADDONS, s);
@@ -351,7 +367,7 @@
    */
   public void saveUserAddons(ILogger log) {
     // See the implementation detail note in loadUserAddons() about the synchronization.
-    synchronized (mSources) {
+    synchronized (mySources) {
       FileOutputStream fos = null;
       try {
         String folder = AndroidLocation.getFolder();
@@ -365,6 +381,10 @@
         for (SdkSource s : getSources(SdkSourceCategory.USER_ADDONS)) {
           props.setProperty(String.format("%s%02d", KEY_SRC, count), //$NON-NLS-1$
                             s.getUrl());
+          if (s.getUiName() != null) {
+            props.setProperty(String.format("%s%02d", KEY_DISPLAY, count), //$NON-NLS-1$
+                              s.getUiName());
+          }
           count++;
         }
         props.setProperty(KEY_COUNT, Integer.toString(count));
diff --git a/android/src/com/android/tools/idea/sdk/remote/internal/updater/PkgItem.java b/android/src/com/android/tools/idea/sdk/remote/internal/updater/PkgItem.java
deleted file mode 100755
index 02ea288..0000000
--- a/android/src/com/android/tools/idea/sdk/remote/internal/updater/PkgItem.java
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
- * Copyright (C) 2015 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.tools.idea.sdk.remote.internal.updater;
-
-import com.android.annotations.Nullable;
-import com.android.sdklib.AndroidVersion;
-import com.android.sdklib.repository.FullRevision;
-import com.android.sdklib.repository.local.LocalPkgInfo;
-import com.android.tools.idea.sdk.remote.RemotePkgInfo;
-import com.android.tools.idea.sdk.remote.internal.archives.Archive;
-import com.android.tools.idea.sdk.remote.internal.packages.IAndroidVersionProvider;
-import com.android.tools.idea.sdk.remote.internal.sources.SdkSource;
-import com.google.common.base.Objects;
-
-/**
- * A {@link PkgItem} represents one main {@link Package} combined with its state
- * and an optional update package.
- * <p/>
- * The main package is final and cannot change since it's what "defines" this PkgItem.
- * The state or update package can change later.
- */
-public class PkgItem implements Comparable<PkgItem> {
-  private final PkgState mState;
-  private final LocalPkgInfo mMainPkg;
-  private RemotePkgInfo mUpdatePkg;
-  private boolean mChecked;
-
-  /**
-   * The state of the a given {@link PkgItem}, that is the relationship between
-   * a given remote package and the local repository.
-   */
-  public enum PkgState {
-    // Implementation detail: if this is changed then PackageDiffLogic#STATES
-    // and PackageDiffLogic#processSource() need to be changed accordingly.
-
-    /**
-     * Package is locally installed and may or may not have an update.
-     */
-    INSTALLED,
-
-    /**
-     * There's a new package available on the remote site that isn't installed locally.
-     */
-    NEW
-  }
-
-  /**
-   * Create a new {@link PkgItem} for this main package.
-   * The main package is final and cannot change since it's what "defines" this PkgItem.
-   * The state or update package can change later.
-   */
-  public PkgItem(LocalPkgInfo mainPkg, PkgState state) {
-    mMainPkg = mainPkg;
-    mState = state;
-    assert mMainPkg != null;
-  }
-
-  public boolean isObsolete() {
-    return mMainPkg.getDesc().isObsolete();
-  }
-
-  public boolean isChecked() {
-    return mChecked;
-  }
-
-  public void setChecked(boolean checked) {
-    mChecked = checked;
-  }
-
-  public RemotePkgInfo getUpdatePkg() {
-    return mUpdatePkg;
-  }
-
-  public boolean hasUpdatePkg() {
-    return mUpdatePkg != null;
-  }
-
-  public String getName() {
-    return mMainPkg.getListDescription();
-  }
-
-  public FullRevision getRevision() {
-    return mMainPkg.getDesc().getFullRevision();
-  }
-
-  public LocalPkgInfo getMainPackage() {
-    return mMainPkg;
-  }
-
-  public PkgState getState() {
-    return mState;
-  }
-
-  @Nullable
-  public SdkSource getSource() {
-    return mUpdatePkg == null ? null : mUpdatePkg.getParentSource();
-  }
-
-  @Nullable
-  public AndroidVersion getAndroidVersion() {
-    return mMainPkg instanceof IAndroidVersionProvider ? ((IAndroidVersionProvider)mMainPkg).getAndroidVersion() : null;
-  }
-
-  @Nullable
-  public Archive[] getArchives() {
-    return mUpdatePkg == null ? null : mUpdatePkg.getArchives();
-  }
-
-  @Override
-  public int compareTo(PkgItem pkg) {
-    return getMainPackage().compareTo(pkg.getMainPackage());
-  }
-
-  /**
-   * Equality is defined as {@link #isSameItemAs(PkgItem)}: state, main package
-   * and update package must be the similar.
-   */
-  @Override
-  public boolean equals(Object obj) {
-    if (!(obj instanceof PkgItem)) {
-      return false;
-    }
-    PkgItem other = (PkgItem)obj;
-    return mMainPkg.equals(other.mMainPkg)
-      && Objects.equal(mUpdatePkg, other.mUpdatePkg)
-      && mState.equals(other.mState);
-  }
-
-  @Override
-  public int hashCode() {
-    final int prime = 31;
-    int result = 1;
-    result = prime * result + ((mState == null) ? 0 : mState.hashCode());
-    result = prime * result + ((mMainPkg == null) ? 0 : mMainPkg.hashCode());
-    result = prime * result + ((mUpdatePkg == null) ? 0 : mUpdatePkg.hashCode());
-    return result;
-  }
-
-  /**
-   * Check whether the 'pkg' argument is an update for this package.
-   * If it is, record it as an updating package.
-   * If there's already an updating package, only keep the most recent update.
-   * Returns true if it is update (even if there was already an update and this
-   * ended up not being the most recent), false if incompatible or not an update.
-   * <p/>
-   * This should only be used for installed packages.
-   */
-  public boolean mergeUpdate(RemotePkgInfo pkg) {
-    if (mUpdatePkg == pkg) {
-      return true;
-    }
-    if (pkg.canUpdate(mMainPkg) == RemotePkgInfo.UpdateInfo.UPDATE) {
-      if (mUpdatePkg == null) {
-        mUpdatePkg = pkg;
-      }
-      return true;
-    }
-
-    return false;
-  }
-
-  public void removeUpdate() {
-    mUpdatePkg = null;
-  }
-
-  /**
-   * Returns a string representation of this item, useful when debugging.
-   */
-  @Override
-  public String toString() {
-    StringBuilder sb = new StringBuilder();
-    sb.append('<');
-
-    if (mChecked) {
-      sb.append(" * "); //$NON-NLS-1$
-    }
-
-    sb.append(mState.toString());
-
-    if (mMainPkg != null) {
-      sb.append(", pkg:"); //$NON-NLS-1$
-      sb.append(mMainPkg.toString());
-    }
-
-    if (mUpdatePkg != null) {
-      sb.append(", updated by:"); //$NON-NLS-1$
-      sb.append(mUpdatePkg.toString());
-    }
-
-    sb.append('>');
-    return sb.toString();
-  }
-}
diff --git a/android/src/com/android/tools/idea/sdk/remote/internal/updater/UpdaterData.java b/android/src/com/android/tools/idea/sdk/remote/internal/updater/UpdaterData.java
index 9a29d7f..53deade 100755
--- a/android/src/com/android/tools/idea/sdk/remote/internal/updater/UpdaterData.java
+++ b/android/src/com/android/tools/idea/sdk/remote/internal/updater/UpdaterData.java
@@ -606,7 +606,7 @@
     SdkState state = SdkState.getInstance(AndroidSdkUtils.tryToChooseAndroidSdk());
     state.loadSynchronously(SdkState.DEFAULT_EXPIRATION_PERIOD_MS, false, null, null, null, false);
     List<ArchiveInfo> result = Lists.newArrayList();
-    for (RemotePkgInfo remote : state.getRemotePkgInfos().values()) {
+    for (RemotePkgInfo remote : state.getPackages().getRemotePkgInfos().values()) {
       if (includeAll || !remote.isObsolete()) {
         for (Archive archive : remote.getArchives()) {
           if (archive.isCompatible()) {
diff --git a/android/src/com/android/tools/idea/sdk/wizard/SmwOldApiDirectInstall.java b/android/src/com/android/tools/idea/sdk/wizard/SmwOldApiDirectInstall.java
index 4cb9afb..d50c865 100755
--- a/android/src/com/android/tools/idea/sdk/wizard/SmwOldApiDirectInstall.java
+++ b/android/src/com/android/tools/idea/sdk/wizard/SmwOldApiDirectInstall.java
@@ -26,6 +26,7 @@
 import com.android.utils.ILogger;
 import com.google.common.collect.Lists;
 import com.intellij.openapi.Disposable;
+import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.progress.PerformInBackgroundOption;
 import com.intellij.openapi.progress.ProgressIndicator;
@@ -109,6 +110,10 @@
     Runnable onSdkAvailable = new Runnable() {
       @Override
       public void run() {
+        if (!ApplicationManager.getApplication().isDispatchThread()) {
+          ApplicationManager.getApplication().invokeLater(this);
+          return;
+        }
         // TODO: since the local SDK has been parsed, this is now a good time
         // to filter requestedPackages to remove current installed packages.
         // That's because on Windows trying to update some of the packages in-place
diff --git a/android/src/com/android/tools/idea/welcome/install/ComponentInstaller.java b/android/src/com/android/tools/idea/welcome/install/ComponentInstaller.java
index 24d88da..f674c17 100644
--- a/android/src/com/android/tools/idea/welcome/install/ComponentInstaller.java
+++ b/android/src/com/android/tools/idea/welcome/install/ComponentInstaller.java
@@ -22,8 +22,7 @@
 import com.android.sdklib.repository.local.LocalSdk;
 import com.android.tools.idea.sdk.remote.RemotePkgInfo;
 import com.android.tools.idea.sdk.remote.UpdatablePkgInfo;
-import com.android.tools.idea.sdk.remote.Update;
-import com.android.tools.idea.sdk.remote.UpdateResult;
+import com.android.tools.idea.sdk.SdkPackages;
 import com.android.tools.idea.sdk.remote.internal.updater.SdkUpdaterNoWindow;
 import com.android.utils.ILogger;
 import com.android.utils.NullLogger;
@@ -69,7 +68,7 @@
   private Iterable<LocalPkgInfo> getOldPackages(Collection<LocalPkgInfo> installed) {
     if (myRemotePackages != null) {
       LocalPkgInfo[] packagesArray = ArrayUtil.toObjectArray(installed, LocalPkgInfo.class);
-      UpdateResult result = Update.computeUpdates(packagesArray, myRemotePackages);
+      SdkPackages result = new SdkPackages(packagesArray, myRemotePackages);
       return Iterables.transform(result.getUpdatedPkgs(), new Function<UpdatablePkgInfo, LocalPkgInfo>() {
         @Override
         public LocalPkgInfo apply(@Nullable UpdatablePkgInfo input) {
@@ -100,7 +99,7 @@
    *
    * @param manager SDK manager instance or <code>null</code> if this is a new install.
    * @param defaultUpdateAvailable If true, and if remote package information is not available, assume each package may have an update and
-        *                          try to reinstall. If false and remote package information not available, assume no updates are available.
+   *                               try to reinstall. If false and remote package information not available, assume no updates are available.
    */
   public ArrayList<String> getPackagesToInstall(@Nullable SdkManager manager, @NotNull Iterable<? extends InstallableComponent> components,
                                                 boolean defaultUpdateAvailable) {
diff --git a/android/src/com/android/tools/idea/welcome/wizard/AndroidStudioWelcomeScreenProvider.java b/android/src/com/android/tools/idea/welcome/wizard/AndroidStudioWelcomeScreenProvider.java
index 422443c..7559a74 100644
--- a/android/src/com/android/tools/idea/welcome/wizard/AndroidStudioWelcomeScreenProvider.java
+++ b/android/src/com/android/tools/idea/welcome/wizard/AndroidStudioWelcomeScreenProvider.java
@@ -160,7 +160,7 @@
 
     SdkState state = SdkState.getInstance(AndroidSdkUtils.tryToChooseAndroidSdk());
     state.loadSynchronously(SdkState.DEFAULT_EXPIRATION_PERIOD_MS, false, null, null, null, true);
-    return state.getRemotePkgInfos();
+    return state.getPackages().getRemotePkgInfos();
   }
 
   @Override
diff --git a/android/src/com/android/tools/idea/wizard/ConfigureFormFactorStep.java b/android/src/com/android/tools/idea/wizard/ConfigureFormFactorStep.java
index e4638d0..c46aa7e 100755
--- a/android/src/com/android/tools/idea/wizard/ConfigureFormFactorStep.java
+++ b/android/src/com/android/tools/idea/wizard/ConfigureFormFactorStep.java
@@ -261,8 +261,7 @@
         ApplicationManager.getApplication().invokeLater(new Runnable() {
           @Override
           public void run() {
-
-            List<RemotePkgInfo> packageList = Lists.newArrayList(state.getUpdates().getNewPkgs());
+            List<RemotePkgInfo> packageList = Lists.newArrayList(state.getPackages().getNewPkgs());
             Collections.sort(packageList);
             Iterator<RemotePkgInfo> result =
               Iterables.filter(packageList, FormFactorUtils.getMinSdkPackageFilter(formFactor, minSdkLevel)).iterator();
diff --git a/sdk-updates/sdk-updates.iml b/sdk-updates/sdk-updates.iml
new file mode 100644
index 0000000..d8cb223
--- /dev/null
+++ b/sdk-updates/sdk-updates.iml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4">
+  <component name="NewModuleRootManager" inherit-compiler-output="true">
+    <exclude-output />
+    <content url="file://$MODULE_DIR$">
+      <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+    <orderEntry type="module" module-name="platform-impl" />
+    <orderEntry type="module" module-name="android" />
+    <orderEntry type="module" module-name="lang-api" />
+  </component>
+</module>
\ No newline at end of file
diff --git a/sdk-updates/src/META-INF/plugin.xml b/sdk-updates/src/META-INF/plugin.xml
new file mode 100644
index 0000000..3408fd8
--- /dev/null
+++ b/sdk-updates/src/META-INF/plugin.xml
@@ -0,0 +1,38 @@
+<!--
+  ~ Copyright (C) 2015 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.
+  -->
+<idea-plugin version="2">
+  <id>com.android.tools.idea.updater</id>
+  <name>SDK Updater</name>
+  <version>1.0</version>
+  <vendor>JetBrains</vendor>
+
+  <description><![CDATA[
+      Android SDK Updater Plugin
+    ]]></description>
+
+  <!-- please see http://confluence.jetbrains.com/display/IDEADEV/Build+Number+Ranges for description -->
+  <idea-version since-build="131"/>
+
+  <depends>org.jetbrains.android</depends>
+
+  <application-components>
+    <component>
+      <implementation-class>com.android.tools.idea.updater.AndroidSdkUpdaterPlugin</implementation-class>
+      <interface-class>com.android.tools.idea.updater.AndroidSdkUpdaterPlugin</interface-class>
+    </component>
+  </application-components>
+
+</idea-plugin>
diff --git a/sdk-updates/src/com/android/tools/idea/updater/AndroidSdkUpdaterPlugin.java b/sdk-updates/src/com/android/tools/idea/updater/AndroidSdkUpdaterPlugin.java
new file mode 100644
index 0000000..e85405e
--- /dev/null
+++ b/sdk-updates/src/com/android/tools/idea/updater/AndroidSdkUpdaterPlugin.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2015 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.tools.idea.updater;
+
+import com.intellij.ide.externalComponents.ExternalComponentManagerImpl;
+import com.intellij.ide.externalComponents.UpdatableExternalComponent;
+import com.intellij.openapi.components.ApplicationComponent;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Plugin to set up the android sdk {@link UpdatableExternalComponent} and
+ * {@link com.android.tools.idea.updater.configure.SdkUpdaterConfigurable}.
+ */
+public class AndroidSdkUpdaterPlugin implements ApplicationComponent {
+  @Override
+  public void initComponent() {
+    ExternalComponentManagerImpl.getInstance().registerComponentSource(new SdkComponentSource());
+  }
+
+  @Override
+  public void disposeComponent() {
+    // nothing
+  }
+
+  @NotNull
+  @Override
+  public String getComponentName() {
+    return "Android Sdk Updater";
+  }
+}
diff --git a/sdk-updates/src/com/android/tools/idea/updater/SdkComponentSource.java b/sdk-updates/src/com/android/tools/idea/updater/SdkComponentSource.java
new file mode 100644
index 0000000..78afb69
--- /dev/null
+++ b/sdk-updates/src/com/android/tools/idea/updater/SdkComponentSource.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2015 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.tools.idea.updater;
+
+import com.android.sdklib.repository.descriptors.IPkgDesc;
+import com.android.sdklib.repository.local.LocalSdk;
+import com.android.tools.idea.sdk.SdkState;
+import com.android.tools.idea.sdk.remote.RemoteSdk;
+import com.android.tools.idea.sdk.remote.UpdatablePkgInfo;
+import com.android.tools.idea.sdk.remote.internal.sources.SdkSources;
+import com.android.tools.idea.sdk.wizard.SdkQuickfixWizard;
+import com.android.tools.idea.wizard.DialogWrapperHost;
+import com.android.utils.ILogger;
+import com.android.utils.StdLogger;
+import com.google.common.collect.Lists;
+import com.intellij.ide.externalComponents.ExternalComponentSource;
+import com.intellij.ide.externalComponents.UpdatableExternalComponent;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.ui.DialogWrapper;
+import org.jetbrains.android.sdk.AndroidSdkData;
+import org.jetbrains.android.sdk.AndroidSdkUtils;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * An {@link ExternalComponentSource} that retrieves information from the {@link LocalSdk} and {@link RemoteSdk} provided
+ * by the Android SDK.
+ */
+public class SdkComponentSource implements ExternalComponentSource {
+  SdkSources mySources;
+  SdkState mySdkState;
+  private static final ILogger ILOG = new StdLogger(StdLogger.Level.ERROR);
+
+  private void initIfNecessary() {
+    if (mySdkState != null) {
+      return;
+    }
+    AndroidSdkData data = AndroidSdkUtils.tryToChooseAndroidSdk();
+    assert data != null;
+    mySdkState = SdkState.getInstance(data);
+
+    mySources = mySdkState.getRemoteSdk().fetchSources(RemoteSdk.DEFAULT_EXPIRATION_PERIOD_MS, ILOG);
+  }
+
+  /**
+   * Install the given new versions of components using the {@link SdkQuickfixWizard}.
+   *
+   * @param request The components to install.
+   */
+  @Override
+  public void installUpdates(@NotNull Collection<UpdatableExternalComponent> request) {
+    final List<IPkgDesc> packages = Lists.newArrayList();
+    for (UpdatableExternalComponent p : request) {
+      packages.add((IPkgDesc)p.getKey());
+    }
+    SdkQuickfixWizard sdkQuickfixWizard =
+      new SdkQuickfixWizard(null, null, packages, new DialogWrapperHost(null, DialogWrapper.IdeModalityType.PROJECT));
+    sdkQuickfixWizard.init();
+    sdkQuickfixWizard.show();
+  }
+
+  /**
+   * Retrieves information on updates available from the {@link RemoteSdk}.
+   *
+   * @param indicator A {@code ProgressIndicator} that can be updated to show progress, or can be used to cancel the process.
+   * @return A collection of {@link UpdatablePackage}s corresponding to the currently installed Packages.
+   */
+  @NotNull
+  @Override
+  public Collection<UpdatableExternalComponent> getAvailableVersions(ProgressIndicator indicator) {
+    return getComponents(indicator, true);
+  }
+
+  /**
+   * Retrieves information on updates installed using the {@link LocalSdk}.
+   *
+   * @return A collection of {@link UpdatablePackage}s corresponding to the currently installed Packages.
+   */
+  @NotNull
+  @Override
+  public Collection<UpdatableExternalComponent> getCurrentVersions() {
+    return getComponents(null, false);
+  }
+
+  private Collection<UpdatableExternalComponent> getComponents(ProgressIndicator indicator, boolean remote) {
+    initIfNecessary();
+    List<UpdatableExternalComponent> result = Lists.newArrayList();
+    mySdkState.loadSynchronously(SdkState.DEFAULT_EXPIRATION_PERIOD_MS, true, null, null, null, true);
+    for (UpdatablePkgInfo info : mySdkState.getPackages().getConsolidatedPkgs()) {
+      if (remote) {
+        if (info.hasRemote()) {
+          result.add(new UpdatablePackage(info.getRemote().getPkgDesc()));
+        }
+      }
+      else {
+        if (info.hasLocal()) {
+          result.add(new UpdatablePackage(info.getLocalInfo().getDesc()));
+        }
+      }
+    }
+    return result;
+  }
+
+  @NotNull
+  @Override
+  public String getName() {
+    return "Android SDK";
+  }
+}
diff --git a/sdk-updates/src/com/android/tools/idea/updater/UpdatablePackage.java b/sdk-updates/src/com/android/tools/idea/updater/UpdatablePackage.java
new file mode 100644
index 0000000..4523b88
--- /dev/null
+++ b/sdk-updates/src/com/android/tools/idea/updater/UpdatablePackage.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2015 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.tools.idea.updater;
+
+import com.android.sdklib.repository.descriptors.IPkgDesc;
+import com.intellij.ide.externalComponents.UpdatableExternalComponent;
+
+/**
+ * An {@link UpdatableExternalComponent} that corresponds to an
+ * {@link IPkgDesc} for a local or remote package.
+ */
+public class UpdatablePackage implements UpdatableExternalComponent {
+  private IPkgDesc myPackage;
+
+  public UpdatablePackage(IPkgDesc p) {
+    myPackage = p;
+  }
+
+  @Override
+  public IPkgDesc getKey() {
+    return myPackage;
+  }
+
+  @Override
+  public boolean isUpdateFor(UpdatableExternalComponent c) {
+    if (c == null) {
+      return false;
+    }
+    Object otherKey = c.getKey();
+    if (!(otherKey instanceof IPkgDesc)) {
+      return false;
+    }
+    return myPackage.isUpdateFor((IPkgDesc)otherKey);
+  }
+
+  @Override
+  public String getName() {
+    return myPackage.getListDescription();
+  }
+
+  @Override
+  public String toString() {
+    return getName();
+  }
+}