Frameworks/base: New preload tool

Add a new preload tool based on hprof dumps. This means that only
a userdebug build (to adjust the pre-existing preloaded-classes
file) is required now, not a recompiled runtime.

Change-Id: Ib0c00de3b248e49fa8271cbace67c5d4a50170a1
diff --git a/tools/preload2/Android.mk b/tools/preload2/Android.mk
new file mode 100644
index 0000000..35d28fb
--- /dev/null
+++ b/tools/preload2/Android.mk
@@ -0,0 +1,32 @@
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under,src)
+LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
+
+# To connect to devices (and take hprof dumps).
+LOCAL_STATIC_JAVA_LIBRARIES := ddmlib-prebuilt
+
+# To process hprof dumps.
+LOCAL_STATIC_JAVA_LIBRARIES += perflib-prebuilt trove-prebuilt guavalib
+
+# For JDWP access we use the framework in the JDWP tests from Apache Harmony, for
+# convenience (and to not depend on internal JDK APIs).
+LOCAL_STATIC_JAVA_LIBRARIES += apache-harmony-jdwp-tests-host junit
+
+LOCAL_MODULE:= preload2
+
+include $(BUILD_HOST_JAVA_LIBRARY)
+
+# Copy the preload-tool shell script to the host's bin directory.
+include $(CLEAR_VARS)
+LOCAL_IS_HOST_MODULE := true
+LOCAL_MODULE_TAGS := optional
+LOCAL_MODULE_CLASS := EXECUTABLES
+LOCAL_MODULE := preload-tool
+include $(BUILD_SYSTEM)/base_rules.mk
+$(LOCAL_BUILT_MODULE): $(LOCAL_PATH)/preload-tool $(ACP)
+	@echo "Copy: $(PRIVATE_MODULE) ($@)"
+	$(copy-file-to-new-target)
+	$(hide) chmod 755 $@
diff --git a/tools/preload2/preload-tool b/tools/preload2/preload-tool
new file mode 100644
index 0000000..36dbc1c
--- /dev/null
+++ b/tools/preload2/preload-tool
@@ -0,0 +1,37 @@
+# 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.
+
+# This script is used on the host only. It uses a common subset
+# shell dialect that should work well. It is partially derived
+# from art/tools/art.
+
+function follow_links() {
+  if [ z"$BASH_SOURCE" != z ]; then
+    file="$BASH_SOURCE"
+  else
+    file="$0"
+  fi
+  while [ -h "$file" ]; do
+    # On Mac OS, readlink -f doesn't work.
+    file="$(readlink "$file")"
+  done
+  echo "$file"
+}
+
+
+PROG_NAME="$(follow_links)"
+PROG_DIR="$(cd "${PROG_NAME%/*}" ; pwd -P)"
+ANDROID_ROOT=$PROG_DIR/..
+
+java -cp $ANDROID_ROOT/framework/preload2.jar com.android.preload.Main
diff --git a/tools/preload2/src/com/android/preload/ClientUtils.java b/tools/preload2/src/com/android/preload/ClientUtils.java
new file mode 100644
index 0000000..71ef025
--- /dev/null
+++ b/tools/preload2/src/com/android/preload/ClientUtils.java
@@ -0,0 +1,224 @@
+/*
+ * 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.preload;
+
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.IDevice;
+
+/**
+ * Helper class for common communication with a Client (the ddms name for a running application).
+ *
+ * Instances take a default timeout parameter that's applied to all functions without explicit
+ * timeout. Timeouts are in milliseconds.
+ */
+public class ClientUtils {
+
+    private int defaultTimeout;
+
+    public ClientUtils() {
+        this(10000);
+    }
+
+    public ClientUtils(int defaultTimeout) {
+        this.defaultTimeout = defaultTimeout;
+    }
+
+    /**
+     * Shortcut for findClient with default timeout.
+     */
+    public Client findClient(IDevice device, String processName, int processPid) {
+        return findClient(device, processName, processPid, defaultTimeout);
+    }
+
+    /**
+     * Find the client with the given process name or process id. The name takes precedence over
+     * the process id (if valid). Stop looking after the given timeout.
+     *
+     * @param device The device to communicate with.
+     * @param processName The name of the process. May be null.
+     * @param processPid The pid of the process. Values less than or equal to zero are ignored.
+     * @param timeout The amount of milliseconds to wait, at most.
+     * @return The client, if found. Otherwise null.
+     */
+    public Client findClient(IDevice device, String processName, int processPid, int timeout) {
+        WaitForClient wfc = new WaitForClient(device, processName, processPid, timeout);
+        return wfc.get();
+    }
+
+    /**
+     * Shortcut for findAllClients with default timeout.
+     */
+    public Client[] findAllClients(IDevice device) {
+        return findAllClients(device, defaultTimeout);
+    }
+
+    /**
+     * Retrieve all clients known to the given device. Wait at most the given timeout.
+     *
+     * @param device The device to investigate.
+     * @param timeout The amount of milliseconds to wait, at most.
+     * @return An array of clients running on the given device. May be null depending on the
+     *         device implementation.
+     */
+    public Client[] findAllClients(IDevice device, int timeout) {
+        if (device.hasClients()) {
+            return device.getClients();
+        }
+        WaitForClients wfc = new WaitForClients(device, timeout);
+        return wfc.get();
+    }
+
+    private static class WaitForClient implements IClientChangeListener {
+
+        private IDevice device;
+        private String processName;
+        private int processPid;
+        private long timeout;
+        private Client result;
+
+        public WaitForClient(IDevice device, String processName, int processPid, long timeout) {
+            this.device = device;
+            this.processName = processName;
+            this.processPid = processPid;
+            this.timeout = timeout;
+            this.result = null;
+        }
+
+        public Client get() {
+            synchronized (this) {
+                AndroidDebugBridge.addClientChangeListener(this);
+
+                // Maybe it's already there.
+                if (result == null) {
+                    result = searchForClient(device);
+                }
+
+                if (result == null) {
+                    try {
+                        wait(timeout);
+                    } catch (InterruptedException e) {
+                        // Note: doesn't guard for spurious wakeup.
+                    }
+                }
+            }
+
+            AndroidDebugBridge.removeClientChangeListener(this);
+            return result;
+        }
+
+        private Client searchForClient(IDevice device) {
+            if (processName != null) {
+                Client tmp = device.getClient(processName);
+                if (tmp != null) {
+                    return tmp;
+                }
+            }
+            if (processPid > 0) {
+                String name = device.getClientName(processPid);
+                if (name != null && !name.isEmpty()) {
+                    Client tmp = device.getClient(name);
+                    if (tmp != null) {
+                        return tmp;
+                    }
+                }
+            }
+            if (processPid > 0) {
+                // Try manual search.
+                for (Client cl : device.getClients()) {
+                    if (cl.getClientData().getPid() == processPid
+                            && cl.getClientData().getClientDescription() != null) {
+                        return cl;
+                    }
+                }
+            }
+            return null;
+        }
+
+        private boolean isTargetClient(Client c) {
+            if (processPid > 0 && c.getClientData().getPid() == processPid) {
+                return true;
+            }
+            if (processName != null
+                    && processName.equals(c.getClientData().getClientDescription())) {
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public void clientChanged(Client arg0, int arg1) {
+            synchronized (this) {
+                if ((arg1 & Client.CHANGE_INFO) != 0 && (arg0.getDevice() == device)) {
+                    if (isTargetClient(arg0)) {
+                        result = arg0;
+                        notifyAll();
+                    }
+                }
+            }
+        }
+    }
+
+    private static class WaitForClients implements IClientChangeListener {
+
+        private IDevice device;
+        private long timeout;
+
+        public WaitForClients(IDevice device, long timeout) {
+            this.device = device;
+            this.timeout = timeout;
+        }
+
+        public Client[] get() {
+            synchronized (this) {
+                AndroidDebugBridge.addClientChangeListener(this);
+
+                if (device.hasClients()) {
+                    return device.getClients();
+                }
+
+                try {
+                    wait(timeout); // Note: doesn't guard for spurious wakeup.
+                } catch (InterruptedException exc) {
+                }
+
+                // We will be woken up when the first client data arrives. Sleep a little longer
+                // to give (hopefully all of) the rest of the clients a chance to become available.
+                // Note: a loop with timeout is brittle as well and complicated, just accept this
+                //       for now.
+                try {
+                    Thread.sleep(500);
+                } catch (InterruptedException exc) {
+                }
+            }
+
+            AndroidDebugBridge.removeClientChangeListener(this);
+
+            return device.getClients();
+        }
+
+        @Override
+        public void clientChanged(Client arg0, int arg1) {
+            synchronized (this) {
+                if ((arg1 & Client.CHANGE_INFO) != 0 && (arg0.getDevice() == device)) {
+                    notifyAll();
+                }
+            }
+        }
+    }
+}
diff --git a/tools/preload2/src/com/android/preload/DeviceUtils.java b/tools/preload2/src/com/android/preload/DeviceUtils.java
new file mode 100644
index 0000000..72de7b5
--- /dev/null
+++ b/tools/preload2/src/com/android/preload/DeviceUtils.java
@@ -0,0 +1,390 @@
+/*
+ * 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.preload;
+
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
+import com.android.preload.classdataretrieval.hprof.Hprof;
+import com.android.ddmlib.DdmPreferences;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.IShellOutputReceiver;
+
+import java.util.Date;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Helper class for some device routines.
+ */
+public class DeviceUtils {
+
+  public static void init(int debugPort) {
+    DdmPreferences.setSelectedDebugPort(debugPort);
+
+    Hprof.init();
+
+    AndroidDebugBridge.init(true);
+
+    AndroidDebugBridge.createBridge();
+  }
+
+  /**
+   * Run a command in the shell on the device.
+   */
+  public static void doShell(IDevice device, String cmdline, long timeout, TimeUnit unit) {
+    doShell(device, cmdline, new NullShellOutputReceiver(), timeout, unit);
+  }
+
+  /**
+   * Run a command in the shell on the device. Collects and returns the console output.
+   */
+  public static String doShellReturnString(IDevice device, String cmdline, long timeout,
+      TimeUnit unit) {
+    CollectStringShellOutputReceiver rec = new CollectStringShellOutputReceiver();
+    doShell(device, cmdline, rec, timeout, unit);
+    return rec.toString();
+  }
+
+  /**
+   * Run a command in the shell on the device, directing all output to the given receiver.
+   */
+  public static void doShell(IDevice device, String cmdline, IShellOutputReceiver receiver,
+      long timeout, TimeUnit unit) {
+    try {
+      device.executeShellCommand(cmdline, receiver, timeout, unit);
+    } catch (Exception e) {
+      e.printStackTrace();
+    }
+  }
+
+  /**
+   * Run am start on the device.
+   */
+  public static void doAMStart(IDevice device, String name, String activity) {
+    doShell(device, "am start -n " + name + " /." + activity, 30, TimeUnit.SECONDS);
+  }
+
+  /**
+   * Find the device with the given serial. Give up after the given timeout (in milliseconds).
+   */
+  public static IDevice findDevice(String serial, int timeout) {
+    WaitForDevice wfd = new WaitForDevice(serial, timeout);
+    return wfd.get();
+  }
+
+  /**
+   * Get all devices ddms knows about. Wait at most for the given timeout.
+   */
+  public static IDevice[] findDevices(int timeout) {
+    WaitForDevice wfd = new WaitForDevice(null, timeout);
+    wfd.get();
+    return AndroidDebugBridge.getBridge().getDevices();
+  }
+
+  /**
+   * Return the build type of the given device. This is the value of the "ro.build.type"
+   * system property.
+   */
+  public static String getBuildType(IDevice device) {
+    try {
+      Future<String> buildType = device.getSystemProperty("ro.build.type");
+      return buildType.get(500, TimeUnit.MILLISECONDS);
+    } catch (Exception e) {
+    }
+    return null;
+  }
+
+  /**
+   * Check whether the given device has a pre-optimized boot image. More precisely, checks
+   * whether /system/framework/ * /boot.art exists.
+   */
+  public static boolean hasPrebuiltBootImage(IDevice device) {
+    String ret =
+        doShellReturnString(device, "ls /system/framework/*/boot.art", 500, TimeUnit.MILLISECONDS);
+
+    return !ret.contains("No such file or directory");
+  }
+
+  /**
+   * Remove files involved in a standard build that interfere with collecting data. This will
+   * remove /etc/preloaded-classes, which determines which classes are allocated already in the
+   * boot image. It also deletes any compiled boot image on the device. Then it restarts the
+   * device.
+   *
+   * This is a potentially long-running operation, as the boot after the deletion may take a while.
+   * The method will abort after the given timeout.
+   */
+  public static boolean removePreloaded(IDevice device, long preloadedWaitTimeInSeconds) {
+    String oldContent =
+        DeviceUtils.doShellReturnString(device, "cat /etc/preloaded-classes", 1, TimeUnit.SECONDS);
+    if (oldContent.trim().equals("")) {
+      System.out.println("Preloaded-classes already empty.");
+      return true;
+    }
+
+    // Stop the system server etc.
+    doShell(device, "stop", 100, TimeUnit.MILLISECONDS);
+
+    // Remount /system, delete /etc/preloaded-classes. It would be nice to use "adb remount,"
+    // but AndroidDebugBridge doesn't expose it.
+    doShell(device, "mount -o remount,rw /system", 500, TimeUnit.MILLISECONDS);
+    doShell(device, "rm /etc/preloaded-classes", 100, TimeUnit.MILLISECONDS);
+    // We do need an empty file.
+    doShell(device, "touch /etc/preloaded-classes", 100, TimeUnit.MILLISECONDS);
+
+    // Delete the files in the dalvik cache.
+    doShell(device, "rm /data/dalvik-cache/*/*boot.art", 500, TimeUnit.MILLISECONDS);
+
+    // We'll try to use dev.bootcomplete to know when the system server is back up. But stop
+    // doesn't reset it, so do it manually.
+    doShell(device, "setprop dev.bootcomplete \"0\"", 500, TimeUnit.MILLISECONDS);
+
+    // Start the system server.
+    doShell(device, "start", 100, TimeUnit.MILLISECONDS);
+
+    // Do a loop checking each second whether bootcomplete. Wait for at most the given
+    // threshold.
+    Date startDate = new Date();
+    for (;;) {
+      try {
+        Thread.sleep(1000);
+      } catch (InterruptedException e) {
+        // Ignore spurious wakeup.
+      }
+      // Check whether bootcomplete.
+      String ret =
+          doShellReturnString(device, "getprop dev.bootcomplete", 500, TimeUnit.MILLISECONDS);
+      if (ret.trim().equals("1")) {
+        break;
+      }
+      System.out.println("Still not booted: " + ret);
+
+      // Check whether we timed out. This is a simplistic check that doesn't take into account
+      // things like switches in time.
+      Date endDate = new Date();
+      long seconds =
+          TimeUnit.SECONDS.convert(endDate.getTime() - startDate.getTime(), TimeUnit.MILLISECONDS);
+      if (seconds > preloadedWaitTimeInSeconds) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  /**
+   * Enable method-tracing on device. The system should be restarted after this.
+   */
+  public static void enableTracing(IDevice device) {
+    // Disable selinux.
+    doShell(device, "setenforce 0", 100, TimeUnit.MILLISECONDS);
+
+    // Make the profile directory world-writable.
+    doShell(device, "chmod 777 /data/dalvik-cache/profiles", 100, TimeUnit.MILLISECONDS);
+
+    // Enable streaming method tracing with a small 1K buffer.
+    doShell(device, "setprop dalvik.vm.method-trace true", 100, TimeUnit.MILLISECONDS);
+    doShell(device, "setprop dalvik.vm.method-trace-file "
+                    + "/data/dalvik-cache/profiles/zygote.trace.bin", 100, TimeUnit.MILLISECONDS);
+    doShell(device, "setprop dalvik.vm.method-trace-file-siz 1024", 100, TimeUnit.MILLISECONDS);
+    doShell(device, "setprop dalvik.vm.method-trace-stream true", 100, TimeUnit.MILLISECONDS);
+  }
+
+  private static class NullShellOutputReceiver implements IShellOutputReceiver {
+    @Override
+    public boolean isCancelled() {
+      return false;
+    }
+
+    @Override
+    public void flush() {}
+
+    @Override
+    public void addOutput(byte[] arg0, int arg1, int arg2) {}
+  }
+
+  private static class CollectStringShellOutputReceiver implements IShellOutputReceiver {
+
+    private StringBuilder builder = new StringBuilder();
+
+    @Override
+    public String toString() {
+      String ret = builder.toString();
+      // Strip trailing newlines. They are especially ugly because adb uses DOS line endings.
+      while (ret.endsWith("\r") || ret.endsWith("\n")) {
+        ret = ret.substring(0, ret.length() - 1);
+      }
+      return ret;
+    }
+
+    @Override
+    public void addOutput(byte[] arg0, int arg1, int arg2) {
+      builder.append(new String(arg0, arg1, arg2));
+    }
+
+    @Override
+    public void flush() {}
+
+    @Override
+    public boolean isCancelled() {
+      return false;
+    }
+  }
+
+  private static class WaitForDevice {
+
+    private String serial;
+    private long timeout;
+    private IDevice device;
+
+    public WaitForDevice(String serial, long timeout) {
+      this.serial = serial;
+      this.timeout = timeout;
+      device = null;
+    }
+
+    public IDevice get() {
+      if (device == null) {
+          WaitForDeviceListener wfdl = new WaitForDeviceListener(serial);
+          synchronized (wfdl) {
+              AndroidDebugBridge.addDeviceChangeListener(wfdl);
+
+              // Check whether we already know about this device.
+              IDevice[] devices = AndroidDebugBridge.getBridge().getDevices();
+              if (serial != null) {
+                  for (IDevice d : devices) {
+                      if (serial.equals(d.getSerialNumber())) {
+                          // Only accept if there are clients already. Else wait for the callback informing
+                          // us that we now have clients.
+                          if (d.hasClients()) {
+                              device = d;
+                          }
+
+                          break;
+                      }
+                  }
+              } else {
+                  if (devices.length > 0) {
+                      device = devices[0];
+                  }
+              }
+
+              if (device == null) {
+                  try {
+                      wait(timeout);
+                  } catch (InterruptedException e) {
+                      // Ignore spurious wakeups.
+                  }
+                  device = wfdl.getDevice();
+              }
+
+              AndroidDebugBridge.removeDeviceChangeListener(wfdl);
+          }
+      }
+
+      if (device != null) {
+          // Wait for clients.
+          WaitForClientsListener wfcl = new WaitForClientsListener(device);
+          synchronized (wfcl) {
+              AndroidDebugBridge.addDeviceChangeListener(wfcl);
+
+              if (!device.hasClients()) {
+                  try {
+                      wait(timeout);
+                  } catch (InterruptedException e) {
+                      // Ignore spurious wakeups.
+                  }
+              }
+
+              AndroidDebugBridge.removeDeviceChangeListener(wfcl);
+          }
+      }
+
+      return device;
+    }
+
+    private static class WaitForDeviceListener implements IDeviceChangeListener {
+
+        private String serial;
+        private IDevice device;
+
+        public WaitForDeviceListener(String serial) {
+            this.serial = serial;
+        }
+
+        public IDevice getDevice() {
+            return device;
+        }
+
+        @Override
+        public void deviceChanged(IDevice arg0, int arg1) {
+            // We may get a device changed instead of connected. Handle like a connection.
+            deviceConnected(arg0);
+        }
+
+        @Override
+        public void deviceConnected(IDevice arg0) {
+            if (device != null) {
+                // Ignore updates.
+                return;
+            }
+
+            if (serial == null || serial.equals(arg0.getSerialNumber())) {
+                device = arg0;
+                synchronized (this) {
+                    notifyAll();
+                }
+            }
+        }
+
+        @Override
+        public void deviceDisconnected(IDevice arg0) {
+            // Ignore disconnects.
+        }
+
+    }
+
+    private static class WaitForClientsListener implements IDeviceChangeListener {
+
+        private IDevice myDevice;
+
+        public WaitForClientsListener(IDevice myDevice) {
+            this.myDevice = myDevice;
+        }
+
+        @Override
+        public void deviceChanged(IDevice arg0, int arg1) {
+            if (arg0 == myDevice && (arg1 & IDevice.CHANGE_CLIENT_LIST) != 0) {
+                // Got a client list, done here.
+                synchronized (this) {
+                    notifyAll();
+                }
+            }
+        }
+
+        @Override
+        public void deviceConnected(IDevice arg0) {
+        }
+
+        @Override
+        public void deviceDisconnected(IDevice arg0) {
+        }
+
+    }
+  }
+
+}
diff --git a/tools/preload2/src/com/android/preload/DumpData.java b/tools/preload2/src/com/android/preload/DumpData.java
new file mode 100644
index 0000000..d997224
--- /dev/null
+++ b/tools/preload2/src/com/android/preload/DumpData.java
@@ -0,0 +1,91 @@
+/*
+ * 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.preload;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Holds the collected data for a process.
+ */
+public class DumpData {
+    /**
+     * Name of the package (=application).
+     */
+    String packageName;
+
+    /**
+     * A map of class name to a string for the classloader. This may be a toString equivalent,
+     * or just a unique ID.
+     */
+    Map<String, String> dumpData;
+
+    /**
+     * The Date when this data was captured. Mostly for display purposes.
+     */
+    Date date;
+
+    /**
+     * A cached value for the number of boot classpath classes (classloader value in dumpData is
+     * null).
+     */
+    int bcpClasses;
+
+    public DumpData(String packageName, Map<String, String> dumpData, Date date) {
+        this.packageName = packageName;
+        this.dumpData = dumpData;
+        this.date = date;
+
+        countBootClassPath();
+    }
+
+    public String getPackageName() {
+        return packageName;
+    }
+
+    public Date getDate() {
+        return date;
+    }
+
+    public Map<String, String> getDumpData() {
+        return dumpData;
+    }
+
+    public void countBootClassPath() {
+        bcpClasses = 0;
+        for (Map.Entry<String, String> e : dumpData.entrySet()) {
+            if (e.getValue() == null) {
+                bcpClasses++;
+            }
+        }
+    }
+
+    // Return an inverted mapping.
+    public Map<String, Set<String>> invertData() {
+        Map<String, Set<String>> ret = new HashMap<>();
+        for (Map.Entry<String, String> e : dumpData.entrySet()) {
+            if (!ret.containsKey(e.getValue())) {
+                ret.put(e.getValue(), new HashSet<String>());
+            }
+            ret.get(e.getValue()).add(e.getKey());
+        }
+        return ret;
+    }
+}
\ No newline at end of file
diff --git a/tools/preload2/src/com/android/preload/DumpDataIO.java b/tools/preload2/src/com/android/preload/DumpDataIO.java
new file mode 100644
index 0000000..28625c5
--- /dev/null
+++ b/tools/preload2/src/com/android/preload/DumpDataIO.java
@@ -0,0 +1,141 @@
+/*
+ * 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.preload;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.DefaultHandler;
+
+import java.io.File;
+import java.io.FileReader;
+import java.text.DateFormat;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+/**
+ * Helper class for serialization and deserialization of a collection of DumpData objects to XML.
+ */
+public class DumpDataIO {
+
+  /**
+   * Serialize the given collection to an XML document. Returns the produced string.
+   */
+  public static String serialize(Collection<DumpData> data) {
+      // We'll do this by hand, constructing a DOM or similar is too complicated for our simple
+      // use case.
+
+      StringBuilder sb = new StringBuilder();
+      sb.append("<preloaded-classes-data>\n");
+
+      for (DumpData d : data) {
+          serialize(d, sb);
+      }
+
+      sb.append("</preloaded-classes-data>\n");
+      return sb.toString();
+  }
+
+  private static void serialize(DumpData d, StringBuilder sb) {
+      sb.append("<data package=\"" + d.packageName + "\" date=\"" +
+              DateFormat.getDateTimeInstance().format(d.date) +"\">\n");
+
+      for (Map.Entry<String, String> e : d.dumpData.entrySet()) {
+          sb.append("<class name=\"" + e.getKey() + "\" classloader=\"" + e.getValue() + "\"/>\n");
+      }
+
+      sb.append("</data>\n");
+  }
+
+  /**
+   * Load a collection of DumpData objects from the given file.
+   */
+  public static Collection<DumpData> deserialize(File f) throws Exception {
+      // Use SAX parsing. Our format is very simple. Don't do any schema validation or such.
+
+      SAXParserFactory spf = SAXParserFactory.newInstance();
+      spf.setNamespaceAware(false);
+      SAXParser saxParser = spf.newSAXParser();
+
+      XMLReader xmlReader = saxParser.getXMLReader();
+      DumpDataContentHandler ddch = new DumpDataContentHandler();
+      xmlReader.setContentHandler(ddch);
+      xmlReader.parse(new InputSource(new FileReader(f)));
+
+      return ddch.data;
+  }
+
+  private static class DumpDataContentHandler extends DefaultHandler {
+      Collection<DumpData> data = new LinkedList<DumpData>();
+      DumpData openData = null;
+
+      @Override
+      public void startElement(String uri, String localName, String qName, Attributes attributes)
+              throws SAXException {
+          if (qName.equals("data")) {
+              if (openData != null) {
+                  throw new IllegalStateException();
+              }
+              String pkg = attributes.getValue("package");
+              String dateString = attributes.getValue("date");
+
+              if (pkg == null || dateString == null) {
+                  throw new IllegalArgumentException();
+              }
+
+              try {
+                  Date date = DateFormat.getDateTimeInstance().parse(dateString);
+                  openData = new DumpData(pkg, new HashMap<String, String>(), date);
+              } catch (Exception e) {
+                  throw new RuntimeException(e);
+              }
+          } else if (qName.equals("class")) {
+              if (openData == null) {
+                  throw new IllegalStateException();
+              }
+              String className = attributes.getValue("name");
+              String classLoader = attributes.getValue("classloader");
+
+              if (className == null || classLoader == null) {
+                  throw new IllegalArgumentException();
+              }
+
+              openData.dumpData.put(className, classLoader.equals("null") ? null : classLoader);
+          }
+      }
+
+      @Override
+      public void endElement(String uri, String localName, String qName) throws SAXException {
+          if (qName.equals("data")) {
+              if (openData == null) {
+                  throw new IllegalStateException();
+              }
+              openData.countBootClassPath();
+
+              data.add(openData);
+              openData = null;
+          }
+      }
+  }
+}
diff --git a/tools/preload2/src/com/android/preload/DumpTableModel.java b/tools/preload2/src/com/android/preload/DumpTableModel.java
new file mode 100644
index 0000000..d97cbf0
--- /dev/null
+++ b/tools/preload2/src/com/android/preload/DumpTableModel.java
@@ -0,0 +1,93 @@
+/*
+ * 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.preload;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.table.AbstractTableModel;
+
+/**
+ * A table model for collected DumpData. This is both the internal storage as well as the model
+ * for display.
+ */
+public class DumpTableModel extends AbstractTableModel {
+
+    private List<DumpData> data = new ArrayList<DumpData>();
+
+    public void addData(DumpData d) {
+        data.add(d);
+        fireTableRowsInserted(data.size() - 1, data.size() - 1);
+    }
+
+    public void clear() {
+        int size = data.size();
+        if (size > 0) {
+            data.clear();
+            fireTableRowsDeleted(0, size - 1);
+        }
+    }
+
+    public List<DumpData> getData() {
+        return data;
+    }
+
+    @Override
+    public int getRowCount() {
+        return data.size();
+    }
+
+    @Override
+    public int getColumnCount() {
+        return 4;
+    }
+
+    @Override
+    public String getColumnName(int column) {
+        switch (column) {
+            case 0:
+                return "Package";
+            case 1:
+                return "Date";
+            case 2:
+                return "# All Classes";
+            case 3:
+                return "# Boot Classpath Classes";
+
+            default:
+                throw new IndexOutOfBoundsException(String.valueOf(column));
+        }
+    }
+
+    @Override
+    public Object getValueAt(int rowIndex, int columnIndex) {
+        DumpData d = data.get(rowIndex);
+        switch (columnIndex) {
+            case 0:
+                return d.packageName;
+            case 1:
+                return d.date;
+            case 2:
+                return d.dumpData.size();
+            case 3:
+                return d.bcpClasses;
+
+            default:
+                throw new IndexOutOfBoundsException(String.valueOf(columnIndex));
+        }
+    }
+}
\ No newline at end of file
diff --git a/tools/preload2/src/com/android/preload/Main.java b/tools/preload2/src/com/android/preload/Main.java
new file mode 100644
index 0000000..ca5b0e0
--- /dev/null
+++ b/tools/preload2/src/com/android/preload/Main.java
@@ -0,0 +1,242 @@
+/*
+ * 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.preload;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.IDevice;
+import com.android.preload.actions.ClearTableAction;
+import com.android.preload.actions.ComputeThresholdAction;
+import com.android.preload.actions.ComputeThresholdXAction;
+import com.android.preload.actions.DeviceSpecific;
+import com.android.preload.actions.ExportAction;
+import com.android.preload.actions.ImportAction;
+import com.android.preload.actions.ReloadListAction;
+import com.android.preload.actions.RunMonkeyAction;
+import com.android.preload.actions.ScanAllPackagesAction;
+import com.android.preload.actions.ScanPackageAction;
+import com.android.preload.actions.ShowDataAction;
+import com.android.preload.classdataretrieval.ClassDataRetriever;
+import com.android.preload.classdataretrieval.hprof.Hprof;
+import com.android.preload.classdataretrieval.jdwp.JDWPClassDataRetriever;
+import com.android.preload.ui.UI;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import javax.swing.Action;
+import javax.swing.DefaultListModel;
+
+public class Main {
+
+    /**
+     * Enable tracing mode. This is a work-in-progress to derive compiled-methods data, so it is
+     * off for now.
+     */
+    public final static boolean ENABLE_TRACING = false;
+
+    /**
+     * Ten-second timeout.
+     */
+    public final static int DEFAULT_TIMEOUT_MILLIS = 10 * 1000;
+
+    /**
+     * Hprof timeout. Two minutes.
+     */
+    public final static int HPROF_TIMEOUT_MILLIS = 120 * 1000;
+
+    private IDevice device;
+    private static ClientUtils clientUtils;
+
+    private DumpTableModel dataTableModel;
+    private DefaultListModel<Client> clientListModel;
+
+    private UI ui;
+
+    // Actions that need to be updated once a device is selected.
+    private Collection<DeviceSpecific> deviceSpecificActions;
+
+    // Current main instance.
+    private static Main top;
+    private static boolean useJdwpClassDataRetriever = false;
+
+    public final static String CLASS_PRELOAD_BLACKLIST = "android.app.AlarmManager$" + "|"
+            + "android.app.SearchManager$" + "|" + "android.os.FileObserver$" + "|"
+            + "com.android.server.PackageManagerService\\$AppDirObserver$" + "|" +
+
+
+            // Threads
+            "android.os.AsyncTask$" + "|" + "android.pim.ContactsAsyncHelper$" + "|"
+            + "android.webkit.WebViewClassic\\$1$" + "|" + "java.lang.ProcessManager$" + "|"
+            + "(.*\\$NoPreloadHolder$)";
+
+    /**
+     * @param args
+     */
+    public static void main(String[] args) {
+        Main m = new Main();
+        top = m;
+
+        m.startUp();
+    }
+
+    public Main() {
+        clientListModel = new DefaultListModel<Client>();
+        dataTableModel = new DumpTableModel();
+
+        clientUtils = new ClientUtils(DEFAULT_TIMEOUT_MILLIS);  // Client utils with 10s timeout.
+
+        List<Action> actions = new ArrayList<Action>();
+        actions.add(new ReloadListAction(clientUtils, null, clientListModel));
+        actions.add(new ClearTableAction(dataTableModel));
+        actions.add(new RunMonkeyAction(null, dataTableModel));
+        actions.add(new ScanPackageAction(clientUtils, null, dataTableModel));
+        actions.add(new ScanAllPackagesAction(clientUtils, null, dataTableModel));
+        actions.add(new ComputeThresholdAction("Compute preloaded-classes", dataTableModel, 2,
+                CLASS_PRELOAD_BLACKLIST));
+        actions.add(new ComputeThresholdAction("Compute compiled-classes", dataTableModel, 1,
+                null));
+        actions.add(new ComputeThresholdXAction("Compute(X)", dataTableModel,
+                CLASS_PRELOAD_BLACKLIST));
+        actions.add(new ShowDataAction(dataTableModel));
+        actions.add(new ImportAction(dataTableModel));
+        actions.add(new ExportAction(dataTableModel));
+
+        deviceSpecificActions = new ArrayList<DeviceSpecific>();
+        for (Action a : actions) {
+            if (a instanceof DeviceSpecific) {
+                deviceSpecificActions.add((DeviceSpecific)a);
+            }
+        }
+
+        ui = new UI(clientListModel, dataTableModel, actions);
+        ui.setVisible(true);
+    }
+
+    public static UI getUI() {
+        return top.ui;
+    }
+
+    public static ClassDataRetriever getClassDataRetriever() {
+        if (useJdwpClassDataRetriever) {
+            return new JDWPClassDataRetriever();
+        } else {
+            return new Hprof(HPROF_TIMEOUT_MILLIS);
+        }
+    }
+
+    public IDevice getDevice() {
+        return device;
+    }
+
+    public void setDevice(IDevice device) {
+        this.device = device;
+        for (DeviceSpecific ds : deviceSpecificActions) {
+            ds.setDevice(device);
+        }
+    }
+
+    public DefaultListModel<Client> getClientListModel() {
+        return clientListModel;
+    }
+
+    static class DeviceWrapper {
+        IDevice device;
+
+        public DeviceWrapper(IDevice d) {
+            device = d;
+        }
+
+        @Override
+        public String toString() {
+            return device.getName() + " (#" + device.getSerialNumber() + ")";
+        }
+    }
+
+    private void startUp() {
+        getUI().showWaitDialog();
+        initDevice();
+
+        // Load clients.
+        new ReloadListAction(clientUtils, getDevice(), clientListModel).run();
+
+        getUI().hideWaitDialog();
+    }
+
+    private void initDevice() {
+        DeviceUtils.init(DEFAULT_TIMEOUT_MILLIS);
+
+        IDevice devices[] = DeviceUtils.findDevices(DEFAULT_TIMEOUT_MILLIS);
+        if (devices == null || devices.length == 0) {
+            throw new RuntimeException("Could not find any devices...");
+        }
+
+        getUI().hideWaitDialog();
+
+        DeviceWrapper deviceWrappers[] = new DeviceWrapper[devices.length];
+        for (int i = 0; i < devices.length; i++) {
+            deviceWrappers[i] = new DeviceWrapper(devices[i]);
+        }
+
+        DeviceWrapper ret = Main.getUI().showChoiceDialog("Choose a device", "Choose device",
+                deviceWrappers);
+        if (ret != null) {
+            setDevice(ret.device);
+        } else {
+            System.exit(0);
+        }
+
+        boolean prepare = Main.getUI().showConfirmDialog("Prepare device?",
+                "Do you want to prepare the device? This is highly recommended.");
+        if (prepare) {
+            String buildType = DeviceUtils.getBuildType(device);
+            if (buildType == null || (!buildType.equals("userdebug") && !buildType.equals("eng"))) {
+                Main.getUI().showMessageDialog("Need a userdebug or eng build! (Found " + buildType
+                        + ")");
+                return;
+            }
+            if (DeviceUtils.hasPrebuiltBootImage(device)) {
+                Main.getUI().showMessageDialog("Cannot prepare a device with pre-optimized boot "
+                        + "image!");
+                return;
+            }
+
+            if (ENABLE_TRACING) {
+                DeviceUtils.enableTracing(device);
+            }
+
+            Main.getUI().showMessageDialog("The device will reboot. This will potentially take a "
+                    + "long time. Please be patient.");
+            if (!DeviceUtils.removePreloaded(device, 15 * 60) /* 15m timeout */) {
+                Main.getUI().showMessageDialog("Removing preloaded-classes failed unexpectedly!");
+            }
+        }
+    }
+
+    public static Map<String, String> findAndGetClassData(IDevice device, String packageName)
+            throws Exception {
+        Client client = clientUtils.findClient(device, packageName, -1);
+        if (client == null) {
+            throw new RuntimeException("Could not find client...");
+        }
+        System.out.println("Found client: " + client);
+
+        return getClassDataRetriever().getClassData(client);
+    }
+
+}
diff --git a/tools/preload2/src/com/android/preload/actions/AbstractThreadedAction.java b/tools/preload2/src/com/android/preload/actions/AbstractThreadedAction.java
new file mode 100644
index 0000000..fbf83d2
--- /dev/null
+++ b/tools/preload2/src/com/android/preload/actions/AbstractThreadedAction.java
@@ -0,0 +1,34 @@
+/*
+ * 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.preload.actions;
+
+import java.awt.event.ActionEvent;
+
+import javax.swing.AbstractAction;
+
+public abstract class AbstractThreadedAction extends AbstractAction implements Runnable {
+
+    protected AbstractThreadedAction(String title) {
+        super(title);
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        new Thread(this).start();
+    }
+
+}
diff --git a/tools/preload2/src/com/android/preload/actions/AbstractThreadedDeviceSpecificAction.java b/tools/preload2/src/com/android/preload/actions/AbstractThreadedDeviceSpecificAction.java
new file mode 100644
index 0000000..7906417
--- /dev/null
+++ b/tools/preload2/src/com/android/preload/actions/AbstractThreadedDeviceSpecificAction.java
@@ -0,0 +1,45 @@
+/*
+ * 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.preload.actions;
+
+import com.android.ddmlib.IDevice;
+
+import java.awt.event.ActionEvent;
+
+public abstract class AbstractThreadedDeviceSpecificAction extends AbstractThreadedAction
+        implements DeviceSpecific {
+
+    protected IDevice device;
+
+    protected AbstractThreadedDeviceSpecificAction(String title, IDevice device) {
+        super(title);
+        this.device = device;
+    }
+
+    @Override
+    public void setDevice(IDevice device) {
+        this.device = device;
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        if (device == null) {
+            return;
+        }
+        super.actionPerformed(e);
+    }
+}
diff --git a/tools/preload2/src/com/android/preload/actions/ClearTableAction.java b/tools/preload2/src/com/android/preload/actions/ClearTableAction.java
new file mode 100644
index 0000000..c0e4795
--- /dev/null
+++ b/tools/preload2/src/com/android/preload/actions/ClearTableAction.java
@@ -0,0 +1,37 @@
+/*
+ * 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.preload.actions;
+
+import com.android.preload.DumpTableModel;
+
+import java.awt.event.ActionEvent;
+
+import javax.swing.AbstractAction;
+
+public class ClearTableAction extends AbstractAction {
+    private final DumpTableModel dataTableModel;
+
+    public ClearTableAction(DumpTableModel dataTableModel) {
+        super("Clear");
+        this.dataTableModel = dataTableModel;
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        dataTableModel.clear();
+    }
+}
\ No newline at end of file
diff --git a/tools/preload2/src/com/android/preload/actions/ComputeThresholdAction.java b/tools/preload2/src/com/android/preload/actions/ComputeThresholdAction.java
new file mode 100644
index 0000000..b524716
--- /dev/null
+++ b/tools/preload2/src/com/android/preload/actions/ComputeThresholdAction.java
@@ -0,0 +1,151 @@
+/*
+ * 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.preload.actions;
+
+import com.android.preload.DumpData;
+import com.android.preload.DumpTableModel;
+import com.android.preload.Main;
+
+import java.awt.event.ActionEvent;
+import java.io.File;
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.regex.Pattern;
+
+import javax.swing.AbstractAction;
+import javax.swing.JFileChooser;
+
+/**
+ * Compute an intersection of classes from the given data. A class is in the intersection if it
+ * appears in at least the number of threshold given packages. An optional blacklist can be
+ * used to filter classes from the intersection.
+ */
+public class ComputeThresholdAction extends AbstractAction implements Runnable {
+    protected int threshold;
+    private Pattern blacklist;
+    private DumpTableModel dataTableModel;
+
+    /**
+     * Create an action with the given parameters. The blacklist is a regular expression
+     * that filters classes.
+     */
+    public ComputeThresholdAction(String name, DumpTableModel dataTableModel, int threshold,
+            String blacklist) {
+        super(name);
+        this.dataTableModel = dataTableModel;
+        this.threshold = threshold;
+        if (blacklist != null) {
+            this.blacklist = Pattern.compile(blacklist);
+        }
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        List<DumpData> data = dataTableModel.getData();
+        if (data.size() == 0) {
+            Main.getUI().showMessageDialog("No data available, please scan packages or run "
+                    + "monkeys.");
+            return;
+        }
+        if (data.size() == 1) {
+            Main.getUI().showMessageDialog("Cannot compute list from only one data set, please "
+                    + "scan packages or run monkeys.");
+            return;
+        }
+
+        new Thread(this).start();
+    }
+
+    @Override
+    public void run() {
+        Main.getUI().showWaitDialog();
+
+        Map<String, Set<String>> uses = new HashMap<String, Set<String>>();
+        for (DumpData d : dataTableModel.getData()) {
+            Main.getUI().updateWaitDialog("Merging " + d.getPackageName());
+            updateClassUse(d.getPackageName(), uses, getBootClassPathClasses(d.getDumpData()));
+        }
+
+        Main.getUI().updateWaitDialog("Computing thresholded set");
+        Set<String> result = fromThreshold(uses, blacklist, threshold);
+        Main.getUI().hideWaitDialog();
+
+        boolean ret = Main.getUI().showConfirmDialog("Computed a set with " + result.size()
+                + " classes, would you like to save to disk?", "Save?");
+        if (ret) {
+            JFileChooser jfc = new JFileChooser();
+            int ret2 = jfc.showSaveDialog(Main.getUI());
+            if (ret2 == JFileChooser.APPROVE_OPTION) {
+                File f = jfc.getSelectedFile();
+                saveSet(result, f);
+            }
+        }
+    }
+
+    private Set<String> fromThreshold(Map<String, Set<String>> classUses, Pattern blacklist,
+            int threshold) {
+        TreeSet<String> ret = new TreeSet<>(); // TreeSet so it's nicely ordered by name.
+
+        for (Map.Entry<String, Set<String>> e : classUses.entrySet()) {
+            if (e.getValue().size() >= threshold) {
+                if (blacklist == null || !blacklist.matcher(e.getKey()).matches()) {
+                    ret.add(e.getKey());
+                }
+            }
+        }
+
+        return ret;
+    }
+
+    private static void updateClassUse(String pkg, Map<String, Set<String>> classUses,
+            Set<String> classes) {
+        for (String className : classes) {
+            Set<String> old = classUses.get(className);
+            if (old == null) {
+                classUses.put(className, new HashSet<String>());
+            }
+            classUses.get(className).add(pkg);
+        }
+    }
+
+    private static Set<String> getBootClassPathClasses(Map<String, String> source) {
+        Set<String> ret = new HashSet<>();
+        for (Map.Entry<String, String> e : source.entrySet()) {
+            if (e.getValue() == null) {
+                ret.add(e.getKey());
+            }
+        }
+        return ret;
+    }
+
+    private static void saveSet(Set<String> result, File f) {
+        try {
+            PrintWriter out = new PrintWriter(f);
+            for (String s : result) {
+                out.println(s);
+            }
+            out.close();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+}
\ No newline at end of file
diff --git a/tools/preload2/src/com/android/preload/actions/ComputeThresholdXAction.java b/tools/preload2/src/com/android/preload/actions/ComputeThresholdXAction.java
new file mode 100644
index 0000000..3ec0a4c
--- /dev/null
+++ b/tools/preload2/src/com/android/preload/actions/ComputeThresholdXAction.java
@@ -0,0 +1,41 @@
+/*
+ * 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.preload.actions;
+
+import com.android.preload.DumpTableModel;
+import com.android.preload.Main;
+
+public class ComputeThresholdXAction extends ComputeThresholdAction {
+
+    public ComputeThresholdXAction(String name, DumpTableModel dataTableModel,
+            String blacklist) {
+        super(name, dataTableModel, 1, blacklist);
+    }
+
+    @Override
+    public void run() {
+        String value = Main.getUI().showInputDialog("Threshold?");
+
+        if (value != null) {
+            try {
+                threshold = Integer.parseInt(value);
+                super.run();
+            } catch (Exception exc) {
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tools/preload2/src/com/android/preload/actions/DeviceSpecific.java b/tools/preload2/src/com/android/preload/actions/DeviceSpecific.java
new file mode 100644
index 0000000..35a8f26
--- /dev/null
+++ b/tools/preload2/src/com/android/preload/actions/DeviceSpecific.java
@@ -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.
+ */
+
+package com.android.preload.actions;
+
+import com.android.ddmlib.IDevice;
+
+/**
+ * Marks an action as being device-specific. The user must set the device through the specified
+ * method if the device selection changes.
+ *
+ * Implementors must tolerate a null device (for example, with a no-op). This includes calling
+ * any methods before setDevice has been called.
+ */
+public interface DeviceSpecific {
+
+    /**
+     * Set the device that should be used. Note that there is no restriction on calling other
+     * methods of the implementor before a setDevice call. Neither is device guaranteed to be
+     * non-null.
+     *
+     * @param device The device to use going forward.
+     */
+    public void setDevice(IDevice device);
+}
diff --git a/tools/preload2/src/com/android/preload/actions/ExportAction.java b/tools/preload2/src/com/android/preload/actions/ExportAction.java
new file mode 100644
index 0000000..cb8b3df
--- /dev/null
+++ b/tools/preload2/src/com/android/preload/actions/ExportAction.java
@@ -0,0 +1,65 @@
+/*
+ * 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.preload.actions;
+
+import com.android.preload.DumpDataIO;
+import com.android.preload.DumpTableModel;
+import com.android.preload.Main;
+
+import java.awt.event.ActionEvent;
+import java.io.File;
+import java.io.PrintWriter;
+
+import javax.swing.AbstractAction;
+
+public class ExportAction extends AbstractAction implements Runnable {
+    private File lastSaveFile;
+    private DumpTableModel dataTableModel;
+
+    public ExportAction(DumpTableModel dataTableModel) {
+        super("Export data");
+        this.dataTableModel = dataTableModel;
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        lastSaveFile = Main.getUI().showSaveDialog();
+        if (lastSaveFile != null) {
+            new Thread(this).start();
+        }
+    }
+
+    @Override
+    public void run() {
+        Main.getUI().showWaitDialog();
+
+        String serialized = DumpDataIO.serialize(dataTableModel.getData());
+
+        if (serialized != null) {
+            try {
+                PrintWriter out = new PrintWriter(lastSaveFile);
+                out.println(serialized);
+                out.close();
+
+                Main.getUI().hideWaitDialog();
+            } catch (Exception e) {
+                Main.getUI().hideWaitDialog();
+                Main.getUI().showMessageDialog("Failed writing: " + e.getMessage());
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tools/preload2/src/com/android/preload/actions/ImportAction.java b/tools/preload2/src/com/android/preload/actions/ImportAction.java
new file mode 100644
index 0000000..5c19765
--- /dev/null
+++ b/tools/preload2/src/com/android/preload/actions/ImportAction.java
@@ -0,0 +1,68 @@
+/*
+ * 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.preload.actions;
+
+import com.android.preload.DumpData;
+import com.android.preload.DumpDataIO;
+import com.android.preload.DumpTableModel;
+import com.android.preload.Main;
+
+import java.awt.event.ActionEvent;
+import java.io.File;
+import java.util.Collection;
+
+import javax.swing.AbstractAction;
+
+public class ImportAction extends AbstractAction implements Runnable {
+    private File[] lastOpenFiles;
+    private DumpTableModel dataTableModel;
+
+    public ImportAction(DumpTableModel dataTableModel) {
+        super("Import data");
+        this.dataTableModel = dataTableModel;
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        lastOpenFiles = Main.getUI().showOpenDialog(true);
+        if (lastOpenFiles != null) {
+            new Thread(this).start();
+        }
+    }
+
+    @Override
+    public void run() {
+        Main.getUI().showWaitDialog();
+
+        try {
+            for (File f : lastOpenFiles) {
+                try {
+                    Collection<DumpData> data = DumpDataIO.deserialize(f);
+
+                    for (DumpData d : data) {
+                        dataTableModel.addData(d);
+                    }
+                } catch (Exception e) {
+                    Main.getUI().showMessageDialog("Failed reading: " + e.getMessage());
+                }
+            }
+        } finally {
+            Main.getUI().hideWaitDialog();
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/tools/preload2/src/com/android/preload/actions/ReloadListAction.java b/tools/preload2/src/com/android/preload/actions/ReloadListAction.java
new file mode 100644
index 0000000..29f0557
--- /dev/null
+++ b/tools/preload2/src/com/android/preload/actions/ReloadListAction.java
@@ -0,0 +1,68 @@
+/*
+ * 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.preload.actions;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.IDevice;
+import com.android.preload.ClientUtils;
+
+import java.util.Arrays;
+import java.util.Comparator;
+
+import javax.swing.DefaultListModel;
+
+public class ReloadListAction extends AbstractThreadedDeviceSpecificAction {
+
+    private ClientUtils clientUtils;
+    private final DefaultListModel<Client> clientListModel;
+
+    public ReloadListAction(ClientUtils utils, IDevice device,
+            DefaultListModel<Client> clientListModel) {
+        super("Reload", device);
+        this.clientUtils = utils;
+        this.clientListModel = clientListModel;
+    }
+
+    @Override
+    public void run() {
+        Client[] clients = clientUtils.findAllClients(device);
+        if (clients != null) {
+            Arrays.sort(clients, new ClientComparator());
+        }
+        clientListModel.removeAllElements();
+        for (Client c : clients) {
+            clientListModel.addElement(c);
+        }
+    }
+
+    private static class ClientComparator implements Comparator<Client> {
+
+        @Override
+        public int compare(Client o1, Client o2) {
+            String s1 = o1.getClientData().getClientDescription();
+            String s2 = o2.getClientData().getClientDescription();
+
+            if (s1 == null || s2 == null) {
+                // Not good, didn't get all data?
+                return (s1 == null) ? -1 : 1;
+            }
+
+            return s1.compareTo(s2);
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/tools/preload2/src/com/android/preload/actions/RunMonkeyAction.java b/tools/preload2/src/com/android/preload/actions/RunMonkeyAction.java
new file mode 100644
index 0000000..385e857
--- /dev/null
+++ b/tools/preload2/src/com/android/preload/actions/RunMonkeyAction.java
@@ -0,0 +1,119 @@
+/*
+ * 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.preload.actions;
+
+import com.android.ddmlib.IDevice;
+import com.android.preload.DeviceUtils;
+import com.android.preload.DumpData;
+import com.android.preload.DumpTableModel;
+import com.android.preload.Main;
+
+import java.awt.event.ActionEvent;
+import java.util.Date;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import javax.swing.AbstractAction;
+
+public class RunMonkeyAction extends AbstractAction implements DeviceSpecific {
+
+    private final static String DEFAULT_MONKEY_PACKAGES =
+            "com.android.calendar,com.android.gallery3d";
+
+    private IDevice device;
+    private DumpTableModel dataTableModel;
+
+    public RunMonkeyAction(IDevice device, DumpTableModel dataTableModel) {
+        super("Run monkey");
+        this.device = device;
+        this.dataTableModel = dataTableModel;
+    }
+
+    @Override
+    public void setDevice(IDevice device) {
+        this.device = device;
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        String packages = Main.getUI().showInputDialog("Please enter packages name to run with"
+                + " the monkey, or leave empty for default.");
+        if (packages == null) {
+            return;
+        }
+        if (packages.isEmpty()) {
+            packages = DEFAULT_MONKEY_PACKAGES;
+        }
+        new Thread(new RunMonkeyRunnable(packages)).start();
+    }
+
+    private class RunMonkeyRunnable implements Runnable {
+
+        private String packages;
+        private final static int ITERATIONS = 1000;
+
+        public RunMonkeyRunnable(String packages) {
+            this.packages = packages;
+        }
+
+        @Override
+        public void run() {
+            Main.getUI().showWaitDialog();
+
+            try {
+                String pkgs[] = packages.split(",");
+
+                for (String pkg : pkgs) {
+                    Main.getUI().updateWaitDialog("Running monkey on " + pkg);
+
+                    try {
+                        // Stop running app.
+                        forceStop(pkg);
+
+                        // Little bit of breather here.
+                        try {
+                            Thread.sleep(1000);
+                        } catch (Exception e) {
+                        }
+
+                        DeviceUtils.doShell(device, "monkey -p " + pkg + " " + ITERATIONS, 1,
+                                TimeUnit.MINUTES);
+
+                        Main.getUI().updateWaitDialog("Retrieving heap data for " + pkg);
+                        Map<String, String> data = Main.findAndGetClassData(device, pkg);
+                        DumpData dumpData = new DumpData(pkg, data, new Date());
+                        dataTableModel.addData(dumpData);
+                    } catch (Exception e) {
+                        e.printStackTrace();
+                    } finally {
+                        // Stop running app.
+                        forceStop(pkg);
+                    }
+                }
+            } finally {
+                Main.getUI().hideWaitDialog();
+            }
+        }
+
+        private void forceStop(String packageName) {
+            // Stop running app.
+            DeviceUtils.doShell(device, "force-stop " + packageName, 5, TimeUnit.SECONDS);
+            DeviceUtils.doShell(device, "kill " + packageName, 5, TimeUnit.SECONDS);
+            DeviceUtils.doShell(device, "kill `pid " + packageName + "`", 5, TimeUnit.SECONDS);
+        }
+    }
+}
\ No newline at end of file
diff --git a/tools/preload2/src/com/android/preload/actions/ScanAllPackagesAction.java b/tools/preload2/src/com/android/preload/actions/ScanAllPackagesAction.java
new file mode 100644
index 0000000..d74b8a3
--- /dev/null
+++ b/tools/preload2/src/com/android/preload/actions/ScanAllPackagesAction.java
@@ -0,0 +1,63 @@
+/*
+ * 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.preload.actions;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.IDevice;
+import com.android.preload.ClientUtils;
+import com.android.preload.DumpData;
+import com.android.preload.DumpTableModel;
+import com.android.preload.Main;
+
+import java.util.Date;
+import java.util.Map;
+
+public class ScanAllPackagesAction extends AbstractThreadedDeviceSpecificAction {
+
+    private ClientUtils clientUtils;
+    private DumpTableModel dataTableModel;
+
+    public ScanAllPackagesAction(ClientUtils utils, IDevice device, DumpTableModel dataTableModel) {
+        super("Scan all packages", device);
+        this.clientUtils = utils;
+        this.dataTableModel = dataTableModel;
+    }
+
+    @Override
+    public void run() {
+        Main.getUI().showWaitDialog();
+
+        try {
+            Client[] clients = clientUtils.findAllClients(device);
+            for (Client c : clients) {
+                String pkg = c.getClientData().getClientDescription();
+                Main.getUI().showWaitDialog();
+                Main.getUI().updateWaitDialog("Retrieving heap data for " + pkg);
+
+                try {
+                    Map<String, String> data = Main.getClassDataRetriever().getClassData(c);
+                    DumpData dumpData = new DumpData(pkg, data, new Date());
+                    dataTableModel.addData(dumpData);
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+            }
+        } finally {
+            Main.getUI().hideWaitDialog();
+        }
+    }
+}
\ No newline at end of file
diff --git a/tools/preload2/src/com/android/preload/actions/ScanPackageAction.java b/tools/preload2/src/com/android/preload/actions/ScanPackageAction.java
new file mode 100644
index 0000000..98492bd
--- /dev/null
+++ b/tools/preload2/src/com/android/preload/actions/ScanPackageAction.java
@@ -0,0 +1,97 @@
+/*
+ * 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.preload.actions;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.IDevice;
+import com.android.preload.ClientUtils;
+import com.android.preload.DumpData;
+import com.android.preload.DumpTableModel;
+import com.android.preload.Main;
+
+import java.util.Date;
+import java.util.Map;
+
+public class ScanPackageAction extends AbstractThreadedDeviceSpecificAction {
+
+    private ClientUtils clientUtils;
+    private DumpTableModel dataTableModel;
+
+    public ScanPackageAction(ClientUtils utils, IDevice device, DumpTableModel dataTableModel) {
+        super("Scan package", device);
+        this.clientUtils = utils;
+        this.dataTableModel = dataTableModel;
+    }
+
+    @Override
+    public void run() {
+        Main.getUI().showWaitDialog();
+
+        try {
+            Client client = Main.getUI().getSelectedClient();
+            if (client != null) {
+                work(client);
+            } else {
+                Client[] clients = clientUtils.findAllClients(device);
+                if (clients.length > 0) {
+                    ClientWrapper[] clientWrappers = new ClientWrapper[clients.length];
+                    for (int i = 0; i < clientWrappers.length; i++) {
+                        clientWrappers[i] = new ClientWrapper(clients[i]);
+                    }
+                    Main.getUI().hideWaitDialog();
+
+                    ClientWrapper ret = Main.getUI().showChoiceDialog("Choose a package to scan",
+                            "Choose package",
+                            clientWrappers);
+                    if (ret != null) {
+                        work(ret.client);
+                    }
+                }
+            }
+        } finally {
+            Main.getUI().hideWaitDialog();
+        }
+    }
+
+    private void work(Client c) {
+        String pkg = c.getClientData().getClientDescription();
+        Main.getUI().showWaitDialog();
+        Main.getUI().updateWaitDialog("Retrieving heap data for " + pkg);
+
+        try {
+            Map<String, String> data = Main.findAndGetClassData(device, pkg);
+            DumpData dumpData = new DumpData(pkg, data, new Date());
+            dataTableModel.addData(dumpData);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    private static class ClientWrapper {
+        private Client client;
+
+        public ClientWrapper(Client c) {
+            client = c;
+        }
+
+        @Override
+        public String toString() {
+            return client.getClientData().getClientDescription() + " (pid "
+                    + client.getClientData().getPid() + ")";
+        }
+    }
+}
\ No newline at end of file
diff --git a/tools/preload2/src/com/android/preload/actions/ShowDataAction.java b/tools/preload2/src/com/android/preload/actions/ShowDataAction.java
new file mode 100644
index 0000000..2bb175f
--- /dev/null
+++ b/tools/preload2/src/com/android/preload/actions/ShowDataAction.java
@@ -0,0 +1,94 @@
+/*
+ * 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.preload.actions;
+
+import com.android.preload.DumpData;
+import com.android.preload.DumpTableModel;
+import com.android.preload.Main;
+
+import java.awt.BorderLayout;
+import java.awt.event.ActionEvent;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.swing.AbstractAction;
+import javax.swing.JFrame;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+
+public class ShowDataAction extends AbstractAction {
+    private DumpTableModel dataTableModel;
+
+    public ShowDataAction(DumpTableModel dataTableModel) {
+        super("Show data");
+        this.dataTableModel = dataTableModel;
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        // TODO(agampe): Auto-generated method stub
+        int selRow = Main.getUI().getSelectedDataTableRow();
+        if (selRow != -1) {
+            DumpData data = dataTableModel.getData().get(selRow);
+            Map<String, Set<String>> inv = data.invertData();
+
+            StringBuilder builder = new StringBuilder();
+
+            // First bootclasspath.
+            add(builder, "Boot classpath:", inv.get(null));
+
+            // Now everything else.
+            for (String k : inv.keySet()) {
+                if (k != null) {
+                    builder.append("==================\n\n");
+                    add(builder, k, inv.get(k));
+                }
+            }
+
+            JFrame newFrame = new JFrame(data.getPackageName() + " " + data.getDate());
+            newFrame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
+            newFrame.getContentPane().add(new JScrollPane(new JTextArea(builder.toString())),
+                    BorderLayout.CENTER);
+            newFrame.setSize(800, 600);
+            newFrame.setLocationRelativeTo(null);
+            newFrame.setVisible(true);
+        }
+    }
+
+    private void add(StringBuilder builder, String head, Set<String> set) {
+        builder.append(head);
+        builder.append('\n');
+        addSet(builder, set);
+        builder.append('\n');
+    }
+
+    private void addSet(StringBuilder builder, Set<String> set) {
+        if (set == null) {
+            builder.append("  NONE\n");
+            return;
+        }
+        List<String> sorted = new ArrayList<>(set);
+        Collections.sort(sorted);
+        for (String s : sorted) {
+            builder.append(s);
+            builder.append('\n');
+        }
+    }
+}
\ No newline at end of file
diff --git a/tools/preload2/src/com/android/preload/classdataretrieval/ClassDataRetriever.java b/tools/preload2/src/com/android/preload/classdataretrieval/ClassDataRetriever.java
new file mode 100644
index 0000000..f04360f
--- /dev/null
+++ b/tools/preload2/src/com/android/preload/classdataretrieval/ClassDataRetriever.java
@@ -0,0 +1,29 @@
+/*
+ * 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.preload.classdataretrieval;
+
+import com.android.ddmlib.Client;
+
+import java.util.Map;
+
+/**
+ * Retrieve a class-to-classloader map for loaded classes from the client.
+ */
+public interface ClassDataRetriever {
+
+    public Map<String, String> getClassData(Client client);
+}
diff --git a/tools/preload2/src/com/android/preload/classdataretrieval/hprof/GeneralHprofDumpHandler.java b/tools/preload2/src/com/android/preload/classdataretrieval/hprof/GeneralHprofDumpHandler.java
new file mode 100644
index 0000000..8d797ee
--- /dev/null
+++ b/tools/preload2/src/com/android/preload/classdataretrieval/hprof/GeneralHprofDumpHandler.java
@@ -0,0 +1,70 @@
+/*
+ * 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.preload.classdataretrieval.hprof;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData.IHprofDumpHandler;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class GeneralHprofDumpHandler implements IHprofDumpHandler {
+
+    private List<IHprofDumpHandler> handlers = new ArrayList<>();
+
+    public void addHandler(IHprofDumpHandler h) {
+      synchronized (handlers) {
+        handlers.add(h);
+      }
+    }
+
+    public void removeHandler(IHprofDumpHandler h) {
+      synchronized (handlers) {
+        handlers.remove(h);
+      }
+    }
+
+    private List<IHprofDumpHandler> getIterationList() {
+      synchronized (handlers) {
+        return new ArrayList<>(handlers);
+      }
+    }
+
+    @Override
+    public void onEndFailure(Client arg0, String arg1) {
+      List<IHprofDumpHandler> iterList = getIterationList();
+      for (IHprofDumpHandler h : iterList) {
+        h.onEndFailure(arg0, arg1);
+      }
+    }
+
+    @Override
+    public void onSuccess(String arg0, Client arg1) {
+      List<IHprofDumpHandler> iterList = getIterationList();
+      for (IHprofDumpHandler h : iterList) {
+        h.onSuccess(arg0, arg1);
+      }
+    }
+
+    @Override
+    public void onSuccess(byte[] arg0, Client arg1) {
+      List<IHprofDumpHandler> iterList = getIterationList();
+      for (IHprofDumpHandler h : iterList) {
+        h.onSuccess(arg0, arg1);
+      }
+    }
+  }
\ No newline at end of file
diff --git a/tools/preload2/src/com/android/preload/classdataretrieval/hprof/Hprof.java b/tools/preload2/src/com/android/preload/classdataretrieval/hprof/Hprof.java
new file mode 100644
index 0000000..21b7a04
--- /dev/null
+++ b/tools/preload2/src/com/android/preload/classdataretrieval/hprof/Hprof.java
@@ -0,0 +1,220 @@
+/*
+ * 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.preload.classdataretrieval.hprof;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData;
+import com.android.ddmlib.ClientData.IHprofDumpHandler;
+import com.android.preload.classdataretrieval.ClassDataRetriever;
+import com.android.preload.ui.NullProgressMonitor;
+import com.android.tools.perflib.captures.MemoryMappedFileBuffer;
+import com.android.tools.perflib.heap.ClassObj;
+import com.android.tools.perflib.heap.Queries;
+import com.android.tools.perflib.heap.Snapshot;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class Hprof implements ClassDataRetriever {
+
+    private static GeneralHprofDumpHandler hprofHandler;
+
+    public static void init() {
+        synchronized(Hprof.class) {
+            if (hprofHandler == null) {
+                ClientData.setHprofDumpHandler(hprofHandler = new GeneralHprofDumpHandler());
+            }
+        }
+    }
+
+    public static File doHprof(Client client, int timeout) {
+        GetHprof gh = new GetHprof(client, timeout);
+        return gh.get();
+    }
+
+    /**
+     * Return a map of class names to class-loader names derived from the hprof dump.
+     *
+     * @param hprofLocalFile
+     */
+    public static Map<String, String> analyzeHprof(File hprofLocalFile) throws Exception {
+        Snapshot snapshot = Snapshot.createSnapshot(new MemoryMappedFileBuffer(hprofLocalFile));
+
+        Map<String, Set<ClassObj>> classes = Queries.classes(snapshot, null);
+        Map<String, String> retValue = new HashMap<String, String>();
+        for (Map.Entry<String, Set<ClassObj>> e : classes.entrySet()) {
+            for (ClassObj c : e.getValue()) {
+                String cl = c.getClassLoader() == null ? null : c.getClassLoader().toString();
+                String cName = c.getClassName();
+                int aDepth = 0;
+                while (cName.endsWith("[]")) {
+                    cName = cName.substring(0, cName.length()-2);
+                    aDepth++;
+                }
+                String newName = transformPrimitiveClass(cName);
+                if (aDepth > 0) {
+                    // Need to use kind-a descriptor syntax. If it was transformed, it is primitive.
+                    if (newName.equals(cName)) {
+                        newName = "L" + newName + ";";
+                    }
+                    for (int i = 0; i < aDepth; i++) {
+                        newName = "[" + newName;
+                    }
+                }
+                retValue.put(newName, cl);
+            }
+        }
+
+        // Free up memory.
+        snapshot.dispose();
+
+        return retValue;
+    }
+
+    private static Map<String, String> primitiveMapping;
+
+    static {
+        primitiveMapping = new HashMap<>();
+        primitiveMapping.put("boolean", "Z");
+        primitiveMapping.put("byte", "B");
+        primitiveMapping.put("char", "C");
+        primitiveMapping.put("double", "D");
+        primitiveMapping.put("float", "F");
+        primitiveMapping.put("int", "I");
+        primitiveMapping.put("long", "J");
+        primitiveMapping.put("short", "S");
+        primitiveMapping.put("void", "V");
+    }
+
+    private static String transformPrimitiveClass(String name) {
+        String rep = primitiveMapping.get(name);
+        if (rep != null) {
+            return rep;
+        }
+        return name;
+    }
+
+    private static class GetHprof implements IHprofDumpHandler {
+
+        private File target;
+        private long timeout;
+        private Client client;
+
+        public GetHprof(Client client, long timeout) {
+            this.client = client;
+            this.timeout = timeout;
+        }
+
+        public File get() {
+            synchronized (this) {
+                hprofHandler.addHandler(this);
+                client.dumpHprof();
+                if (target == null) {
+                    try {
+                        wait(timeout);
+                    } catch (Exception e) {
+                        System.out.println(e);
+                    }
+                }
+            }
+
+            hprofHandler.removeHandler(this);
+            return target;
+        }
+
+        private void wakeUp() {
+            synchronized (this) {
+                notifyAll();
+            }
+        }
+
+        @Override
+        public void onEndFailure(Client arg0, String arg1) {
+            System.out.println("GetHprof.onEndFailure");
+            if (client == arg0) {
+                wakeUp();
+            }
+        }
+
+        private static File createTargetFile() {
+            try {
+                return File.createTempFile("ddms", ".hprof");
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        @Override
+        public void onSuccess(String arg0, Client arg1) {
+            System.out.println("GetHprof.onSuccess");
+            if (client == arg1) {
+                try {
+                    target = createTargetFile();
+                    arg1.getDevice().getSyncService().pullFile(arg0,
+                            target.getAbsoluteFile().toString(), new NullProgressMonitor());
+                } catch (Exception e) {
+                    e.printStackTrace();
+                    target = null;
+                }
+                wakeUp();
+            }
+        }
+
+        @Override
+        public void onSuccess(byte[] arg0, Client arg1) {
+            System.out.println("GetHprof.onSuccess");
+            if (client == arg1) {
+                try {
+                    target = createTargetFile();
+                    BufferedOutputStream out =
+                            new BufferedOutputStream(new FileOutputStream(target));
+                    out.write(arg0);
+                    out.close();
+                } catch (Exception e) {
+                    e.printStackTrace();
+                    target = null;
+                }
+                wakeUp();
+            }
+        }
+    }
+
+    private int timeout;
+
+    public Hprof(int timeout) {
+        this.timeout = timeout;
+    }
+
+    @Override
+    public Map<String, String> getClassData(Client client) {
+        File hprofLocalFile = Hprof.doHprof(client, timeout);
+        if (hprofLocalFile == null) {
+            throw new RuntimeException("Failed getting dump...");
+        }
+        System.out.println("Dump file is " + hprofLocalFile);
+
+        try {
+            return analyzeHprof(hprofLocalFile);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/tools/preload2/src/com/android/preload/classdataretrieval/jdwp/JDWPClassDataRetriever.java b/tools/preload2/src/com/android/preload/classdataretrieval/jdwp/JDWPClassDataRetriever.java
new file mode 100644
index 0000000..dbd4c89
--- /dev/null
+++ b/tools/preload2/src/com/android/preload/classdataretrieval/jdwp/JDWPClassDataRetriever.java
@@ -0,0 +1,221 @@
+/*
+ * 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.preload.classdataretrieval.jdwp;
+
+import com.android.ddmlib.Client;
+import com.android.preload.classdataretrieval.ClassDataRetriever;
+
+import org.apache.harmony.jpda.tests.framework.jdwp.CommandPacket;
+import org.apache.harmony.jpda.tests.framework.jdwp.JDWPCommands;
+import org.apache.harmony.jpda.tests.framework.jdwp.JDWPConstants;
+import org.apache.harmony.jpda.tests.framework.jdwp.ReplyPacket;
+import org.apache.harmony.jpda.tests.jdwp.share.JDWPTestCase;
+import org.apache.harmony.jpda.tests.jdwp.share.JDWPUnitDebuggeeWrapper;
+import org.apache.harmony.jpda.tests.share.JPDALogWriter;
+import org.apache.harmony.jpda.tests.share.JPDATestOptions;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class JDWPClassDataRetriever extends JDWPTestCase implements ClassDataRetriever {
+
+    private final Client client;
+
+    public JDWPClassDataRetriever() {
+        this(null);
+    }
+
+    public JDWPClassDataRetriever(Client client) {
+        this.client = client;
+    }
+
+
+    @Override
+    protected String getDebuggeeClassName() {
+        return "<unset>";
+    }
+
+    @Override
+    public Map<String, String> getClassData(Client client) {
+        return new JDWPClassDataRetriever(client).retrieve();
+    }
+
+    private Map<String, String> retrieve() {
+        if (client == null) {
+            throw new IllegalStateException();
+        }
+
+        settings = createTestOptions("localhost:" + String.valueOf(client.getDebuggerListenPort()));
+        settings.setDebuggeeSuspend("n");
+
+        logWriter = new JPDALogWriter(System.out, "", false);
+
+        try {
+            internalSetUp();
+
+            return retrieveImpl();
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        } finally {
+            internalTearDown();
+        }
+    }
+
+    private Map<String, String> retrieveImpl() {
+        try {
+            // Suspend the app.
+            {
+                CommandPacket packet = new CommandPacket(
+                        JDWPCommands.VirtualMachineCommandSet.CommandSetID,
+                        JDWPCommands.VirtualMachineCommandSet.SuspendCommand);
+                ReplyPacket reply = debuggeeWrapper.vmMirror.performCommand(packet);
+                if (reply.getErrorCode() != JDWPConstants.Error.NONE) {
+                    return null;
+                }
+            }
+
+            // List all classes.
+            CommandPacket packet = new CommandPacket(
+                    JDWPCommands.VirtualMachineCommandSet.CommandSetID,
+                    JDWPCommands.VirtualMachineCommandSet.AllClassesCommand);
+            ReplyPacket reply = debuggeeWrapper.vmMirror.performCommand(packet);
+
+            if (reply.getErrorCode() != JDWPConstants.Error.NONE) {
+                return null;
+            }
+
+            int classCount = reply.getNextValueAsInt();
+            System.out.println("Runtime reported " + classCount + " classes.");
+
+            Map<Long, String> classes = new HashMap<Long, String>();
+            Map<Long, String> arrayClasses = new HashMap<Long, String>();
+
+            for (int i = 0; i < classCount; i++) {
+                byte refTypeTag = reply.getNextValueAsByte();
+                long typeID = reply.getNextValueAsReferenceTypeID();
+                String signature = reply.getNextValueAsString();
+                /* int status = */ reply.getNextValueAsInt();
+
+                switch (refTypeTag) {
+                    case JDWPConstants.TypeTag.CLASS:
+                    case JDWPConstants.TypeTag.INTERFACE:
+                        classes.put(typeID, signature);
+                        break;
+
+                    case JDWPConstants.TypeTag.ARRAY:
+                        arrayClasses.put(typeID, signature);
+                        break;
+                }
+            }
+
+            Map<String, String> result = new HashMap<String, String>();
+
+            // Parse all classes.
+            for (Map.Entry<Long, String> entry : classes.entrySet()) {
+                long typeID = entry.getKey();
+                String signature = entry.getValue();
+
+                if (!checkClass(typeID, signature, result)) {
+                    System.err.println("Issue investigating " + signature);
+                }
+            }
+
+            // For arrays, look at the leaf component type.
+            for (Map.Entry<Long, String> entry : arrayClasses.entrySet()) {
+                long typeID = entry.getKey();
+                String signature = entry.getValue();
+
+                if (!checkArrayClass(typeID, signature, result)) {
+                    System.err.println("Issue investigating " + signature);
+                }
+            }
+
+            return result;
+        } finally {
+            // Resume the app.
+            {
+                CommandPacket packet = new CommandPacket(
+                        JDWPCommands.VirtualMachineCommandSet.CommandSetID,
+                        JDWPCommands.VirtualMachineCommandSet.ResumeCommand);
+                /* ReplyPacket reply = */ debuggeeWrapper.vmMirror.performCommand(packet);
+            }
+        }
+    }
+
+    private boolean checkClass(long typeID, String signature, Map<String, String> result) {
+        CommandPacket packet = new CommandPacket(
+                JDWPCommands.ReferenceTypeCommandSet.CommandSetID,
+                JDWPCommands.ReferenceTypeCommandSet.ClassLoaderCommand);
+        packet.setNextValueAsReferenceTypeID(typeID);
+        ReplyPacket reply = debuggeeWrapper.vmMirror.performCommand(packet);
+        if (reply.getErrorCode() != JDWPConstants.Error.NONE) {
+            return false;
+        }
+
+        long classLoaderID = reply.getNextValueAsObjectID();
+
+        // TODO: Investigate the classloader to have a better string?
+        String classLoaderString = (classLoaderID == 0) ? null : String.valueOf(classLoaderID);
+
+        result.put(getClassName(signature), classLoaderString);
+
+        return true;
+    }
+
+    private boolean checkArrayClass(long typeID, String signature, Map<String, String> result) {
+        // Classloaders of array classes are the same as the component class'.
+        CommandPacket packet = new CommandPacket(
+                JDWPCommands.ReferenceTypeCommandSet.CommandSetID,
+                JDWPCommands.ReferenceTypeCommandSet.ClassLoaderCommand);
+        packet.setNextValueAsReferenceTypeID(typeID);
+        ReplyPacket reply = debuggeeWrapper.vmMirror.performCommand(packet);
+        if (reply.getErrorCode() != JDWPConstants.Error.NONE) {
+            return false;
+        }
+
+        long classLoaderID = reply.getNextValueAsObjectID();
+
+        // TODO: Investigate the classloader to have a better string?
+        String classLoaderString = (classLoaderID == 0) ? null : String.valueOf(classLoaderID);
+
+        // For array classes, we *need* the signature directly.
+        result.put(signature, classLoaderString);
+
+        return true;
+    }
+
+    private static String getClassName(String signature) {
+        String withoutLAndSemicolon = signature.substring(1, signature.length() - 1);
+        return withoutLAndSemicolon.replace('/', '.');
+    }
+
+
+    private static JPDATestOptions createTestOptions(String address) {
+        JPDATestOptions options = new JPDATestOptions();
+        options.setAttachConnectorKind();
+        options.setTimeout(1000);
+        options.setWaitingTime(1000);
+        options.setTransportAddress(address);
+        return options;
+    }
+
+    @Override
+    protected JDWPUnitDebuggeeWrapper createDebuggeeWrapper() {
+        return new PreloadDebugeeWrapper(settings, logWriter);
+    }
+}
diff --git a/tools/preload2/src/com/android/preload/classdataretrieval/jdwp/PreloadDebugeeWrapper.java b/tools/preload2/src/com/android/preload/classdataretrieval/jdwp/PreloadDebugeeWrapper.java
new file mode 100644
index 0000000..b9df6d0
--- /dev/null
+++ b/tools/preload2/src/com/android/preload/classdataretrieval/jdwp/PreloadDebugeeWrapper.java
@@ -0,0 +1,40 @@
+/*
+ * 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.preload.classdataretrieval.jdwp;
+
+import org.apache.harmony.jpda.tests.framework.LogWriter;
+import org.apache.harmony.jpda.tests.jdwp.share.JDWPManualDebuggeeWrapper;
+import org.apache.harmony.jpda.tests.share.JPDATestOptions;
+
+import java.io.IOException;
+
+public class PreloadDebugeeWrapper extends JDWPManualDebuggeeWrapper {
+
+    public PreloadDebugeeWrapper(JPDATestOptions options, LogWriter writer) {
+        super(options, writer);
+    }
+
+    @Override
+    protected Process launchProcess(String cmdLine) throws IOException {
+        return null;
+    }
+
+    @Override
+    protected void WaitForProcessExit(Process process) {
+    }
+
+}
diff --git a/tools/preload2/src/com/android/preload/ui/NullProgressMonitor.java b/tools/preload2/src/com/android/preload/ui/NullProgressMonitor.java
new file mode 100644
index 0000000..f45aad0
--- /dev/null
+++ b/tools/preload2/src/com/android/preload/ui/NullProgressMonitor.java
@@ -0,0 +1,39 @@
+/*
+ * 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.preload.ui;
+
+import com.android.ddmlib.SyncService.ISyncProgressMonitor;
+
+public class NullProgressMonitor implements ISyncProgressMonitor {
+
+    @Override
+    public void advance(int arg0) {}
+
+    @Override
+    public boolean isCanceled() {
+        return false;
+    }
+
+    @Override
+    public void start(int arg0) {}
+
+    @Override
+    public void startSubTask(String arg0) {}
+
+    @Override
+    public void stop() {}
+}
\ No newline at end of file
diff --git a/tools/preload2/src/com/android/preload/ui/UI.java b/tools/preload2/src/com/android/preload/ui/UI.java
new file mode 100644
index 0000000..47174dd
--- /dev/null
+++ b/tools/preload2/src/com/android/preload/ui/UI.java
@@ -0,0 +1,267 @@
+/*
+ * 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.preload.ui;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.io.File;
+import java.util.List;
+
+import javax.swing.Action;
+import javax.swing.DefaultListCellRenderer;
+import javax.swing.JDialog;
+import javax.swing.JFileChooser;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JOptionPane;
+import javax.swing.JProgressBar;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.JToolBar;
+import javax.swing.ListModel;
+import javax.swing.SwingUtilities;
+import javax.swing.table.TableModel;
+
+public class UI extends JFrame {
+
+    private JList<Client> clientList;
+    private JTable dataTable;
+
+    // Shared file chooser, means the directory is retained.
+    private JFileChooser jfc;
+
+    public UI(ListModel<Client> clientListModel,
+              TableModel dataTableModel,
+              List<Action> actions) {
+        super("Preloaded-classes computation");
+
+        getContentPane().add(new JScrollPane(clientList = new JList<Client>(clientListModel)),
+                BorderLayout.WEST);
+        clientList.setCellRenderer(new ClientListCellRenderer());
+        // clientList.addListSelectionListener(listener);
+
+        dataTable = new JTable(dataTableModel);
+        getContentPane().add(new JScrollPane(dataTable), BorderLayout.CENTER);
+
+        JToolBar toolbar = new JToolBar(JToolBar.HORIZONTAL);
+        for (Action a : actions) {
+            if (a == null) {
+                toolbar.addSeparator();
+            } else {
+                toolbar.add(a);
+            }
+        }
+        getContentPane().add(toolbar, BorderLayout.PAGE_START);
+
+        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+        setBounds(100, 100, 800, 600);
+    }
+
+    public Client getSelectedClient() {
+        return clientList.getSelectedValue();
+    }
+
+    public int getSelectedDataTableRow() {
+        return dataTable.getSelectedRow();
+    }
+
+    private JDialog currentWaitDialog = null;
+
+    public void showWaitDialog() {
+        if (currentWaitDialog == null) {
+            currentWaitDialog = new JDialog(this, "Please wait...", true);
+            currentWaitDialog.getContentPane().add(new JLabel("Please be patient."),
+                    BorderLayout.CENTER);
+            JProgressBar progress = new JProgressBar(JProgressBar.HORIZONTAL);
+            progress.setIndeterminate(true);
+            currentWaitDialog.getContentPane().add(progress, BorderLayout.SOUTH);
+            currentWaitDialog.setSize(200, 100);
+            currentWaitDialog.setLocationRelativeTo(null);
+            showWaitDialogLater();
+        }
+    }
+
+    private void showWaitDialogLater() {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                if (currentWaitDialog != null) {
+                    currentWaitDialog.setVisible(true); // This is blocking.
+                }
+            }
+        });
+    }
+
+    public void updateWaitDialog(String s) {
+        if (currentWaitDialog != null) {
+            ((JLabel) currentWaitDialog.getContentPane().getComponent(0)).setText(s);
+            Dimension prefSize = currentWaitDialog.getPreferredSize();
+            Dimension curSize = currentWaitDialog.getSize();
+            if (prefSize.width > curSize.width || prefSize.height > curSize.height) {
+                currentWaitDialog.setSize(Math.max(prefSize.width, curSize.width),
+                        Math.max(prefSize.height, curSize.height));
+                currentWaitDialog.invalidate();
+            }
+        }
+    }
+
+    public void hideWaitDialog() {
+        if (currentWaitDialog != null) {
+            currentWaitDialog.setVisible(false);
+            currentWaitDialog = null;
+        }
+    }
+
+    public void showMessageDialog(String s) {
+        // Hide the wait dialog...
+        if (currentWaitDialog != null) {
+            currentWaitDialog.setVisible(false);
+        }
+
+        try {
+            JOptionPane.showMessageDialog(this, s);
+        } finally {
+            // And reshow it afterwards...
+            if (currentWaitDialog != null) {
+                showWaitDialogLater();
+            }
+        }
+    }
+
+    public boolean showConfirmDialog(String title, String message) {
+        // Hide the wait dialog...
+        if (currentWaitDialog != null) {
+            currentWaitDialog.setVisible(false);
+        }
+
+        try {
+            return JOptionPane.showConfirmDialog(this, title, message, JOptionPane.YES_NO_OPTION)
+                    == JOptionPane.YES_OPTION;
+        } finally {
+            // And reshow it afterwards...
+            if (currentWaitDialog != null) {
+                showWaitDialogLater();
+            }
+        }
+    }
+
+    public String showInputDialog(String message) {
+        // Hide the wait dialog...
+        if (currentWaitDialog != null) {
+            currentWaitDialog.setVisible(false);
+        }
+
+        try {
+            return JOptionPane.showInputDialog(message);
+        } finally {
+            // And reshow it afterwards...
+            if (currentWaitDialog != null) {
+                showWaitDialogLater();
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    public <T> T showChoiceDialog(String title, String message, T[] choices) {
+        // Hide the wait dialog...
+        if (currentWaitDialog != null) {
+            currentWaitDialog.setVisible(false);
+        }
+
+        try{
+            return (T)JOptionPane.showInputDialog(this,
+                    title,
+                    message,
+                    JOptionPane.QUESTION_MESSAGE,
+                    null,
+                    choices,
+                    choices[0]);
+        } finally {
+            // And reshow it afterwards...
+            if (currentWaitDialog != null) {
+                showWaitDialogLater();
+            }
+        }
+    }
+
+    public File showSaveDialog() {
+        // Hide the wait dialog...
+        if (currentWaitDialog != null) {
+            currentWaitDialog.setVisible(false);
+        }
+
+        try{
+            if (jfc == null) {
+                jfc = new JFileChooser();
+            }
+
+            int ret = jfc.showSaveDialog(this);
+            if (ret == JFileChooser.APPROVE_OPTION) {
+                return jfc.getSelectedFile();
+            } else {
+                return null;
+            }
+        } finally {
+            // And reshow it afterwards...
+            if (currentWaitDialog != null) {
+                showWaitDialogLater();
+            }
+        }
+    }
+
+    public File[] showOpenDialog(boolean multi) {
+        // Hide the wait dialog...
+        if (currentWaitDialog != null) {
+            currentWaitDialog.setVisible(false);
+        }
+
+        try{
+            if (jfc == null) {
+                jfc = new JFileChooser();
+            }
+
+            jfc.setMultiSelectionEnabled(multi);
+            int ret = jfc.showOpenDialog(this);
+            if (ret == JFileChooser.APPROVE_OPTION) {
+                return jfc.getSelectedFiles();
+            } else {
+                return null;
+            }
+        } finally {
+            // And reshow it afterwards...
+            if (currentWaitDialog != null) {
+                showWaitDialogLater();
+            }
+        }
+    }
+
+    private class ClientListCellRenderer extends DefaultListCellRenderer {
+
+        @Override
+        public Component getListCellRendererComponent(JList<?> list, Object value, int index,
+                boolean isSelected, boolean cellHasFocus) {
+            ClientData cd = ((Client) value).getClientData();
+            String s = cd.getClientDescription() + " (pid " + cd.getPid() + ")";
+            return super.getListCellRendererComponent(list, s, index, isSelected, cellHasFocus);
+        }
+    }
+}