[androidsdk-tools] 50/51: Imported Upstream version 22+git20130416~f55ffbb

Tony Mancill tmancill at moszumanska.debian.org
Sun Nov 23 23:38:02 GMT 2014


This is an automated email from the git hooks/post-receive script.

tmancill pushed a commit to branch master
in repository androidsdk-tools.

commit 49df0ce47eaa10c5370534a737050fda71596f6c
Author: Jakub Adam <jakub.adam at ktknet.cz>
Date:   Sun Jun 2 11:01:23 2013 +0200

    Imported Upstream version 22+git20130416~f55ffbb
---
 common/NOTICE                                      |  190 ++
 common/README.txt                                  |   14 +
 common/common.iml                                  |   17 +
 common/src/main/java/com/android/SdkConstants.java | 1184 +++++++++++
 .../main/java/com/android/annotations/NonNull.java |   38 +
 .../com/android/annotations/NonNullByDefault.java  |   47 +
 .../java/com/android/annotations/Nullable.java     |   49 +
 .../com/android/annotations/VisibleForTesting.java |   50 +
 .../android/annotations/concurrency/GuardedBy.java |   34 +
 .../android/annotations/concurrency/Immutable.java |   33 +
 .../src/main/java/com/android/io/FileWrapper.java  |  158 ++
 .../main/java/com/android/io/FolderWrapper.java    |  162 ++
 .../main/java/com/android/io/IAbstractFile.java    |   58 +
 .../main/java/com/android/io/IAbstractFolder.java  |   77 +
 .../java/com/android/io/IAbstractResource.java     |   50 +
 .../main/java/com/android/io/StreamException.java  |   50 +
 .../java/com/android/prefs/AndroidLocation.java    |  129 ++
 .../src/main/java/com/android/utils/ILogger.java   |   78 +
 .../main/java/com/android/utils/IReaderLogger.java |   42 +
 .../main/java/com/android/utils/NullLogger.java    |   55 +
 common/src/main/java/com/android/utils/Pair.java   |  107 +
 .../java/com/android/utils/PositionXmlParser.java  |  729 +++++++
 .../src/main/java/com/android/utils/SdkUtils.java  |  292 +++
 .../src/main/java/com/android/utils/StdLogger.java |  178 ++
 .../src/main/java/com/android/utils/XmlUtils.java  |  432 ++++
 .../main/java/com/android/xml/AndroidManifest.java |  371 ++++
 .../java/com/android/xml/AndroidXPathFactory.java  |  113 +
 ddmlib/.classpath                                  |    8 +
 ddmlib/.gitignore                                  |    2 +
 ddmlib/.project                                    |   17 +
 ddmlib/.settings/org.eclipse.jdt.core.prefs        |   98 +
 ddmlib/NOTICE                                      |  190 ++
 ddmlib/ddmlib.iml                                  |   19 +
 .../ddmlib/AdbCommandRejectedException.java        |   55 +
 .../main/java/com/android/ddmlib/AdbHelper.java    |  791 +++++++
 .../java/com/android/ddmlib/AllocationInfo.java    |  215 ++
 .../com/android/ddmlib/AndroidDebugBridge.java     | 1179 +++++++++++
 .../com/android/ddmlib/BadPacketException.java     |   35 +
 .../java/com/android/ddmlib/CanceledException.java |   40 +
 .../main/java/com/android/ddmlib/ChunkHandler.java |  222 ++
 .../src/main/java/com/android/ddmlib/Client.java   |  871 ++++++++
 .../main/java/com/android/ddmlib/ClientData.java   |  732 +++++++
 .../android/ddmlib/CollectingOutputReceiver.java   |   74 +
 .../main/java/com/android/ddmlib/DdmConstants.java |   64 +
 .../java/com/android/ddmlib/DdmPreferences.java    |  220 ++
 .../java/com/android/ddmlib/DebugPortManager.java  |   70 +
 .../src/main/java/com/android/ddmlib/Debugger.java |  353 ++++
 .../src/main/java/com/android/ddmlib/Device.java   |  872 ++++++++
 .../java/com/android/ddmlib/DeviceMonitor.java     |  945 +++++++++
 .../java/com/android/ddmlib/EmulatorConsole.java   |  740 +++++++
 .../com/android/ddmlib/FileListingService.java     |  852 ++++++++
 .../java/com/android/ddmlib/GetPropReceiver.java   |   75 +
 .../java/com/android/ddmlib/HandleAppName.java     |  116 ++
 .../main/java/com/android/ddmlib/HandleExit.java   |   76 +
 .../main/java/com/android/ddmlib/HandleHeap.java   |  594 ++++++
 .../main/java/com/android/ddmlib/HandleHello.java  |  199 ++
 .../java/com/android/ddmlib/HandleNativeHeap.java  |  303 +++
 .../java/com/android/ddmlib/HandleProfiling.java   |  304 +++
 .../main/java/com/android/ddmlib/HandleTest.java   |   86 +
 .../main/java/com/android/ddmlib/HandleThread.java |  379 ++++
 .../java/com/android/ddmlib/HandleViewDebug.java   |  343 ++++
 .../main/java/com/android/ddmlib/HandleWait.java   |   91 +
 .../main/java/com/android/ddmlib/HeapSegment.java  |  448 ++++
 .../src/main/java/com/android/ddmlib/IDevice.java  |  527 +++++
 .../com/android/ddmlib/IShellOutputReceiver.java   |   44 +
 .../java/com/android/ddmlib/IStackTraceInfo.java   |   29 +
 .../java/com/android/ddmlib/InstallException.java  |   42 +
 .../main/java/com/android/ddmlib/JdwpPacket.java   |  371 ++++
 ddmlib/src/main/java/com/android/ddmlib/Log.java   |  359 ++++
 .../java/com/android/ddmlib/MonitorThread.java     |  790 +++++++
 .../java/com/android/ddmlib/MultiLineReceiver.java |  130 ++
 .../com/android/ddmlib/NativeAllocationInfo.java   |  305 +++
 .../com/android/ddmlib/NativeLibraryMapInfo.java   |   73 +
 .../com/android/ddmlib/NativeStackCallInfo.java    |  113 +
 .../com/android/ddmlib/NullOutputReceiver.java     |   53 +
 .../src/main/java/com/android/ddmlib/RawImage.java |  222 ++
 .../ddmlib/ShellCommandUnresponsiveException.java  |   27 +
 .../java/com/android/ddmlib/SyncException.java     |   97 +
 .../main/java/com/android/ddmlib/SyncService.java  |  887 ++++++++
 .../main/java/com/android/ddmlib/ThreadInfo.java   |  140 ++
 .../java/com/android/ddmlib/TimeoutException.java  |   26 +
 .../com/android/ddmlib/log/EventContainer.java     |  462 +++++
 .../com/android/ddmlib/log/EventLogParser.java     |  588 ++++++
 .../android/ddmlib/log/EventValueDescription.java  |  216 ++
 .../com/android/ddmlib/log/GcEventContainer.java   |  347 ++++
 .../android/ddmlib/log/InvalidTypeException.java   |   74 +
 .../ddmlib/log/InvalidValueTypeException.java      |   78 +
 .../java/com/android/ddmlib/log/LogReceiver.java   |  247 +++
 .../com/android/ddmlib/logcat/LogCatFilter.java    |  231 +++
 .../com/android/ddmlib/logcat/LogCatListener.java  |   23 +
 .../com/android/ddmlib/logcat/LogCatMessage.java   |  105 +
 .../android/ddmlib/logcat/LogCatMessageParser.java |  101 +
 .../android/ddmlib/logcat/LogCatReceiverTask.java  |  136 ++
 .../testrunner/IRemoteAndroidTestRunner.java       |  236 +++
 .../ddmlib/testrunner/ITestRunListener.java        |  109 +
 .../testrunner/InstrumentationResultParser.java    |  609 ++++++
 .../ddmlib/testrunner/RemoteAndroidTestRunner.java |  263 +++
 .../android/ddmlib/testrunner/TestIdentifier.java  |   91 +
 .../com/android/ddmlib/testrunner/TestResult.java  |  141 ++
 .../android/ddmlib/testrunner/TestRunResult.java   |  324 +++
 .../ddmlib/testrunner/XmlTestRunListener.java      |  289 +++
 .../java/com/android/ddmlib/utils/ArrayHelper.java |   90 +
 ddms/app/.classpath                                |   11 +
 ddms/app/.project                                  |   17 +
 ddms/app/.settings/org.eclipse.jdt.core.prefs      |   98 +
 ddms/app/NOTICE                                    |  190 ++
 ddms/app/README                                    |   75 +
 ddms/app/etc/ddms                                  |  111 +
 ddms/app/etc/ddms.bat                              |   74 +
 .../main/java/com/android/ddms/AboutDialog.java    |  158 ++
 .../java/com/android/ddms/DebugPortProvider.java   |  164 ++
 .../java/com/android/ddms/DeviceCommandDialog.java |  441 ++++
 .../android/ddms/DropdownSelectionListener.java    |   80 +
 ddms/app/src/main/java/com/android/ddms/Main.java  |  171 ++
 .../main/java/com/android/ddms/PrefsDialog.java    |  610 ++++++
 .../com/android/ddms/StaticPortConfigDialog.java   |  395 ++++
 .../com/android/ddms/StaticPortEditDialog.java     |  334 +++
 .../src/main/java/com/android/ddms/UIThread.java   | 1812 ++++++++++++++++
 ddms/app/src/main/resources/images/ddms-128.png    |  Bin 0 -> 17692 bytes
 ddms/ddmuilib/.classpath                           |   16 +
 ddms/ddmuilib/.project                             |   17 +
 ddms/ddmuilib/.settings/org.eclipse.jdt.core.prefs |   98 +
 ddms/ddmuilib/NOTICE                               |  190 ++
 ddms/ddmuilib/README                               |   14 +
 .../android/ddmuilib/AbstractBufferFindTarget.java |  117 ++
 .../main/java/com/android/ddmuilib/Addr2Line.java  |  355 ++++
 .../java/com/android/ddmuilib/AllocationPanel.java |  651 ++++++
 .../com/android/ddmuilib/BackgroundThread.java     |   50 +
 .../java/com/android/ddmuilib/BaseHeapPanel.java   |  193 ++
 .../com/android/ddmuilib/ClientDisplayPanel.java   |   33 +
 .../com/android/ddmuilib/DdmUiPreferences.java     |   79 +
 .../java/com/android/ddmuilib/DevicePanel.java     |  784 +++++++
 .../com/android/ddmuilib/EmulatorControlPanel.java | 1463 +++++++++++++
 .../main/java/com/android/ddmuilib/FindDialog.java |  142 ++
 .../main/java/com/android/ddmuilib/HeapPanel.java  | 1310 ++++++++++++
 .../java/com/android/ddmuilib/IFindTarget.java     |   21 +
 .../com/android/ddmuilib/ITableFocusListener.java  |   38 +
 .../java/com/android/ddmuilib/ImageLoader.java     |  206 ++
 .../main/java/com/android/ddmuilib/InfoPanel.java  |  199 ++
 .../java/com/android/ddmuilib/NativeHeapPanel.java | 1648 +++++++++++++++
 .../src/main/java/com/android/ddmuilib/Panel.java  |   49 +
 .../java/com/android/ddmuilib/PortFieldEditor.java |   73 +
 .../com/android/ddmuilib/ScreenShotDialog.java     |  350 ++++
 .../android/ddmuilib/SelectionDependentPanel.java  |   78 +
 .../java/com/android/ddmuilib/StackTracePanel.java |  223 ++
 .../com/android/ddmuilib/SyncProgressHelper.java   |  100 +
 .../com/android/ddmuilib/SyncProgressMonitor.java  |   60 +
 .../java/com/android/ddmuilib/SysinfoPanel.java    |  907 +++++++++
 .../java/com/android/ddmuilib/TableHelper.java     |  209 ++
 .../main/java/com/android/ddmuilib/TablePanel.java |  132 ++
 .../java/com/android/ddmuilib/ThreadPanel.java     |  573 ++++++
 .../android/ddmuilib/actions/ICommonAction.java    |   42 +
 .../android/ddmuilib/actions/ToolItemAction.java   |   71 +
 .../com/android/ddmuilib/annotation/UiThread.java  |   31 +
 .../android/ddmuilib/annotation/WorkerThread.java  |   31 +
 .../com/android/ddmuilib/console/DdmConsole.java   |   91 +
 .../com/android/ddmuilib/console/IDdmConsole.java  |   47 +
 .../ddmuilib/explorer/DeviceContentProvider.java   |  177 ++
 .../android/ddmuilib/explorer/DeviceExplorer.java  |  922 +++++++++
 .../ddmuilib/explorer/FileLabelProvider.java       |  160 ++
 .../android/ddmuilib/handler/BaseFileHandler.java  |  184 ++
 .../ddmuilib/handler/MethodProfilingHandler.java   |  195 ++
 .../ddmuilib/heap/NativeHeapDataImporter.java      |  222 ++
 .../ddmuilib/heap/NativeHeapDiffSnapshot.java      |   65 +
 .../ddmuilib/heap/NativeHeapLabelProvider.java     |  112 +
 .../com/android/ddmuilib/heap/NativeHeapPanel.java | 1150 +++++++++++
 .../heap/NativeHeapProviderByAllocations.java      |   90 +
 .../ddmuilib/heap/NativeHeapProviderByLibrary.java |   92 +
 .../android/ddmuilib/heap/NativeHeapSnapshot.java  |  133 ++
 .../ddmuilib/heap/NativeLibraryAllocationInfo.java |  135 ++
 .../ddmuilib/heap/NativeStackContentProvider.java  |   56 +
 .../ddmuilib/heap/NativeStackLabelProvider.java    |   71 +
 .../ddmuilib/heap/NativeSymbolResolverTask.java    |  306 +++
 .../ddmuilib/location/CoordinateControls.java      |  249 +++
 .../com/android/ddmuilib/location/GpxParser.java   |  373 ++++
 .../com/android/ddmuilib/location/KmlParser.java   |  210 ++
 .../android/ddmuilib/location/LocationPoint.java   |   53 +
 .../ddmuilib/location/TrackContentProvider.java    |   48 +
 .../ddmuilib/location/TrackLabelProvider.java      |   87 +
 .../com/android/ddmuilib/location/TrackPoint.java  |   34 +
 .../com/android/ddmuilib/location/WayPoint.java    |   42 +
 .../ddmuilib/location/WayPointContentProvider.java |   46 +
 .../ddmuilib/location/WayPointLabelProvider.java   |   79 +
 .../ddmuilib/log/event/BugReportImporter.java      |   96 +
 .../ddmuilib/log/event/DisplayFilteredLog.java     |   55 +
 .../android/ddmuilib/log/event/DisplayGraph.java   |  422 ++++
 .../com/android/ddmuilib/log/event/DisplayLog.java |  381 ++++
 .../android/ddmuilib/log/event/DisplaySync.java    |  304 +++
 .../ddmuilib/log/event/DisplaySyncHistogram.java   |  181 ++
 .../ddmuilib/log/event/DisplaySyncPerf.java        |  227 +++
 .../android/ddmuilib/log/event/EventDisplay.java   |  975 +++++++++
 .../ddmuilib/log/event/EventDisplayOptions.java    |  961 +++++++++
 .../ddmuilib/log/event/EventLogImporter.java       |   95 +
 .../android/ddmuilib/log/event/EventLogPanel.java  |  938 +++++++++
 .../ddmuilib/log/event/EventValueSelector.java     |  630 ++++++
 .../ddmuilib/log/event/OccurrenceRenderer.java     |   90 +
 .../com/android/ddmuilib/log/event/SyncCommon.java |  173 ++
 .../android/ddmuilib/logcat/EditFilterDialog.java  |  397 ++++
 .../logcat/ILogCatBufferChangeListener.java        |   33 +
 .../logcat/ILogCatMessageSelectionListener.java    |   26 +
 .../logcat/LogCatFilterContentProvider.java        |   46 +
 .../android/ddmuilib/logcat/LogCatFilterData.java  |   81 +
 .../ddmuilib/logcat/LogCatFilterLabelProvider.java |   63 +
 .../logcat/LogCatFilterSettingsDialog.java         |  327 +++
 .../logcat/LogCatFilterSettingsSerializer.java     |  211 ++
 .../android/ddmuilib/logcat/LogCatMessageList.java |  116 ++
 .../com/android/ddmuilib/logcat/LogCatPanel.java   | 1607 +++++++++++++++
 .../android/ddmuilib/logcat/LogCatReceiver.java    |  151 ++
 .../ddmuilib/logcat/LogCatReceiverFactory.java     |   95 +
 .../ddmuilib/logcat/LogCatStackTraceParser.java    |   81 +
 .../com/android/ddmuilib/logcat/LogColors.java     |   27 +
 .../com/android/ddmuilib/logcat/LogFilter.java     |  556 +++++
 .../java/com/android/ddmuilib/logcat/LogPanel.java | 1626 +++++++++++++++
 .../com/android/ddmuilib/net/NetworkPanel.java     | 1125 ++++++++++
 ddms/ddmuilib/src/main/java/images/add.png         |  Bin 0 -> 146 bytes
 ddms/ddmuilib/src/main/java/images/android.png     |  Bin 0 -> 3609 bytes
 ddms/ddmuilib/src/main/java/images/backward.png    |  Bin 0 -> 136 bytes
 ddms/ddmuilib/src/main/java/images/capture.png     |  Bin 0 -> 691 bytes
 ddms/ddmuilib/src/main/java/images/clear.png       |  Bin 0 -> 217 bytes
 ddms/ddmuilib/src/main/java/images/d.png           |  Bin 0 -> 638 bytes
 .../ddmuilib/src/main/java/images/debug-attach.png |  Bin 0 -> 156 bytes
 ddms/ddmuilib/src/main/java/images/debug-error.png |  Bin 0 -> 222 bytes
 ddms/ddmuilib/src/main/java/images/debug-wait.png  |  Bin 0 -> 156 bytes
 ddms/ddmuilib/src/main/java/images/delete.png      |  Bin 0 -> 107 bytes
 ddms/ddmuilib/src/main/java/images/device.png      |  Bin 0 -> 135 bytes
 ddms/ddmuilib/src/main/java/images/diff.png        |  Bin 0 -> 213 bytes
 .../src/main/java/images/displayfilters.png        |  Bin 0 -> 242 bytes
 ddms/ddmuilib/src/main/java/images/down.png        |  Bin 0 -> 141 bytes
 ddms/ddmuilib/src/main/java/images/e.png           |  Bin 0 -> 511 bytes
 ddms/ddmuilib/src/main/java/images/edit.png        |  Bin 0 -> 223 bytes
 ddms/ddmuilib/src/main/java/images/empty.png       |  Bin 0 -> 75 bytes
 ddms/ddmuilib/src/main/java/images/emulator.png    |  Bin 0 -> 287 bytes
 ddms/ddmuilib/src/main/java/images/file.png        |  Bin 0 -> 157 bytes
 ddms/ddmuilib/src/main/java/images/folder.png      |  Bin 0 -> 123 bytes
 ddms/ddmuilib/src/main/java/images/forward.png     |  Bin 0 -> 137 bytes
 ddms/ddmuilib/src/main/java/images/gc.png          |  Bin 0 -> 165 bytes
 ddms/ddmuilib/src/main/java/images/groupby.png     |  Bin 0 -> 413 bytes
 ddms/ddmuilib/src/main/java/images/halt.png        |  Bin 0 -> 197 bytes
 ddms/ddmuilib/src/main/java/images/heap.png        |  Bin 0 -> 222 bytes
 ddms/ddmuilib/src/main/java/images/hprof.png       |  Bin 0 -> 317 bytes
 ddms/ddmuilib/src/main/java/images/i.png           |  Bin 0 -> 498 bytes
 ddms/ddmuilib/src/main/java/images/importBug.png   |  Bin 0 -> 191 bytes
 ddms/ddmuilib/src/main/java/images/load.png        |  Bin 0 -> 163 bytes
 ddms/ddmuilib/src/main/java/images/pause.png       |  Bin 0 -> 98 bytes
 ddms/ddmuilib/src/main/java/images/play.png        |  Bin 0 -> 138 bytes
 ddms/ddmuilib/src/main/java/images/pull.png        |  Bin 0 -> 329 bytes
 ddms/ddmuilib/src/main/java/images/push.png        |  Bin 0 -> 228 bytes
 ddms/ddmuilib/src/main/java/images/save.png        |  Bin 0 -> 240 bytes
 ddms/ddmuilib/src/main/java/images/scroll_lock.png |  Bin 0 -> 291 bytes
 ddms/ddmuilib/src/main/java/images/sort_down.png   |  Bin 0 -> 102 bytes
 ddms/ddmuilib/src/main/java/images/sort_up.png     |  Bin 0 -> 105 bytes
 ddms/ddmuilib/src/main/java/images/thread.png      |  Bin 0 -> 121 bytes
 .../src/main/java/images/tracing_start.png         |  Bin 0 -> 227 bytes
 .../ddmuilib/src/main/java/images/tracing_stop.png |  Bin 0 -> 217 bytes
 ddms/ddmuilib/src/main/java/images/up.png          |  Bin 0 -> 134 bytes
 ddms/ddmuilib/src/main/java/images/v.png           |  Bin 0 -> 587 bytes
 ddms/ddmuilib/src/main/java/images/w.png           |  Bin 0 -> 681 bytes
 ddms/ddmuilib/src/main/java/images/warning.png     |  Bin 0 -> 147 bytes
 ddms/ddmuilib/src/main/java/images/zygote.png      |  Bin 0 -> 345 bytes
 hierarchyviewer2/MODULE_LICENSE_APACHE2            |    0
 hierarchyviewer2/app/.classpath                    |   12 +
 hierarchyviewer2/app/.project                      |   17 +
 hierarchyviewer2/app/.settings/README.txt          |    2 +
 .../app/.settings/org.eclipse.jdt.core.prefs       |   98 +
 hierarchyviewer2/app/NOTICE                        |  190 ++
 hierarchyviewer2/app/README                        |   69 +
 hierarchyviewer2/app/etc/hierarchyviewer           |  114 ++
 hierarchyviewer2/app/etc/hierarchyviewer.bat       |   75 +
 .../com/android/hierarchyviewer/AboutDialog.java   |   72 +
 .../HierarchyViewerApplication.java                |  942 +++++++++
 .../HierarchyViewerApplicationDirector.java        |   87 +
 .../hierarchyviewer/actions/AboutAction.java       |   65 +
 .../actions/LoadAllViewsAction.java                |   60 +
 .../hierarchyviewer/actions/QuitAction.java        |   44 +
 .../hierarchyviewer/actions/ShowOverlayAction.java |  116 ++
 .../android/hierarchyviewer/util/ActionButton.java |   83 +
 hierarchyviewer2/hierarchyviewer2lib/.classpath    |    9 +
 hierarchyviewer2/hierarchyviewer2lib/.project      |   17 +
 .../hierarchyviewer2lib/.settings/README.txt       |    2 +
 .../.settings/org.eclipse.jdt.core.prefs           |   98 +
 hierarchyviewer2/hierarchyviewer2lib/NOTICE        |  190 ++
 .../HierarchyViewerDirector.java                   |  731 +++++++
 .../actions/CapturePSDAction.java                  |   62 +
 .../actions/DisplayViewAction.java                 |   62 +
 .../actions/DumpDisplayListAction.java             |   56 +
 .../hierarchyviewerlib/actions/ImageAction.java    |   27 +
 .../actions/InspectScreenshotAction.java           |   96 +
 .../actions/InvalidateAction.java                  |   58 +
 .../actions/LoadOverlayAction.java                 |   62 +
 .../actions/LoadViewHierarchyAction.java           |   96 +
 .../actions/PixelPerfectAutoRefreshAction.java     |   59 +
 .../actions/PixelPerfectEnabledAction.java         |   82 +
 .../actions/ProfileNodesAction.java                |   55 +
 .../actions/RefreshPixelPerfectAction.java         |   58 +
 .../actions/RefreshPixelPerfectTreeAction.java     |   58 +
 .../actions/RefreshViewAction.java                 |   58 +
 .../actions/RefreshWindowsAction.java              |   59 +
 .../actions/RequestLayoutAction.java               |   58 +
 .../actions/SavePixelPerfectAction.java            |   62 +
 .../actions/SaveTreeViewAction.java                |   62 +
 .../actions/SelectedNodeEnabledAction.java         |   62 +
 .../actions/TreeViewEnabledAction.java             |   54 +
 .../device/AbstractHvDevice.java                   |   67 +
 .../device/DdmViewDebugDevice.java                 |  417 ++++
 .../hierarchyviewerlib/device/DeviceBridge.java    |  697 +++++++
 .../device/DeviceConnection.java                   |  100 +
 .../hierarchyviewerlib/device/HvDeviceFactory.java |   54 +
 .../hierarchyviewerlib/device/IHvDevice.java       |   62 +
 .../device/ViewServerDevice.java                   |  169 ++
 .../hierarchyviewerlib/device/WindowUpdater.java   |  160 ++
 .../models/DeviceSelectionModel.java               |  260 +++
 .../models/PixelPerfectModel.java                  |  360 ++++
 .../hierarchyviewerlib/models/TreeViewModel.java   |  215 ++
 .../hierarchyviewerlib/models/ViewNode.java        |  369 ++++
 .../android/hierarchyviewerlib/models/Window.java  |  117 ++
 .../hierarchyviewerlib/ui/CaptureDisplay.java      |  218 ++
 .../ui/DevicePropertyEditingSupport.java           |  302 +++
 .../hierarchyviewerlib/ui/DeviceSelector.java      |  342 ++++
 .../hierarchyviewerlib/ui/InvokeMethodPrompt.java  |  166 ++
 .../hierarchyviewerlib/ui/LayoutViewer.java        |  372 ++++
 .../hierarchyviewerlib/ui/PixelPerfect.java        |  392 ++++
 .../ui/PixelPerfectControls.java                   |  296 +++
 .../hierarchyviewerlib/ui/PixelPerfectLoupe.java   |  391 ++++
 .../ui/PixelPerfectPixelPanel.java                 |  203 ++
 .../hierarchyviewerlib/ui/PixelPerfectTree.java    |  241 +++
 .../hierarchyviewerlib/ui/PropertyViewer.java      |  391 ++++
 .../android/hierarchyviewerlib/ui/TreeView.java    | 1086 ++++++++++
 .../hierarchyviewerlib/ui/TreeViewControls.java    |  153 ++
 .../hierarchyviewerlib/ui/TreeViewOverview.java    |  396 ++++
 .../ui/util/DrawableViewNode.java                  |  266 +++
 .../hierarchyviewerlib/ui/util/PsdFile.java        |  508 +++++
 .../ui/util/TreeColumnResizer.java                 |  114 ++
 .../src/main/java/images/auto-refresh.png          |  Bin 0 -> 541 bytes
 .../src/main/java/images/capture-psd.png           |  Bin 0 -> 339 bytes
 .../src/main/java/images/device-view-selected.png  |  Bin 0 -> 254 bytes
 .../src/main/java/images/device-view.png           |  Bin 0 -> 228 bytes
 .../src/main/java/images/display.png               |  Bin 0 -> 946 bytes
 .../src/main/java/images/filtered.png              |  Bin 0 -> 9242 bytes
 .../src/main/java/images/green.png                 |  Bin 0 -> 302 bytes
 .../src/main/java/images/inspect-screenshot.png    |  Bin 0 -> 412 bytes
 .../src/main/java/images/invalidate.png            |  Bin 0 -> 391 bytes
 .../src/main/java/images/load-all-views.png        |  Bin 0 -> 728 bytes
 .../src/main/java/images/load-overlay.png          |  Bin 0 -> 549 bytes
 .../src/main/java/images/load-view-hierarchy.png   |  Bin 0 -> 288 bytes
 .../src/main/java/images/not-selected.png          |  Bin 0 -> 12468 bytes
 .../src/main/java/images/on-black.png              |  Bin 0 -> 157 bytes
 .../src/main/java/images/on-white.png              |  Bin 0 -> 158 bytes
 .../src/main/java/images/picker.png                |  Bin 0 -> 370 bytes
 .../java/images/pixel-perfect-view-selected.png    |  Bin 0 -> 734 bytes
 .../src/main/java/images/pixel-perfect-view.png    |  Bin 0 -> 733 bytes
 .../src/main/java/images/profile.png               |  Bin 0 -> 597 bytes
 .../src/main/java/images/red.png                   |  Bin 0 -> 383 bytes
 .../src/main/java/images/refresh-windows.png       |  Bin 0 -> 872 bytes
 .../src/main/java/images/request-layout.png        |  Bin 0 -> 223 bytes
 .../src/main/java/images/save.png                  |  Bin 0 -> 360 bytes
 .../main/java/images/sdk-hierarchyviewer-128.png   |  Bin 0 -> 17512 bytes
 .../main/java/images/sdk-hierarchyviewer-16.png    |  Bin 0 -> 880 bytes
 .../main/java/images/selected-filtered-small.png   |  Bin 0 -> 5182 bytes
 .../src/main/java/images/selected-filtered.png     |  Bin 0 -> 9015 bytes
 .../src/main/java/images/selected-small.png        |  Bin 0 -> 12611 bytes
 .../src/main/java/images/selected.png              |  Bin 0 -> 12159 bytes
 .../src/main/java/images/show-extras.png           |  Bin 0 -> 330 bytes
 .../src/main/java/images/show-overlay.png          |  Bin 0 -> 958 bytes
 .../src/main/java/images/tree-view-selected.png    |  Bin 0 -> 276 bytes
 .../src/main/java/images/tree-view.png             |  Bin 0 -> 281 bytes
 .../src/main/java/images/yellow.png                |  Bin 0 -> 255 bytes
 sdklib/.classpath                                  |   19 +
 sdklib/.gitignore                                  |    2 +
 sdklib/.project                                    |   17 +
 sdklib/.settings/org.eclipse.core.resources.prefs  |    4 +
 sdklib/.settings/org.eclipse.jdt.core.prefs        |   98 +
 sdklib/.settings/org.eclipse.jdt.ui.prefs          |   55 +
 sdklib/MODULE_LICENSE_APACHE2                      |    0
 sdklib/NOTICE                                      |  190 ++
 sdklib/sdklib.iml                                  |   22 +
 .../java/com/android/sdklib/util/ArrayUtils.java   |  136 ++
 .../com/android/sdklib/util/CommandLineParser.java |  968 +++++++++
 .../java/com/android/sdklib/util/FormatUtils.java  |   53 +
 .../com/android/sdklib/util/GrabProcessOutput.java |  157 ++
 .../java/com/android/sdklib/util/LineUtil.java     |  118 ++
 .../java/com/android/sdklib/util/SparseArray.java  |  401 ++++
 .../com/android/sdklib/util/SparseIntArray.java    |  238 +++
 sdkmanager/MODULE_LICENSE_APACHE2                  |    0
 sdkmanager/sdkuilib/.classpath                     |   13 +
 sdkmanager/sdkuilib/.project                       |   17 +
 .../sdkuilib/.settings/org.eclipse.jdt.core.prefs  |   98 +
 .../sdkuilib/.settings/org.eclipse.jdt.ui.prefs    |   55 +
 sdkmanager/sdkuilib/MODULE_LICENSE_APACHE2         |    0
 sdkmanager/sdkuilib/NOTICE                         |  190 ++
 sdkmanager/sdkuilib/README                         |   45 +
 .../sdkuilib/internal/repository/AboutDialog.java  |  121 ++
 .../internal/repository/ISdkUpdaterWindow.java     |   42 +
 .../internal/repository/ISwtUpdaterData.java       |   36 +
 .../internal/repository/MenuBarWrapper.java        |   60 +
 .../repository/SdkUpdaterChooserDialog.java        | 1130 ++++++++++
 .../internal/repository/SettingsDialog.java        |  286 +++
 .../internal/repository/SwtUpdaterData.java        |  240 +++
 .../internal/repository/UpdaterBaseDialog.java     |  106 +
 .../repository/core/PackagesDiffLogic.java         | 1002 +++++++++
 .../internal/repository/core/PkgCategory.java      |   87 +
 .../internal/repository/core/PkgCategoryApi.java   |  106 +
 .../repository/core/PkgCategorySource.java         |   70 +
 .../repository/core/PkgContentProvider.java        |  237 +++
 .../internal/repository/core/SdkLogAdapter.java    |  112 +
 .../internal/repository/core/SwtPackageLoader.java |   69 +
 .../internal/repository/icons/ImageFactory.java    |  159 ++
 .../internal/repository/icons/accept_icon16.png    |  Bin 0 -> 253 bytes
 .../internal/repository/icons/addon_pkg_16.png     |  Bin 0 -> 539 bytes
 .../internal/repository/icons/android_icon_128.png |  Bin 0 -> 17715 bytes
 .../internal/repository/icons/android_icon_16.png  |  Bin 0 -> 219 bytes
 .../internal/repository/icons/archive_icon16.png   |  Bin 0 -> 493 bytes
 .../internal/repository/icons/broken_16.png        |  Bin 0 -> 257 bytes
 .../internal/repository/icons/broken_pkg_16.png    |  Bin 0 -> 281 bytes
 .../internal/repository/icons/buildtool_pkg_16.png |  Bin 0 -> 453 bytes
 .../repository/icons/devman_generic_16.png         |  Bin 0 -> 269 bytes
 .../repository/icons/devman_manufacturer_16.png    |  Bin 0 -> 269 bytes
 .../internal/repository/icons/devman_user_16.png   |  Bin 0 -> 269 bytes
 .../internal/repository/icons/doc_pkg_16.png       |  Bin 0 -> 296 bytes
 .../internal/repository/icons/error_icon_16.png    |  Bin 0 -> 626 bytes
 .../internal/repository/icons/extra_pkg_16.png     |  Bin 0 -> 428 bytes
 .../internal/repository/icons/incompat_icon16.png  |  Bin 0 -> 735 bytes
 .../internal/repository/icons/log_off_16.png       |  Bin 0 -> 236 bytes
 .../internal/repository/icons/log_on_16.png        |  Bin 0 -> 311 bytes
 .../internal/repository/icons/nopkg_icon_16.png    |  Bin 0 -> 397 bytes
 .../internal/repository/icons/pkg_incompat_16.png  |  Bin 0 -> 454 bytes
 .../internal/repository/icons/pkg_installed_16.png |  Bin 0 -> 356 bytes
 .../internal/repository/icons/pkg_new_16.png       |  Bin 0 -> 299 bytes
 .../internal/repository/icons/pkg_update_16.png    |  Bin 0 -> 296 bytes
 .../internal/repository/icons/pkgcat_16.png        |  Bin 0 -> 388 bytes
 .../internal/repository/icons/pkgcat_other_16.png  |  Bin 0 -> 335 bytes
 .../internal/repository/icons/platform_pkg_16.png  |  Bin 0 -> 460 bytes
 .../repository/icons/platformtool_pkg_16.png       |  Bin 0 -> 453 bytes
 .../internal/repository/icons/reject_icon16.png    |  Bin 0 -> 317 bytes
 .../internal/repository/icons/sample_pkg_16.png    |  Bin 0 -> 433 bytes
 .../internal/repository/icons/sdkman_logo_128.png  |  Bin 0 -> 2381 bytes
 .../repository/icons/source_cat_icon_16.png        |  Bin 0 -> 245 bytes
 .../internal/repository/icons/source_icon_16.png   |  Bin 0 -> 879 bytes
 .../internal/repository/icons/source_pkg_16.png    |  Bin 0 -> 234 bytes
 .../internal/repository/icons/status_ok_16.png     |  Bin 0 -> 264 bytes
 .../internal/repository/icons/stop_disabled_16.png |  Bin 0 -> 321 bytes
 .../internal/repository/icons/stop_enabled_16.png  |  Bin 0 -> 327 bytes
 .../internal/repository/icons/sysimg_pkg_16.png    |  Bin 0 -> 485 bytes
 .../internal/repository/icons/tool_pkg_16.png      |  Bin 0 -> 188 bytes
 .../internal/repository/icons/unknown_icon16.png   |  Bin 0 -> 265 bytes
 .../internal/repository/icons/warning_icon16.png   |  Bin 0 -> 147 bytes
 .../internal/repository/ui/AddonSitesDialog.java   |  574 ++++++
 .../internal/repository/ui/AdtUpdateDialog.java    |  494 +++++
 .../internal/repository/ui/AvdManagerPage.java     |  172 ++
 .../repository/ui/AvdManagerWindowImpl1.java       |  411 ++++
 .../internal/repository/ui/DeviceManagerPage.java  |  832 ++++++++
 .../sdkuilib/internal/repository/ui/LogWindow.java |  379 ++++
 .../internal/repository/ui/PackagesPage.java       | 1301 ++++++++++++
 .../internal/repository/ui/PackagesPageIcons.java  |   33 +
 .../internal/repository/ui/PackagesPageImpl.java   |  574 ++++++
 .../ui/PkgTreeColumnViewerLabelProvider.java       |  137 ++
 .../repository/ui/SdkUpdaterWindowImpl2.java       |  590 ++++++
 .../internal/repository/ui/ShellSizeAndPos.java    |  166 ++
 .../sdkuilib/internal/tasks/ILogUiProvider.java    |   50 +
 .../internal/tasks/IProgressUiProvider.java        |   87 +
 .../sdkuilib/internal/tasks/ProgressTask.java      |  108 +
 .../internal/tasks/ProgressTaskDialog.java         |  520 +++++
 .../internal/tasks/ProgressTaskFactory.java        |   67 +
 .../sdkuilib/internal/tasks/ProgressView.java      |  376 ++++
 .../internal/tasks/ProgressViewFactory.java        |   48 +
 .../sdkuilib/internal/tasks/TaskMonitorImpl.java   |  369 ++++
 .../internal/widgets/AvdCreationDialog.java        | 1392 +++++++++++++
 .../internal/widgets/AvdDetailsDialog.java         |  162 ++
 .../sdkuilib/internal/widgets/AvdSelector.java     | 1252 ++++++++++++
 .../sdkuilib/internal/widgets/AvdStartDialog.java  |  642 ++++++
 .../internal/widgets/DeviceCreationDialog.java     | 1074 ++++++++++
 .../internal/widgets/HardwarePropertyChooser.java  |  150 ++
 .../internal/widgets/ImgDisabledButton.java        |   60 +
 .../internal/widgets/LegacyAvdEditDialog.java      | 1425 +++++++++++++
 .../sdkuilib/internal/widgets/MessageBoxLog.java   |  150 ++
 .../internal/widgets/ResolutionChooserDialog.java  |  123 ++
 .../internal/widgets/SdkTargetSelector.java        |  460 +++++
 .../sdkuilib/internal/widgets/ToggleButton.java    |  134 ++
 .../sdkuilib/repository/AvdManagerWindow.java      |   96 +
 .../sdkuilib/repository/SdkUpdaterWindow.java      |  113 +
 .../android/sdkuilib/ui/AuthenticationDialog.java  |  195 ++
 .../com/android/sdkuilib/ui/GridDataBuilder.java   |  158 ++
 .../java/com/android/sdkuilib/ui/GridDialog.java   |   81 +
 .../com/android/sdkuilib/ui/GridLayoutBuilder.java |  103 +
 .../com/android/sdkuilib/ui/SwtBaseDialog.java     |  247 +++
 .../internal/repository/MockSwtUpdaterData.java    |  232 +++
 .../internal/repository/SdkUpdaterLogicTest.java   |  486 +++++
 .../internal/repository/UpdaterDataTest.java       |   99 +
 .../repository/core/PackagesDiffLogicTest.java     | 1948 ++++++++++++++++++
 .../repository/ui/MockPackagesPageImpl.java        |  236 +++
 .../repository/ui/SdkManagerUpgradeTest.java       |  311 +++
 sdkstats/.classpath                                |   13 +
 sdkstats/.project                                  |   17 +
 sdkstats/.settings/README.txt                      |    2 +
 sdkstats/.settings/org.eclipse.jdt.core.prefs      |   98 +
 sdkstats/NOTICE                                    |  190 ++
 sdkstats/README                                    |   11 +
 .../com/android/sdkstats/DdmsPreferenceStore.java  |  332 +++
 .../android/sdkstats/SdkStatsPermissionDialog.java |  196 ++
 .../java/com/android/sdkstats/SdkStatsService.java |  558 +++++
 swtmenubar/.classpath                              |   10 +
 swtmenubar/.project                                |   17 +
 swtmenubar/MODULE_LICENSE_EPL                      |    0
 swtmenubar/NOTICE                                  |  224 ++
 swtmenubar/README                                  |   80 +
 .../menubar/internal/MenuBarEnhancerCocoa.java     |  341 ++++
 .../java/com/android/menubar/IMenuBarCallback.java |   42 +
 .../java/com/android/menubar/IMenuBarEnhancer.java |   73 +
 .../java/com/android/menubar/MenuBarEnhancer.java  |  248 +++
 .../com/android/menubar/MenuBarEnhancer37.java     |  156 ++
 traceview/.classpath                               |   10 +
 traceview/.project                                 |   17 +
 traceview/.settings/README.txt                     |    2 +
 traceview/.settings/org.eclipse.jdt.core.prefs     |   98 +
 traceview/NOTICE                                   |  190 ++
 traceview/README                                   |   11 +
 traceview/etc/traceview                            |  108 +
 traceview/etc/traceview.bat                        |   65 +
 .../src/main/java/com/android/traceview/Call.java  |  177 ++
 .../com/android/traceview/ColorController.java     |  113 +
 .../java/com/android/traceview/DmTraceReader.java  |  754 +++++++
 .../java/com/android/traceview/MainWindow.java     |  300 +++
 .../java/com/android/traceview/MethodData.java     |  513 +++++
 .../java/com/android/traceview/ProfileData.java    |   88 +
 .../java/com/android/traceview/ProfileNode.java    |   51 +
 .../com/android/traceview/ProfileProvider.java     |  467 +++++
 .../java/com/android/traceview/ProfileSelf.java    |   39 +
 .../java/com/android/traceview/ProfileView.java    |  332 +++
 .../com/android/traceview/PropertiesDialog.java    |  104 +
 .../main/java/com/android/traceview/Selection.java |   70 +
 .../com/android/traceview/SelectionController.java |   35 +
 .../java/com/android/traceview/ThreadData.java     |  170 ++
 .../java/com/android/traceview/TickScaler.java     |  148 ++
 .../main/java/com/android/traceview/TimeBase.java  |   71 +
 .../java/com/android/traceview/TimeLineView.java   | 2154 ++++++++++++++++++++
 .../java/com/android/traceview/TraceAction.java    |   31 +
 .../java/com/android/traceview/TraceReader.java    |   79 +
 .../java/com/android/traceview/TraceUnits.java     |   93 +
 traceview/src/main/resources/icons/sort_down.png   |  Bin 0 -> 102 bytes
 traceview/src/main/resources/icons/sort_up.png     |  Bin 0 -> 105 bytes
 .../src/main/resources/icons/traceview-128.png     |  Bin 0 -> 17131 bytes
 uiautomatorviewer/.classpath                       |   12 +
 uiautomatorviewer/.project                         |   17 +
 .../.settings/org.eclipse.jdt.core.prefs           |   98 +
 uiautomatorviewer/MODULE_LICENSE_APACHE2           |    0
 uiautomatorviewer/NOTICE                           |  190 ++
 uiautomatorviewer/etc/uiautomatorviewer            |  104 +
 uiautomatorviewer/etc/uiautomatorviewer.bat        |   66 +
 .../java/com/android/uiautomator/DebugBridge.java  |   86 +
 .../java/com/android/uiautomator/OpenDialog.java   |  225 ++
 .../com/android/uiautomator/UiAutomatorHelper.java |  196 ++
 .../com/android/uiautomator/UiAutomatorModel.java  |  143 ++
 .../com/android/uiautomator/UiAutomatorView.java   |  436 ++++
 .../com/android/uiautomator/UiAutomatorViewer.java |  108 +
 .../uiautomator/actions/ExpandAllAction.java       |   42 +
 .../android/uiautomator/actions/ImageHelper.java   |   48 +
 .../uiautomator/actions/OpenFilesAction.java       |   83 +
 .../uiautomator/actions/ScreenshotAction.java      |  176 ++
 .../uiautomator/actions/ToggleNafAction.java       |   46 +
 .../android/uiautomator/tree/AttributePair.java    |   26 +
 .../android/uiautomator/tree/BasicTreeNode.java    |  114 ++
 .../tree/BasicTreeNodeContentProvider.java         |   63 +
 .../android/uiautomator/tree/RootWindowNode.java   |   52 +
 .../uiautomator/tree/UiHierarchyXmlLoader.java     |  149 ++
 .../java/com/android/uiautomator/tree/UiNode.java  |  123 ++
 .../src/main/java/images/expandall.png             |  Bin 0 -> 268 bytes
 .../src/main/java/images/open-folder.png           |  Bin 0 -> 383 bytes
 .../src/main/java/images/screenshot.png            |  Bin 0 -> 1226 bytes
 uiautomatorviewer/src/main/java/images/warning.png |  Bin 0 -> 147 bytes
 568 files changed, 108329 insertions(+)

diff --git a/common/NOTICE b/common/NOTICE
new file mode 100644
index 0000000..faed58a
--- /dev/null
+++ b/common/NOTICE
@@ -0,0 +1,190 @@
+
+   Copyright (c) 2005-2013, 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.
+
+   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.
+
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
diff --git a/common/README.txt b/common/README.txt
new file mode 100644
index 0000000..d4c6232
--- /dev/null
+++ b/common/README.txt
@@ -0,0 +1,14 @@
+common.jar contains resource configuration enums. It is used by various tools, but also
+by layoutlib.jar
+
+Layoutlib.jar is built from frameworks/base.git and therefore is versioned with the platform.
+
+IMPORTANT NOTE REGARDING CHANGES IN common.jar:
+
+- The API must stay compatible. This is because while layoutlib.jar compiles against it,
+  the client provides the implementation and must be able to load earlier versions of layoutlib.jar.
+
+- Updated version of common should be copied to the current in-dev branch of
+  prebuilt/common/common/common-prebuilt.jar
+  The PREBUILT file in the same folder must be updated as well to reflect how to rebuild this
+  prebuilt jar file.
\ No newline at end of file
diff --git a/common/common.iml b/common/common.iml
new file mode 100644
index 0000000..416c57a
--- /dev/null
+++ b/common/common.iml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4">
+  <component name="NewModuleRootManager" inherit-compiler-output="true">
+    <exclude-output />
+    <content url="file://$MODULE_DIR$">
+      <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
+      <excludeFolder url="file://$MODULE_DIR$/.settings" />
+      <excludeFolder url="file://$MODULE_DIR$/build" />
+    </content>
+    <orderEntry type="sourceFolder" forTests="false" />
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="library" exported="" name="guava-tools" level="project" />
+    <orderEntry type="library" scope="TEST" name="JUnit3" level="project" />
+  </component>
+</module>
+
diff --git a/common/src/main/java/com/android/SdkConstants.java b/common/src/main/java/com/android/SdkConstants.java
new file mode 100644
index 0000000..fdceadb
--- /dev/null
+++ b/common/src/main/java/com/android/SdkConstants.java
@@ -0,0 +1,1184 @@
+/*
+ * Copyright (C) 2007 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;
+
+import java.io.File;
+
+/**
+ * Constant definition class.<br>
+ * <br>
+ * Most constants have a prefix defining the content.
+ * <ul>
+ * <li><code>OS_</code> OS path constant. These paths are different depending on the platform.</li>
+ * <li><code>FN_</code> File name constant.</li>
+ * <li><code>FD_</code> Folder name constant.</li>
+ * <li><code>TAG_</code> XML element tag name</li>
+ * <li><code>ATTR_</code> XML attribute name</li>
+ * <li><code>VALUE_</code> XML attribute value</li>
+ * <li><code>CLASS_</code> Class name</li>
+ * <li><code>DOT_</code> File name extension, including the dot </li>
+ * <li><code>EXT_</code> File name extension, without the dot </li>
+ * </ul>
+ */
+ at SuppressWarnings("javadoc") // Not documenting all the fields here
+public final class SdkConstants {
+    public static final int PLATFORM_UNKNOWN = 0;
+    public static final int PLATFORM_LINUX = 1;
+    public static final int PLATFORM_WINDOWS = 2;
+    public static final int PLATFORM_DARWIN = 3;
+
+    /**
+     * Returns current platform, one of {@link #PLATFORM_WINDOWS}, {@link #PLATFORM_DARWIN},
+     * {@link #PLATFORM_LINUX} or {@link #PLATFORM_UNKNOWN}.
+     */
+    public static final int CURRENT_PLATFORM = currentPlatform();
+
+    /**
+     * Charset for the ini file handled by the SDK.
+     */
+    public static final String INI_CHARSET = "UTF-8";                                 //$NON-NLS-1$
+
+    /** An SDK Project's AndroidManifest.xml file */
+    public static final String FN_ANDROID_MANIFEST_XML= "AndroidManifest.xml";        //$NON-NLS-1$
+    /** pre-dex jar filename. i.e. "classes.jar" */
+    public static final String FN_CLASSES_JAR = "classes.jar";                        //$NON-NLS-1$
+    /** Dex filename inside the APK. i.e. "classes.dex" */
+    public static final String FN_APK_CLASSES_DEX = "classes.dex";                    //$NON-NLS-1$
+
+    /** An SDK Project's build.xml file */
+    public static final String FN_BUILD_XML = "build.xml";                            //$NON-NLS-1$
+
+    /** Name of the framework library, i.e. "android.jar" */
+    public static final String FN_FRAMEWORK_LIBRARY = "android.jar";                  //$NON-NLS-1$
+    /** Name of the framework library, i.e. "uiautomator.jar" */
+    public static final String FN_UI_AUTOMATOR_LIBRARY = "uiautomator.jar";           //$NON-NLS-1$
+    /** Name of the layout attributes, i.e. "attrs.xml" */
+    public static final String FN_ATTRS_XML = "attrs.xml";                            //$NON-NLS-1$
+    /** Name of the layout attributes, i.e. "attrs_manifest.xml" */
+    public static final String FN_ATTRS_MANIFEST_XML = "attrs_manifest.xml";          //$NON-NLS-1$
+    /** framework aidl import file */
+    public static final String FN_FRAMEWORK_AIDL = "framework.aidl";                  //$NON-NLS-1$
+    /** framework renderscript folder */
+    public static final String FN_FRAMEWORK_RENDERSCRIPT = "renderscript";            //$NON-NLS-1$
+    /** framework include folder */
+    public static final String FN_FRAMEWORK_INCLUDE = "include";                      //$NON-NLS-1$
+    /** framework include (clang) folder */
+    public static final String FN_FRAMEWORK_INCLUDE_CLANG = "clang-include";          //$NON-NLS-1$
+    /** layoutlib.jar file */
+    public static final String FN_LAYOUTLIB_JAR = "layoutlib.jar";                    //$NON-NLS-1$
+    /** widget list file */
+    public static final String FN_WIDGETS = "widgets.txt";                            //$NON-NLS-1$
+    /** Intent activity actions list file */
+    public static final String FN_INTENT_ACTIONS_ACTIVITY = "activity_actions.txt";   //$NON-NLS-1$
+    /** Intent broadcast actions list file */
+    public static final String FN_INTENT_ACTIONS_BROADCAST = "broadcast_actions.txt"; //$NON-NLS-1$
+    /** Intent service actions list file */
+    public static final String FN_INTENT_ACTIONS_SERVICE = "service_actions.txt";     //$NON-NLS-1$
+    /** Intent category list file */
+    public static final String FN_INTENT_CATEGORIES = "categories.txt";               //$NON-NLS-1$
+
+    /** annotations support jar */
+    public static final String FN_ANNOTATIONS_JAR = "annotations.jar";                //$NON-NLS-1$
+
+    /** platform build property file */
+    public static final String FN_BUILD_PROP = "build.prop";                          //$NON-NLS-1$
+    /** plugin properties file */
+    public static final String FN_PLUGIN_PROP = "plugin.prop";                        //$NON-NLS-1$
+    /** add-on manifest file */
+    public static final String FN_MANIFEST_INI = "manifest.ini";                      //$NON-NLS-1$
+    /** add-on layout device XML file. */
+    public static final String FN_DEVICES_XML = "devices.xml";                        //$NON-NLS-1$
+    /** hardware properties definition file */
+    public static final String FN_HARDWARE_INI = "hardware-properties.ini";           //$NON-NLS-1$
+
+    /** project property file */
+    public static final String FN_PROJECT_PROPERTIES = "project.properties";          //$NON-NLS-1$
+
+    /** project local property file */
+    public static final String FN_LOCAL_PROPERTIES = "local.properties";              //$NON-NLS-1$
+
+    /** project ant property file */
+    public static final String FN_ANT_PROPERTIES = "ant.properties";                  //$NON-NLS-1$
+
+    /** Skin layout file */
+    public static final String FN_SKIN_LAYOUT = "layout";                             //$NON-NLS-1$
+
+    /** dx.jar file */
+    public static final String FN_DX_JAR = "dx.jar";                                  //$NON-NLS-1$
+
+    /** dx executable (with extension for the current OS) */
+    public static final String FN_DX =
+        "dx" + ext(".bat", "");                           //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+
+    /** aapt executable (with extension for the current OS) */
+    public static final String FN_AAPT =
+        "aapt" + ext(".exe", "");                         //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+
+    /** aidl executable (with extension for the current OS) */
+    public static final String FN_AIDL =
+        "aidl" + ext(".exe", "");                         //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+
+    /** renderscript executable (with extension for the current OS) */
+    public static final String FN_RENDERSCRIPT =
+        "llvm-rs-cc" + ext(".exe", "");                   //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+
+    /** adb executable (with extension for the current OS) */
+    public static final String FN_ADB =
+        "adb" + ext(".exe", "");                          //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+
+    /** emulator executable for the current OS */
+    public static final String FN_EMULATOR =
+        "emulator" + ext(".exe", "");                     //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+
+    /** zipalign executable (with extension for the current OS) */
+    public static final String FN_ZIPALIGN =
+        "zipalign" + ext(".exe", "");                     //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+
+    /** dexdump executable (with extension for the current OS) */
+    public static final String FN_DEXDUMP =
+        "dexdump" + ext(".exe", "");                      //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+
+    /** proguard executable (with extension for the current OS) */
+    public static final String FN_PROGUARD =
+        "proguard" + ext(".bat", ".sh");                  //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+
+    /** find_lock for Windows (with extension for the current OS) */
+    public static final String FN_FIND_LOCK =
+        "find_lock" + ext(".exe", "");                    //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+
+    /** properties file for SDK Updater packages */
+    public static final String FN_SOURCE_PROP = "source.properties";                  //$NON-NLS-1$
+    /** properties file for content hash of installed packages */
+    public static final String FN_CONTENT_HASH_PROP = "content_hash.properties";      //$NON-NLS-1$
+    /** properties file for the SDK */
+    public static final String FN_SDK_PROP = "sdk.properties";                        //$NON-NLS-1$
+
+    /**
+     * filename for gdbserver.
+     */
+    public static final String FN_GDBSERVER = "gdbserver";              //$NON-NLS-1$
+
+    /** global Android proguard config file */
+    public static final String FN_ANDROID_PROGUARD_FILE = "proguard-android.txt";   //$NON-NLS-1$
+    /** global Android proguard config file with optimization enabled */
+    public static final String FN_ANDROID_OPT_PROGUARD_FILE = "proguard-android-optimize.txt";  //$NON-NLS-1$
+    /** default proguard config file with new file extension (for project specific stuff) */
+    public static final String FN_PROJECT_PROGUARD_FILE = "proguard-project.txt";   //$NON-NLS-1$
+
+    /* Folder Names for Android Projects . */
+
+    /** Resources folder name, i.e. "res". */
+    public static final String FD_RESOURCES = "res";                    //$NON-NLS-1$
+    /** Assets folder name, i.e. "assets" */
+    public static final String FD_ASSETS = "assets";                    //$NON-NLS-1$
+    /** Default source folder name in an SDK project, i.e. "src".
+     * <p/>
+     * Note: this is not the same as {@link #FD_PKG_SOURCES}
+     * which is an SDK sources folder for packages. */
+    public static final String FD_SOURCES = "src";                      //$NON-NLS-1$
+    /** Default generated source folder name, i.e. "gen" */
+    public static final String FD_GEN_SOURCES = "gen";                  //$NON-NLS-1$
+    /** Default native library folder name inside the project, i.e. "libs"
+     * While the folder inside the .apk is "lib", we call that one libs because
+     * that's what we use in ant for both .jar and .so and we need to make the 2 development ways
+     * compatible. */
+    public static final String FD_NATIVE_LIBS = "libs";                 //$NON-NLS-1$
+    /** Native lib folder inside the APK: "lib" */
+    public static final String FD_APK_NATIVE_LIBS = "lib";              //$NON-NLS-1$
+    /** Default output folder name, i.e. "bin" */
+    public static final String FD_OUTPUT = "bin";                       //$NON-NLS-1$
+    /** Classes output folder name, i.e. "classes" */
+    public static final String FD_CLASSES_OUTPUT = "classes";           //$NON-NLS-1$
+    /** proguard output folder for mapping, etc.. files */
+    public static final String FD_PROGUARD = "proguard";                //$NON-NLS-1$
+    /** aidl output folder for copied aidl files */
+    public static final String FD_AIDL = "aidl";                        //$NON-NLS-1$
+
+    /* Folder Names for the Android SDK */
+
+    /** Name of the SDK platforms folder. */
+    public static final String FD_PLATFORMS = "platforms";              //$NON-NLS-1$
+    /** Name of the SDK addons folder. */
+    public static final String FD_ADDONS = "add-ons";                   //$NON-NLS-1$
+    /** Name of the SDK system-images folder. */
+    public static final String FD_SYSTEM_IMAGES = "system-images";      //$NON-NLS-1$
+    /** Name of the SDK sources folder where source packages are installed.
+     * <p/>
+     * Note this is not the same as {@link #FD_SOURCES} which is the folder name where sources
+     * are installed inside a project. */
+    public static final String FD_PKG_SOURCES = "sources";              //$NON-NLS-1$
+    /** Name of the SDK tools folder. */
+    public static final String FD_TOOLS = "tools";                      //$NON-NLS-1$
+    /** Name of the SDK tools/support folder. */
+    public static final String FD_SUPPORT = "support";                  //$NON-NLS-1$
+    /** Name of the SDK platform tools folder. */
+    public static final String FD_PLATFORM_TOOLS = "platform-tools";    //$NON-NLS-1$
+    /** Name of the SDK build tools folder. */
+    public static final String FD_BUILD_TOOLS = "build-tools";          //$NON-NLS-1$
+    /** Name of the SDK tools/lib folder. */
+    public static final String FD_LIB = "lib";                          //$NON-NLS-1$
+    /** Name of the SDK docs folder. */
+    public static final String FD_DOCS = "docs";                        //$NON-NLS-1$
+    /** Name of the doc folder containing API reference doc (javadoc) */
+    public static final String FD_DOCS_REFERENCE = "reference";         //$NON-NLS-1$
+    /** Name of the SDK images folder. */
+    public static final String FD_IMAGES = "images";                    //$NON-NLS-1$
+    /** Name of the ABI to support. */
+    public static final String ABI_ARMEABI = "armeabi";                 //$NON-NLS-1$
+    public static final String ABI_ARMEABI_V7A = "armeabi-v7a";         //$NON-NLS-1$
+    public static final String ABI_INTEL_ATOM = "x86";                  //$NON-NLS-1$
+    public static final String ABI_MIPS = "mips";                       //$NON-NLS-1$
+    /** Name of the CPU arch to support. */
+    public static final String CPU_ARCH_ARM = "arm";                    //$NON-NLS-1$
+    public static final String CPU_ARCH_INTEL_ATOM = "x86";             //$NON-NLS-1$
+    public static final String CPU_ARCH_MIPS = "mips";                  //$NON-NLS-1$
+    /** Name of the CPU model to support. */
+    public static final String CPU_MODEL_CORTEX_A8 = "cortex-a8";       //$NON-NLS-1$
+
+    /** Name of the SDK skins folder. */
+    public static final String FD_SKINS = "skins";                      //$NON-NLS-1$
+    /** Name of the SDK samples folder. */
+    public static final String FD_SAMPLES = "samples";                  //$NON-NLS-1$
+    /** Name of the SDK extras folder. */
+    public static final String FD_EXTRAS = "extras";                    //$NON-NLS-1$
+    /**
+     * Name of an extra's sample folder.
+     * Ideally extras should have one {@link #FD_SAMPLES} folder containing
+     * one or more sub-folders (one per sample). However some older extras
+     * might contain a single "sample" folder with directly the samples files
+     * in it. When possible we should encourage extras' owners to move to the
+     * multi-samples format.
+     */
+    public static final String FD_SAMPLE = "sample";                    //$NON-NLS-1$
+    /** Name of the SDK templates folder, i.e. "templates" */
+    public static final String FD_TEMPLATES = "templates";              //$NON-NLS-1$
+    /** Name of the SDK Ant folder, i.e. "ant" */
+    public static final String FD_ANT = "ant";                          //$NON-NLS-1$
+    /** Name of the SDK data folder, i.e. "data" */
+    public static final String FD_DATA = "data";                        //$NON-NLS-1$
+    /** Name of the SDK renderscript folder, i.e. "rs" */
+    public static final String FD_RENDERSCRIPT = "rs";                  //$NON-NLS-1$
+    /** Name of the SDK resources folder, i.e. "res" */
+    public static final String FD_RES = "res";                          //$NON-NLS-1$
+    /** Name of the SDK font folder, i.e. "fonts" */
+    public static final String FD_FONTS = "fonts";                      //$NON-NLS-1$
+    /** Name of the android sources directory */
+    public static final String FD_ANDROID_SOURCES = "sources";          //$NON-NLS-1$
+    /** Name of the addon libs folder. */
+    public static final String FD_ADDON_LIBS = "libs";                  //$NON-NLS-1$
+
+    /** Name of the cache folder in the $HOME/.android. */
+    public static final String FD_CACHE = "cache";                      //$NON-NLS-1$
+
+    /** API codename of a release (non preview) system image or platform. **/
+    public static final String CODENAME_RELEASE = "REL";                //$NON-NLS-1$
+
+    /** Namespace for the resource XML, i.e. "http://schemas.android.com/apk/res/android" */
+    public static final String NS_RESOURCES =
+        "http://schemas.android.com/apk/res/android";                   //$NON-NLS-1$
+
+    /** Namespace for the device schema, i.e. "http://schemas.android.com/sdk/devices/1" */
+    public static final String NS_DEVICES_XSD =
+        "http://schemas.android.com/sdk/devices/1";                     //$NON-NLS-1$
+
+    /**
+     * Namespace pattern for the custom resource XML, i.e. "http://schemas.android.com/apk/res/%s"
+     * <p/>
+     * This string contains a %s. It must be combined with the desired Java package, e.g.:
+     * <pre>
+     *    String.format(SdkConstants.NS_CUSTOM_RESOURCES_S, "android");
+     *    String.format(SdkConstants.NS_CUSTOM_RESOURCES_S, "com.test.mycustomapp");
+     * </pre>
+     *
+     * Note: if you need an URI specifically for the "android" namespace, consider using
+     * {@link SdkConstants#NS_RESOURCES} instead.
+     */
+    public final static String NS_CUSTOM_RESOURCES_S = "http://schemas.android.com/apk/res/%1$s"; //$NON-NLS-1$
+
+
+    /** The name of the uses-library that provides "android.test.runner" */
+    public static final String ANDROID_TEST_RUNNER_LIB =
+        "android.test.runner";                                          //$NON-NLS-1$
+
+    /* Folder path relative to the SDK root */
+    /** Path of the documentation directory relative to the sdk folder.
+     *  This is an OS path, ending with a separator. */
+    public static final String OS_SDK_DOCS_FOLDER = FD_DOCS + File.separator;
+
+    /** Path of the tools directory relative to the sdk folder, or to a platform folder.
+     *  This is an OS path, ending with a separator. */
+    public static final String OS_SDK_TOOLS_FOLDER = FD_TOOLS + File.separator;
+
+    /** Path of the lib directory relative to the sdk folder, or to a platform folder.
+     *  This is an OS path, ending with a separator. */
+    public static final String OS_SDK_TOOLS_LIB_FOLDER =
+            OS_SDK_TOOLS_FOLDER + FD_LIB + File.separator;
+
+    /**
+     * Path of the lib directory relative to the sdk folder, or to a platform
+     * folder. This is an OS path, ending with a separator.
+     */
+    public static final String OS_SDK_TOOLS_LIB_EMULATOR_FOLDER = OS_SDK_TOOLS_LIB_FOLDER
+            + "emulator" + File.separator;                              //$NON-NLS-1$
+
+    /** Path of the platform tools directory relative to the sdk folder.
+     *  This is an OS path, ending with a separator. */
+    public static final String OS_SDK_PLATFORM_TOOLS_FOLDER = FD_PLATFORM_TOOLS + File.separator;
+
+    /** Path of the build tools directory relative to the sdk folder.
+     *  This is an OS path, ending with a separator. */
+    public static final String OS_SDK_BUILD_TOOLS_FOLDER = FD_BUILD_TOOLS + File.separator;
+
+    /** Path of the Platform tools Lib directory relative to the sdk folder.
+     *  This is an OS path, ending with a separator. */
+    public static final String OS_SDK_PLATFORM_TOOLS_LIB_FOLDER =
+            OS_SDK_PLATFORM_TOOLS_FOLDER + FD_LIB + File.separator;
+
+    /** Path of the bin folder of proguard folder relative to the sdk folder.
+     *  This is an OS path, ending with a separator. */
+    public static final String OS_SDK_TOOLS_PROGUARD_BIN_FOLDER =
+        SdkConstants.OS_SDK_TOOLS_FOLDER +
+        "proguard" + File.separator +                                   //$NON-NLS-1$
+        "bin" + File.separator;                                         //$NON-NLS-1$
+
+    /* Folder paths relative to a platform or add-on folder */
+
+    /** Path of the images directory relative to a platform or addon folder.
+     *  This is an OS path, ending with a separator. */
+    public static final String OS_IMAGES_FOLDER = FD_IMAGES + File.separator;
+
+    /** Path of the skin directory relative to a platform or addon folder.
+     *  This is an OS path, ending with a separator. */
+    public static final String OS_SKINS_FOLDER = FD_SKINS + File.separator;
+
+    /* Folder paths relative to a Platform folder */
+
+    /** Path of the data directory relative to a platform folder.
+     *  This is an OS path, ending with a separator. */
+    public static final String OS_PLATFORM_DATA_FOLDER = FD_DATA + File.separator;
+
+    /** Path of the renderscript directory relative to a platform folder.
+     *  This is an OS path, ending with a separator. */
+    public static final String OS_PLATFORM_RENDERSCRIPT_FOLDER = FD_RENDERSCRIPT + File.separator;
+
+
+    /** Path of the samples directory relative to a platform folder.
+     *  This is an OS path, ending with a separator. */
+    public static final String OS_PLATFORM_SAMPLES_FOLDER = FD_SAMPLES + File.separator;
+
+    /** Path of the resources directory relative to a platform folder.
+     *  This is an OS path, ending with a separator. */
+    public static final String OS_PLATFORM_RESOURCES_FOLDER =
+            OS_PLATFORM_DATA_FOLDER + FD_RES + File.separator;
+
+    /** Path of the fonts directory relative to a platform folder.
+     *  This is an OS path, ending with a separator. */
+    public static final String OS_PLATFORM_FONTS_FOLDER =
+            OS_PLATFORM_DATA_FOLDER + FD_FONTS + File.separator;
+
+    /** Path of the android source directory relative to a platform folder.
+     *  This is an OS path, ending with a separator. */
+    public static final String OS_PLATFORM_SOURCES_FOLDER = FD_ANDROID_SOURCES + File.separator;
+
+    /** Path of the android templates directory relative to a platform folder.
+     *  This is an OS path, ending with a separator. */
+    public static final String OS_PLATFORM_TEMPLATES_FOLDER = FD_TEMPLATES + File.separator;
+
+    /** Path of the Ant build rules directory relative to a platform folder.
+     *  This is an OS path, ending with a separator. */
+    public static final String OS_PLATFORM_ANT_FOLDER = FD_ANT + File.separator;
+
+    /** Path of the attrs.xml file relative to a platform folder. */
+    public static final String OS_PLATFORM_ATTRS_XML =
+            OS_PLATFORM_RESOURCES_FOLDER + SdkConstants.FD_RES_VALUES + File.separator +
+            FN_ATTRS_XML;
+
+    /** Path of the attrs_manifest.xml file relative to a platform folder. */
+    public static final String OS_PLATFORM_ATTRS_MANIFEST_XML =
+            OS_PLATFORM_RESOURCES_FOLDER + SdkConstants.FD_RES_VALUES + File.separator +
+            FN_ATTRS_MANIFEST_XML;
+
+    /** Path of the layoutlib.jar file relative to a platform folder. */
+    public static final String OS_PLATFORM_LAYOUTLIB_JAR =
+            OS_PLATFORM_DATA_FOLDER + FN_LAYOUTLIB_JAR;
+
+    /** Path of the renderscript include folder relative to a platform folder. */
+    public static final String OS_FRAMEWORK_RS =
+            FN_FRAMEWORK_RENDERSCRIPT + File.separator + FN_FRAMEWORK_INCLUDE;
+    /** Path of the renderscript (clang) include folder relative to a platform folder. */
+    public static final String OS_FRAMEWORK_RS_CLANG =
+            FN_FRAMEWORK_RENDERSCRIPT + File.separator + FN_FRAMEWORK_INCLUDE_CLANG;
+
+    /* Folder paths relative to a addon folder */
+    /** Path of the images directory relative to a folder folder.
+     *  This is an OS path, ending with a separator. */
+    public static final String OS_ADDON_LIBS_FOLDER = FD_ADDON_LIBS + File.separator;
+
+    /** Skin default **/
+    public static final String SKIN_DEFAULT = "default";                    //$NON-NLS-1$
+
+    /** SDK property: ant templates revision */
+    public static final String PROP_SDK_ANT_TEMPLATES_REVISION =
+        "sdk.ant.templates.revision";                                       //$NON-NLS-1$
+
+    /** SDK property: default skin */
+    public static final String PROP_SDK_DEFAULT_SKIN = "sdk.skin.default"; //$NON-NLS-1$
+
+    /* Android Class Constants */
+    public static final String CLASS_ACTIVITY = "android.app.Activity"; //$NON-NLS-1$
+    public static final String CLASS_APPLICATION = "android.app.Application"; //$NON-NLS-1$
+    public static final String CLASS_SERVICE = "android.app.Service"; //$NON-NLS-1$
+    public static final String CLASS_BROADCASTRECEIVER = "android.content.BroadcastReceiver"; //$NON-NLS-1$
+    public static final String CLASS_CONTENTPROVIDER = "android.content.ContentProvider"; //$NON-NLS-1$
+    public static final String CLASS_INSTRUMENTATION = "android.app.Instrumentation"; //$NON-NLS-1$
+    public static final String CLASS_INSTRUMENTATION_RUNNER =
+        "android.test.InstrumentationTestRunner"; //$NON-NLS-1$
+    public static final String CLASS_BUNDLE = "android.os.Bundle"; //$NON-NLS-1$
+    public static final String CLASS_R = "android.R"; //$NON-NLS-1$
+    public static final String CLASS_MANIFEST_PERMISSION = "android.Manifest$permission"; //$NON-NLS-1$
+    public static final String CLASS_INTENT = "android.content.Intent"; //$NON-NLS-1$
+    public static final String CLASS_CONTEXT = "android.content.Context"; //$NON-NLS-1$
+    public static final String CLASS_VIEW = "android.view.View"; //$NON-NLS-1$
+    public static final String CLASS_VIEWGROUP = "android.view.ViewGroup"; //$NON-NLS-1$
+    public static final String CLASS_NAME_LAYOUTPARAMS = "LayoutParams"; //$NON-NLS-1$
+    public static final String CLASS_VIEWGROUP_LAYOUTPARAMS =
+        CLASS_VIEWGROUP + "$" + CLASS_NAME_LAYOUTPARAMS; //$NON-NLS-1$
+    public static final String CLASS_NAME_FRAMELAYOUT = "FrameLayout"; //$NON-NLS-1$
+    public static final String CLASS_FRAMELAYOUT =
+        "android.widget." + CLASS_NAME_FRAMELAYOUT; //$NON-NLS-1$
+    public static final String CLASS_PREFERENCE = "android.preference.Preference"; //$NON-NLS-1$
+    public static final String CLASS_NAME_PREFERENCE_SCREEN = "PreferenceScreen"; //$NON-NLS-1$
+    public static final String CLASS_PREFERENCES =
+        "android.preference." + CLASS_NAME_PREFERENCE_SCREEN; //$NON-NLS-1$
+    public static final String CLASS_PREFERENCEGROUP = "android.preference.PreferenceGroup"; //$NON-NLS-1$
+    public static final String CLASS_PARCELABLE = "android.os.Parcelable"; //$NON-NLS-1$
+    public static final String CLASS_FRAGMENT = "android.app.Fragment"; //$NON-NLS-1$
+    public static final String CLASS_V4_FRAGMENT = "android.support.v4.app.Fragment"; //$NON-NLS-1$
+    /** MockView is part of the layoutlib bridge and used to display classes that have
+     * no rendering in the graphical layout editor. */
+    public static final String CLASS_MOCK_VIEW = "com.android.layoutlib.bridge.MockView"; //$NON-NLS-1$
+
+    /** Returns the appropriate name for the 'android' command, which is 'android.exe' for
+     * Windows and 'android' for all other platforms. */
+    public static String androidCmdName() {
+        String os = System.getProperty("os.name");          //$NON-NLS-1$
+        String cmd = "android";                             //$NON-NLS-1$
+        if (os.startsWith("Windows")) {                     //$NON-NLS-1$
+            cmd += ".bat";                                  //$NON-NLS-1$
+        }
+        return cmd;
+    }
+
+    /** Returns the appropriate name for the 'mksdcard' command, which is 'mksdcard.exe' for
+     * Windows and 'mkdsdcard' for all other platforms. */
+    public static String mkSdCardCmdName() {
+        String os = System.getProperty("os.name");          //$NON-NLS-1$
+        String cmd = "mksdcard";                            //$NON-NLS-1$
+        if (os.startsWith("Windows")) {                     //$NON-NLS-1$
+            cmd += ".exe";                                  //$NON-NLS-1$
+        }
+        return cmd;
+    }
+
+    /**
+     * Returns current platform
+     *
+     * @return one of {@link #PLATFORM_WINDOWS}, {@link #PLATFORM_DARWIN},
+     * {@link #PLATFORM_LINUX} or {@link #PLATFORM_UNKNOWN}.
+     */
+    public static int currentPlatform() {
+        String os = System.getProperty("os.name");          //$NON-NLS-1$
+        if (os.startsWith("Mac OS")) {                      //$NON-NLS-1$
+            return PLATFORM_DARWIN;
+        } else if (os.startsWith("Windows")) {              //$NON-NLS-1$
+            return PLATFORM_WINDOWS;
+        } else if (os.startsWith("Linux")) {                //$NON-NLS-1$
+            return PLATFORM_LINUX;
+        }
+
+        return PLATFORM_UNKNOWN;
+    }
+
+    /**
+     * Returns current platform's UI name
+     *
+     * @return one of "Windows", "Mac OS X", "Linux" or "other".
+     */
+    public static String currentPlatformName() {
+        String os = System.getProperty("os.name");          //$NON-NLS-1$
+        if (os.startsWith("Mac OS")) {                      //$NON-NLS-1$
+            return "Mac OS X";                              //$NON-NLS-1$
+        } else if (os.startsWith("Windows")) {              //$NON-NLS-1$
+            return "Windows";                               //$NON-NLS-1$
+        } else if (os.startsWith("Linux")) {                //$NON-NLS-1$
+            return "Linux";                                 //$NON-NLS-1$
+        }
+
+        return "Other";
+    }
+
+    private static String ext(String windowsExtension, String nonWindowsExtension) {
+        if (CURRENT_PLATFORM == PLATFORM_WINDOWS) {
+            return windowsExtension;
+        } else {
+            return nonWindowsExtension;
+        }
+    }
+
+    /** Default anim resource folder name, i.e. "anim" */
+    public static final String FD_RES_ANIM = "anim"; //$NON-NLS-1$
+    /** Default animator resource folder name, i.e. "animator" */
+    public static final String FD_RES_ANIMATOR = "animator"; //$NON-NLS-1$
+    /** Default color resource folder name, i.e. "color" */
+    public static final String FD_RES_COLOR = "color"; //$NON-NLS-1$
+    /** Default drawable resource folder name, i.e. "drawable" */
+    public static final String FD_RES_DRAWABLE = "drawable"; //$NON-NLS-1$
+    /** Default interpolator resource folder name, i.e. "interpolator" */
+    public static final String FD_RES_INTERPOLATOR = "interpolator"; //$NON-NLS-1$
+    /** Default layout resource folder name, i.e. "layout" */
+    public static final String FD_RES_LAYOUT = "layout"; //$NON-NLS-1$
+    /** Default menu resource folder name, i.e. "menu" */
+    public static final String FD_RES_MENU = "menu"; //$NON-NLS-1$
+    /** Default menu resource folder name, i.e. "mipmap" */
+    public static final String FD_RES_MIPMAP = "mipmap"; //$NON-NLS-1$
+    /** Default values resource folder name, i.e. "values" */
+    public static final String FD_RES_VALUES = "values"; //$NON-NLS-1$
+    /** Default xml resource folder name, i.e. "xml" */
+    public static final String FD_RES_XML = "xml"; //$NON-NLS-1$
+    /** Default raw resource folder name, i.e. "raw" */
+    public static final String FD_RES_RAW = "raw"; //$NON-NLS-1$
+    /** Separator between the resource folder qualifier. */
+    public static final String RES_QUALIFIER_SEP = "-"; //$NON-NLS-1$
+    /** Namespace used in XML files for Android attributes */
+
+    // ---- XML ----
+
+    /** URI of the reserved "xmlns"  prefix */
+    public static final String XMLNS_URI = "http://www.w3.org/2000/xmlns/";  //$NON-NLS-1$
+    /** The "xmlns" attribute name */
+    public static final String XMLNS = "xmlns";                              //$NON-NLS-1$
+    /** The default prefix used for the {@link #XMLNS_URI} */
+    public static final String XMLNS_PREFIX = "xmlns:";                      //$NON-NLS-1$
+    /** Qualified name of the xmlns android declaration element */
+    public static final String XMLNS_ANDROID = "xmlns:android";              //$NON-NLS-1$
+    /** The default prefix used for the {@link #ANDROID_URI} name space */
+    public static final String ANDROID_NS_NAME = "android";                  //$NON-NLS-1$
+    /** The default prefix used for the {@link #ANDROID_URI} name space including the colon  */
+    public static final String ANDROID_NS_NAME_PREFIX = "android:";          //$NON-NLS-1$
+    /** The default prefix used for the app */
+    public static final String APP_PREFIX = "app";                          //$NON-NLS-1$
+    /** The entity for the ampersand character */
+    public static final String AMP_ENTITY = "&";                         //$NON-NLS-1$
+    /** The entity for the quote character */
+    public static final String QUOT_ENTITY = """;                       //$NON-NLS-1$
+    /** The entity for the apostrophe character */
+    public static final String APOS_ENTITY = "'";                       //$NON-NLS-1$
+    /** The entity for the less than character */
+    public static final String LT_ENTITY = "<";                           //$NON-NLS-1$
+    /** The entity for the greater than character */
+    public static final String GT_ENTITY = ">";                           //$NON-NLS-1$
+
+    // ---- Elements and Attributes ----
+
+    /** Namespace prefix used for all resources */
+    public static final String URI_PREFIX =
+            "http://schemas.android.com/apk/res/";                     //$NON-NLS-1$
+    /** Namespace used in XML files for Android attributes */
+    public static final String ANDROID_URI =
+            "http://schemas.android.com/apk/res/android";              //$NON-NLS-1$
+    /** Namespace used in XML files for Android Tooling attributes */
+    public static final String TOOLS_URI =
+            "http://schemas.android.com/tools";                        //$NON-NLS-1$
+    /** Namespace used for auto-adjusting namespaces */
+    public static final String AUTO_URI =
+            "http://schemas.android.com/apk/res-auto";                 //$NON-NLS-1$
+    /** Default prefix used for tools attributes */
+    public static final String TOOLS_PREFIX = "tools";                 //$NON-NLS-1$
+    public static final String R_CLASS = "R";                          //$NON-NLS-1$
+    public static final String ANDROID_PKG = "android";                //$NON-NLS-1$
+
+    // Tags: Manifest
+    public static final String TAG_SERVICE = "service";                //$NON-NLS-1$
+    public static final String TAG_PERMISSION = "permission";          //$NON-NLS-1$
+    public static final String TAG_USES_FEATURE = "uses-feature";      //$NON-NLS-1$
+    public static final String TAG_USES_PERMISSION = "uses-permission";//$NON-NLS-1$
+    public static final String TAG_USES_LIBRARY = "uses-library";      //$NON-NLS-1$
+    public static final String TAG_APPLICATION = "application";        //$NON-NLS-1$
+    public static final String TAG_INTENT_FILTER = "intent-filter";    //$NON-NLS-1$
+    public static final String TAG_USES_SDK = "uses-sdk";              //$NON-NLS-1$
+    public static final String TAG_ACTIVITY = "activity";              //$NON-NLS-1$
+    public static final String TAG_RECEIVER = "receiver";              //$NON-NLS-1$
+    public static final String TAG_PROVIDER = "provider";              //$NON-NLS-1$
+    public static final String TAG_GRANT_PERMISSION = "grant-uri-permission"; //$NON-NLS-1$
+    public static final String TAG_PATH_PERMISSION = "path-permission"; //$NON-NLS-1$
+
+    // Tags: Resources
+    public static final String TAG_RESOURCES = "resources";            //$NON-NLS-1$
+    public static final String TAG_STRING = "string";                  //$NON-NLS-1$
+    public static final String TAG_ARRAY = "array";                    //$NON-NLS-1$
+    public static final String TAG_STYLE = "style";                    //$NON-NLS-1$
+    public static final String TAG_ITEM = "item";                      //$NON-NLS-1$
+    public static final String TAG_STRING_ARRAY = "string-array";      //$NON-NLS-1$
+    public static final String TAG_PLURALS = "plurals";                //$NON-NLS-1$
+    public static final String TAG_INTEGER_ARRAY = "integer-array";    //$NON-NLS-1$
+    public static final String TAG_COLOR = "color";                    //$NON-NLS-1$
+    public static final String TAG_DIMEN = "dimen";                    //$NON-NLS-1$
+    public static final String TAG_DRAWABLE = "drawable";              //$NON-NLS-1$
+    public static final String TAG_MENU = "menu";                      //$NON-NLS-1$
+
+    // Tags: XML
+    public static final String TAG_HEADER = "header";                  //$NON-NLS-1$
+
+    // Tags: Layouts
+    public static final String VIEW_TAG = "view";                      //$NON-NLS-1$
+    public static final String VIEW_INCLUDE = "include";               //$NON-NLS-1$
+    public static final String VIEW_MERGE = "merge";                   //$NON-NLS-1$
+    public static final String VIEW_FRAGMENT = "fragment";             //$NON-NLS-1$
+    public static final String REQUEST_FOCUS = "requestFocus";         //$NON-NLS-1$
+
+    public static final String VIEW = "View";                          //$NON-NLS-1$
+    public static final String VIEW_GROUP = "ViewGroup";               //$NON-NLS-1$
+    public static final String FRAME_LAYOUT = "FrameLayout";           //$NON-NLS-1$
+    public static final String LINEAR_LAYOUT = "LinearLayout";         //$NON-NLS-1$
+    public static final String RELATIVE_LAYOUT = "RelativeLayout";     //$NON-NLS-1$
+    public static final String GRID_LAYOUT = "GridLayout";             //$NON-NLS-1$
+    public static final String SCROLL_VIEW = "ScrollView";             //$NON-NLS-1$
+    public static final String BUTTON = "Button";                      //$NON-NLS-1$
+    public static final String COMPOUND_BUTTON = "CompoundButton";     //$NON-NLS-1$
+    public static final String ADAPTER_VIEW = "AdapterView";           //$NON-NLS-1$
+    public static final String GALLERY = "Gallery";                    //$NON-NLS-1$
+    public static final String GRID_VIEW = "GridView";                 //$NON-NLS-1$
+    public static final String TAB_HOST = "TabHost";                   //$NON-NLS-1$
+    public static final String RADIO_GROUP = "RadioGroup";             //$NON-NLS-1$
+    public static final String RADIO_BUTTON = "RadioButton";           //$NON-NLS-1$
+    public static final String SWITCH = "Switch";                      //$NON-NLS-1$
+    public static final String EDIT_TEXT = "EditText";                 //$NON-NLS-1$
+    public static final String LIST_VIEW = "ListView";                 //$NON-NLS-1$
+    public static final String TEXT_VIEW = "TextView";                 //$NON-NLS-1$
+    public static final String CHECKED_TEXT_VIEW = "CheckedTextView";  //$NON-NLS-1$
+    public static final String IMAGE_VIEW = "ImageView";               //$NON-NLS-1$
+    public static final String SURFACE_VIEW = "SurfaceView";           //$NON-NLS-1$
+    public static final String ABSOLUTE_LAYOUT = "AbsoluteLayout";     //$NON-NLS-1$
+    public static final String TABLE_LAYOUT = "TableLayout";           //$NON-NLS-1$
+    public static final String TABLE_ROW = "TableRow";                 //$NON-NLS-1$
+    public static final String TAB_WIDGET = "TabWidget";               //$NON-NLS-1$
+    public static final String IMAGE_BUTTON = "ImageButton";           //$NON-NLS-1$
+    public static final String SEEK_BAR = "SeekBar";                   //$NON-NLS-1$
+    public static final String VIEW_STUB = "ViewStub";                 //$NON-NLS-1$
+    public static final String SPINNER = "Spinner";                    //$NON-NLS-1$
+    public static final String WEB_VIEW = "WebView";                   //$NON-NLS-1$
+    public static final String TOGGLE_BUTTON = "ToggleButton";         //$NON-NLS-1$
+    public static final String CHECK_BOX = "CheckBox";                 //$NON-NLS-1$
+    public static final String ABS_LIST_VIEW = "AbsListView";          //$NON-NLS-1$
+    public static final String PROGRESS_BAR = "ProgressBar";           //$NON-NLS-1$
+    public static final String ABS_SPINNER = "AbsSpinner";             //$NON-NLS-1$
+    public static final String ABS_SEEK_BAR = "AbsSeekBar";            //$NON-NLS-1$
+    public static final String VIEW_ANIMATOR = "ViewAnimator";         //$NON-NLS-1$
+    public static final String VIEW_SWITCHER = "ViewSwitcher";         //$NON-NLS-1$
+    public static final String EXPANDABLE_LIST_VIEW = "ExpandableListView";    //$NON-NLS-1$
+    public static final String HORIZONTAL_SCROLL_VIEW = "HorizontalScrollView"; //$NON-NLS-1$
+    public static final String MULTI_AUTO_COMPLETE_TEXT_VIEW = "MultiAutoCompleteTextView"; //$NON-NLS-1$
+    public static final String AUTO_COMPLETE_TEXT_VIEW = "AutoCompleteTextView"; //$NON-NLS-1$
+    public static final String CHECKABLE = "Checkable";                //$NON-NLS-1$
+
+    // Tags: Drawables
+    public static final String TAG_BITMAP = "bitmap";                  //$NON-NLS-1$
+
+    // Attributes: Manifest
+    public static final String ATTR_EXPORTED = "exported";             //$NON-NLS-1$
+    public static final String ATTR_PERMISSION = "permission";         //$NON-NLS-1$
+    public static final String ATTR_MIN_SDK_VERSION = "minSdkVersion"; //$NON-NLS-1$
+    public static final String ATTR_TARGET_SDK_VERSION = "targetSdkVersion"; //$NON-NLS-1$
+    public static final String ATTR_ICON = "icon";                     //$NON-NLS-1$
+    public static final String ATTR_PACKAGE = "package";               //$NON-NLS-1$
+    public static final String ATTR_CORE_APP = "coreApp";              //$NON-NLS-1$
+    public static final String ATTR_THEME = "theme";                   //$NON-NLS-1$
+    public static final String ATTR_PATH = "path";                     //$NON-NLS-1$
+    public static final String ATTR_PATH_PREFIX = "pathPrefix";        //$NON-NLS-1$
+    public static final String ATTR_PATH_PATTERN = "pathPattern";      //$NON-NLS-1$
+    public static final String ATTR_ALLOW_BACKUP = "allowBackup";      //$NON_NLS-1$
+    public static final String ATTR_DEBUGGABLE = "debuggable";         //$NON-NLS-1$
+    public static final String ATTR_READ_PERMISSION = "readPermission"; //$NON_NLS-1$
+    public static final String ATTR_WRITE_PERMISSION = "writePermission"; //$NON_NLS-1$
+
+    // Attributes: Resources
+    public static final String ATTR_NAME = "name";                     //$NON-NLS-1$
+    public static final String ATTR_FRAGMENT = "fragment";             //$NON-NLS-1$
+    public static final String ATTR_TYPE = "type";                     //$NON-NLS-1$
+    public static final String ATTR_PARENT = "parent";                 //$NON-NLS-1$
+    public static final String ATTR_TRANSLATABLE = "translatable";     //$NON-NLS-1$
+    public static final String ATTR_COLOR = "color";                   //$NON-NLS-1$
+
+    // Attributes: Layout
+    public static final String ATTR_LAYOUT_RESOURCE_PREFIX = "layout_";//$NON-NLS-1$
+    public static final String ATTR_CLASS = "class";                   //$NON-NLS-1$
+    public static final String ATTR_STYLE = "style";                   //$NON-NLS-1$
+    public static final String ATTR_CONTEXT = "context";               //$NON-NLS-1$
+    public static final String ATTR_ID = "id";                         //$NON-NLS-1$
+    public static final String ATTR_TEXT = "text";                     //$NON-NLS-1$
+    public static final String ATTR_TEXT_SIZE = "textSize";            //$NON-NLS-1$
+    public static final String ATTR_LABEL = "label";                   //$NON-NLS-1$
+    public static final String ATTR_HINT = "hint";                     //$NON-NLS-1$
+    public static final String ATTR_PROMPT = "prompt";                 //$NON-NLS-1$
+    public static final String ATTR_ON_CLICK = "onClick";              //$NON-NLS-1$
+    public static final String ATTR_INPUT_TYPE = "inputType";          //$NON-NLS-1$
+    public static final String ATTR_INPUT_METHOD = "inputMethod";      //$NON-NLS-1$
+    public static final String ATTR_LAYOUT_GRAVITY = "layout_gravity"; //$NON-NLS-1$
+    public static final String ATTR_LAYOUT_WIDTH = "layout_width";     //$NON-NLS-1$
+    public static final String ATTR_LAYOUT_HEIGHT = "layout_height";   //$NON-NLS-1$
+    public static final String ATTR_LAYOUT_WEIGHT = "layout_weight";   //$NON-NLS-1$
+    public static final String ATTR_PADDING = "padding";               //$NON-NLS-1$
+    public static final String ATTR_PADDING_BOTTOM = "paddingBottom";  //$NON-NLS-1$
+    public static final String ATTR_PADDING_TOP = "paddingTop";        //$NON-NLS-1$
+    public static final String ATTR_PADDING_RIGHT = "paddingRight";    //$NON-NLS-1$
+    public static final String ATTR_PADDING_LEFT = "paddingLeft";      //$NON-NLS-1$
+    public static final String ATTR_FOREGROUND = "foreground";         //$NON-NLS-1$
+    public static final String ATTR_BACKGROUND = "background";         //$NON-NLS-1$
+    public static final String ATTR_ORIENTATION = "orientation";       //$NON-NLS-1$
+    public static final String ATTR_LAYOUT = "layout";                 //$NON-NLS-1$
+    public static final String ATTR_ROW_COUNT = "rowCount";            //$NON-NLS-1$
+    public static final String ATTR_COLUMN_COUNT = "columnCount";      //$NON-NLS-1$
+    public static final String ATTR_LABEL_FOR = "labelFor";            //$NON-NLS-1$
+    public static final String ATTR_BASELINE_ALIGNED = "baselineAligned";       //$NON-NLS-1$
+    public static final String ATTR_CONTENT_DESCRIPTION = "contentDescription"; //$NON-NLS-1$
+    public static final String ATTR_IME_ACTION_LABEL = "imeActionLabel";        //$NON-NLS-1$
+    public static final String ATTR_PRIVATE_IME_OPTIONS = "privateImeOptions";  //$NON-NLS-1$
+    public static final String VALUE_NONE = "none";                    //$NON-NLS-1$
+    public static final String VALUE_NO = "no";                        //$NON-NLS-1$
+    public static final String ATTR_NUMERIC = "numeric";               //$NON-NLS-1$
+    public static final String ATTR_IME_ACTION_ID = "imeActionId";     //$NON-NLS-1$
+    public static final String ATTR_IME_OPTIONS = "imeOptions";        //$NON-NLS-1$
+    public static final String ATTR_FREEZES_TEXT = "freezesText";      //$NON-NLS-1$
+    public static final String ATTR_EDITOR_EXTRAS = "editorExtras";    //$NON-NLS-1$
+    public static final String ATTR_EDITABLE = "editable";             //$NON-NLS-1$
+    public static final String ATTR_DIGITS = "digits";                 //$NON-NLS-1$
+    public static final String ATTR_CURSOR_VISIBLE = "cursorVisible";  //$NON-NLS-1$
+    public static final String ATTR_CAPITALIZE = "capitalize";         //$NON-NLS-1$
+    public static final String ATTR_PHONE_NUMBER = "phoneNumber";      //$NON-NLS-1$
+    public static final String ATTR_PASSWORD = "password";             //$NON-NLS-1$
+    public static final String ATTR_BUFFER_TYPE = "bufferType";        //$NON-NLS-1$
+    public static final String ATTR_AUTO_TEXT = "autoText";            //$NON-NLS-1$
+    public static final String ATTR_ENABLED = "enabled";               //$NON-NLS-1$
+    public static final String ATTR_SINGLE_LINE = "singleLine";        //$NON-NLS-1$
+    public static final String ATTR_SCALE_TYPE = "scaleType";          //$NON-NLS-1$
+    public static final String ATTR_VISIBILITY = "visibility";         //$NON-NLS-1$
+    public static final String ATTR_TEXT_IS_SELECTABLE =
+            "textIsSelectable";                                        //$NON-NLS-1$
+    public static final String ATTR_IMPORTANT_FOR_ACCESSIBILITY =
+            "importantForAccessibility";                               //$NON-NLS-1$
+
+    // AbsoluteLayout layout params
+    public static final String ATTR_LAYOUT_Y = "layout_y";             //$NON-NLS-1$
+    public static final String ATTR_LAYOUT_X = "layout_x";             //$NON-NLS-1$
+
+    // GridLayout layout params
+    public static final String ATTR_LAYOUT_ROW = "layout_row";         //$NON-NLS-1$
+    public static final String ATTR_LAYOUT_ROW_SPAN = "layout_rowSpan";//$NON-NLS-1$
+    public static final String ATTR_LAYOUT_COLUMN = "layout_column";   //$NON-NLS-1$
+    public static final String ATTR_LAYOUT_COLUMN_SPAN = "layout_columnSpan";       //$NON-NLS-1$
+
+    // TableRow
+    public static final String ATTR_LAYOUT_SPAN = "layout_span";       //$NON-NLS-1$
+
+    // RelativeLayout layout params:
+    public static final String ATTR_LAYOUT_ALIGN_LEFT = "layout_alignLeft";        //$NON-NLS-1$
+    public static final String ATTR_LAYOUT_ALIGN_RIGHT = "layout_alignRight";      //$NON-NLS-1$
+    public static final String ATTR_LAYOUT_ALIGN_TOP = "layout_alignTop";          //$NON-NLS-1$
+    public static final String ATTR_LAYOUT_ALIGN_BOTTOM = "layout_alignBottom";    //$NON-NLS-1$
+    public static final String ATTR_LAYOUT_ALIGN_PARENT_TOP = "layout_alignParentTop"; //$NON-NLS-1$
+    public static final String ATTR_LAYOUT_ALIGN_PARENT_BOTTOM = "layout_alignParentBottom"; //$NON-NLS-1$
+    public static final String ATTR_LAYOUT_ALIGN_PARENT_LEFT = "layout_alignParentLeft";//$NON-NLS-1$
+    public static final String ATTR_LAYOUT_ALIGN_PARENT_RIGHT = "layout_alignParentRight";   //$NON-NLS-1$
+    public static final String ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING = "layout_alignWithParentIfMissing"; //$NON-NLS-1$
+    public static final String ATTR_LAYOUT_ALIGN_BASELINE = "layout_alignBaseline"; //$NON-NLS-1$
+    public static final String ATTR_LAYOUT_CENTER_IN_PARENT = "layout_centerInParent"; //$NON-NLS-1$
+    public static final String ATTR_LAYOUT_CENTER_VERTICAL = "layout_centerVertical"; //$NON-NLS-1$
+    public static final String ATTR_LAYOUT_CENTER_HORIZONTAL = "layout_centerHorizontal"; //$NON-NLS-1$
+    public static final String ATTR_LAYOUT_TO_RIGHT_OF = "layout_toRightOf";    //$NON-NLS-1$
+    public static final String ATTR_LAYOUT_TO_LEFT_OF = "layout_toLeftOf";      //$NON-NLS-1$
+    public static final String ATTR_LAYOUT_BELOW = "layout_below";              //$NON-NLS-1$
+    public static final String ATTR_LAYOUT_ABOVE = "layout_above";              //$NON-NLS-1$
+
+    // Margins
+    public static final String ATTR_LAYOUT_MARGIN = "layout_margin";               //$NON-NLS-1$
+    public static final String ATTR_LAYOUT_MARGIN_LEFT = "layout_marginLeft";      //$NON-NLS-1$
+    public static final String ATTR_LAYOUT_MARGIN_RIGHT = "layout_marginRight";    //$NON-NLS-1$
+    public static final String ATTR_LAYOUT_MARGIN_TOP = "layout_marginTop";        //$NON-NLS-1$
+    public static final String ATTR_LAYOUT_MARGIN_BOTTOM = "layout_marginBottom";  //$NON-NLS-1$
+
+    // Attributes: Drawables
+    public static final String ATTR_TILE_MODE = "tileMode";            //$NON-NLS-1$
+
+    // Values: Layouts
+    public static final String VALUE_FILL_PARENT = "fill_parent";       //$NON-NLS-1$
+    public static final String VALUE_MATCH_PARENT = "match_parent";     //$NON-NLS-1$
+    public static final String VALUE_VERTICAL = "vertical";             //$NON-NLS-1$
+    public static final String VALUE_TRUE = "true";                     //$NON-NLS-1$
+    public static final String VALUE_EDITABLE = "editable";             //$NON-NLS-1$
+    public static final String VALUE_AUTO_FIT = "auto_fit";             //$NON-NLS-1$
+    public static final String VALUE_SELECTABLE_ITEM_BACKGROUND =
+            "?android:attr/selectableItemBackground";                   //$NON-NLS-1$
+
+
+    // Values: Resources
+    public static final String VALUE_ID = "id";                        //$NON-NLS-1$
+
+    // Values: Drawables
+    public static final String VALUE_DISABLED = "disabled";            //$NON-NLS-1$
+    public static final String VALUE_CLAMP = "clamp";                  //$NON-NLS-1$
+
+    // Menus
+    public static final String ATTR_SHOW_AS_ACTION = "showAsAction";   //$NON-NLS-1$
+    public static final String ATTR_TITLE = "title";                   //$NON-NLS-1$
+    public static final String ATTR_VISIBLE = "visible";               //$NON-NLS-1$
+    public static final String VALUE_IF_ROOM = "ifRoom";               //$NON-NLS-1$
+    public static final String VALUE_ALWAYS = "always";                //$NON-NLS-1$
+
+    // Units
+    public static final String UNIT_DP = "dp";                         //$NON-NLS-1$
+    public static final String UNIT_DIP = "dip";                       //$NON-NLS-1$
+    public static final String UNIT_SP = "sp";                         //$NON-NLS-1$
+    public static final String UNIT_PX = "px";                         //$NON-NLS-1$
+    public static final String UNIT_IN = "in";                         //$NON-NLS-1$
+    public static final String UNIT_MM = "mm";                         //$NON-NLS-1$
+    public static final String UNIT_PT = "pt";                         //$NON-NLS-1$
+
+    // Filenames and folder names
+    public static final String ANDROID_MANIFEST_XML = "AndroidManifest.xml"; //$NON-NLS-1$
+    public static final String OLD_PROGUARD_FILE = "proguard.cfg";     //$NON-NLS-1$
+    public static final String CLASS_FOLDER =
+            "bin" + File.separator + "classes";                        //$NON-NLS-1$ //$NON-NLS-2$
+    public static final String GEN_FOLDER = "gen";                     //$NON-NLS-1$
+    public static final String SRC_FOLDER = "src";                     //$NON-NLS-1$
+    public static final String LIBS_FOLDER = "libs";                   //$NON-NLS-1$
+    public static final String BIN_FOLDER = "bin";                     //$NON-NLS-1$
+
+    public static final String RES_FOLDER = "res";                     //$NON-NLS-1$
+    public static final String DOT_XML = ".xml";                       //$NON-NLS-1$
+    public static final String DOT_GIF = ".gif";                       //$NON-NLS-1$
+    public static final String DOT_JPG = ".jpg";                       //$NON-NLS-1$
+    public static final String DOT_JPEG = ".jpeg";                     //$NON-NLS-1$
+    public static final String DOT_PNG = ".png";                       //$NON-NLS-1$
+    public static final String DOT_9PNG = ".9.png";                    //$NON-NLS-1$
+    public static final String DOT_JAVA = ".java";                     //$NON-NLS-1$
+    public static final String DOT_CLASS = ".class";                   //$NON-NLS-1$
+    public static final String DOT_JAR = ".jar";                       //$NON-NLS-1$
+
+
+    /** Extension of the Application package Files, i.e. "apk". */
+    public static final String EXT_ANDROID_PACKAGE = "apk"; //$NON-NLS-1$
+    /** Extension of java files, i.e. "java" */
+    public static final String EXT_JAVA = "java"; //$NON-NLS-1$
+    /** Extension of compiled java files, i.e. "class" */
+    public static final String EXT_CLASS = "class"; //$NON-NLS-1$
+    /** Extension of xml files, i.e. "xml" */
+    public static final String EXT_XML = "xml"; //$NON-NLS-1$
+    /** Extension of jar files, i.e. "jar" */
+    public static final String EXT_JAR = "jar"; //$NON-NLS-1$
+    /** Extension of aidl files, i.e. "aidl" */
+    public static final String EXT_AIDL = "aidl"; //$NON-NLS-1$
+    /** Extension of Renderscript files, i.e. "rs" */
+    public static final String EXT_RS = "rs"; //$NON-NLS-1$
+    /** Extension of FilterScript files, i.e. "fs" */
+    public static final String EXT_FS = "fs"; //$NON-NLS-1$
+    /** Extension of dependency files, i.e. "d" */
+    public static final String EXT_DEP = "d"; //$NON-NLS-1$
+    /** Extension of native libraries, i.e. "so" */
+    public static final String EXT_NATIVE_LIB = "so"; //$NON-NLS-1$
+    /** Extension of dex files, i.e. "dex" */
+    public static final String EXT_DEX = "dex"; //$NON-NLS-1$
+    /** Extension for temporary resource files, ie "ap_ */
+    public static final String EXT_RES = "ap_"; //$NON-NLS-1$
+    /** Extension for pre-processable images. Right now pngs */
+    public static final String EXT_PNG = "png"; //$NON-NLS-1$
+
+    private static final String DOT = "."; //$NON-NLS-1$
+
+    /** Dot-Extension of the Application package Files, i.e. ".apk". */
+    public static final String DOT_ANDROID_PACKAGE = DOT + EXT_ANDROID_PACKAGE;
+    /** Dot-Extension of aidl files, i.e. ".aidl" */
+    public static final String DOT_AIDL = DOT + EXT_AIDL;
+    /** Dot-Extension of renderscript files, i.e. ".rs" */
+    public static final String DOT_RS = DOT + EXT_RS;
+    /** Dot-Extension of FilterScript files, i.e. ".fs" */
+    public static final String DOT_FS = DOT + EXT_FS;
+    /** Dot-Extension of dependency files, i.e. ".d" */
+    public static final String DOT_DEP = DOT + EXT_DEP;
+    /** Dot-Extension of dex files, i.e. ".dex" */
+    public static final String DOT_DEX = DOT + EXT_DEX;
+    /** Dot-Extension for temporary resource files, ie "ap_ */
+    public static final String DOT_RES = DOT + EXT_RES;
+    /** Dot-Extension for BMP files, i.e. ".bmp" */
+    public static final String DOT_BMP = ".bmp"; //$NON-NLS-1$
+    /** Dot-Extension for SVG files, i.e. ".svg" */
+    public static final String DOT_SVG = ".svg"; //$NON-NLS-1$
+    /** Dot-Extension for template files */
+    public static final String DOT_FTL = ".ftl"; //$NON-NLS-1$
+    /** Dot-Extension of text files, i.e. ".txt" */
+    public static final String DOT_TXT = ".txt"; //$NON-NLS-1$
+
+    /** Resource base name for java files and classes */
+    public static final String FN_RESOURCE_BASE = "R"; //$NON-NLS-1$
+    /** Resource java class  filename, i.e. "R.java" */
+    public static final String FN_RESOURCE_CLASS = FN_RESOURCE_BASE + DOT_JAVA;
+    /** Resource class file  filename, i.e. "R.class" */
+    public static final String FN_COMPILED_RESOURCE_CLASS = FN_RESOURCE_BASE + DOT_CLASS;
+    /** Resource text filename, i.e. "R.txt" */
+    public static final String FN_RESOURCE_TEXT = FN_RESOURCE_BASE + DOT_TXT;
+    /** Generated manifest class name */
+    public static final String FN_MANIFEST_BASE = "Manifest";          //$NON-NLS-1$
+    /** Generated BuildConfig class name */
+    public static final String FN_BUILD_CONFIG_BASE = "BuildConfig";   //$NON-NLS-1$
+    /** Manifest java class filename, i.e. "Manifest.java" */
+    public static final String FN_MANIFEST_CLASS = FN_MANIFEST_BASE + DOT_JAVA;
+    /** BuildConfig java class filename, i.e. "BuildConfig.java" */
+    public static final String FN_BUILD_CONFIG = FN_BUILD_CONFIG_BASE + DOT_JAVA;
+
+    public static final String DRAWABLE_FOLDER = "drawable";           //$NON-NLS-1$
+    public static final String DRAWABLE_XHDPI = "drawable-xhdpi";      //$NON-NLS-1$
+    public static final String DRAWABLE_HDPI = "drawable-hdpi";        //$NON-NLS-1$
+    public static final String DRAWABLE_MDPI = "drawable-mdpi";        //$NON-NLS-1$
+    public static final String DRAWABLE_LDPI = "drawable-ldpi";        //$NON-NLS-1$
+
+    // Resources
+    public static final String PREFIX_RESOURCE_REF = "@";               //$NON-NLS-1$
+    public static final String PREFIX_THEME_REF = "?";                  //$NON-NLS-1$
+    public static final String ANDROID_PREFIX = "@android:";            //$NON-NLS-1$
+    public static final String ANDROID_THEME_PREFIX = "?android:";      //$NON-NLS-1$
+    public static final String LAYOUT_RESOURCE_PREFIX = "@layout/";     //$NON-NLS-1$
+    public static final String STYLE_RESOURCE_PREFIX = "@style/";       //$NON-NLS-1$
+    public static final String NEW_ID_PREFIX = "@+id/";                 //$NON-NLS-1$
+    public static final String ID_PREFIX = "@id/";                      //$NON-NLS-1$
+    public static final String DRAWABLE_PREFIX = "@drawable/";          //$NON-NLS-1$
+    public static final String STRING_PREFIX = "@string/";              //$NON-NLS-1$
+    public static final String ANDROID_STRING_PREFIX = "@android:string/"; //$NON-NLS-1$
+    public static final String ANDROID_LAYOUT_RESOURCE_PREFIX = "@android:layout/"; //$NON-NLS-1$
+
+    public static final String RESOURCE_CLZ_ID = "id";                  //$NON-NLS-1$
+    public static final String RESOURCE_CLZ_COLOR = "color";            //$NON-NLS-1$
+    public static final String RESOURCE_CLZ_ARRAY = "array";            //$NON-NLS-1$
+    public static final String RESOURCE_CLZ_ATTR = "attr";              //$NON-NLS-1$
+    public static final String RESOURCE_CLR_STYLEABLE = "styleable";    //$NON-NLS-1$
+    public static final String NULL_RESOURCE = "@null";                 //$NON-NLS-1$
+    public static final String TRANSPARENT_COLOR = "@android:color/transparent";      //$NON-NLS-1$
+    public static final String ANDROID_STYLE_RESOURCE_PREFIX = "@android:style/";     //$NON-NLS-1$
+    public static final String REFERENCE_STYLE = "style/";                     //$NON-NLS-1$
+    public static final String PREFIX_ANDROID = "android:";                    //$NON-NLS-1$
+
+    // Resource Types
+    public static final String DRAWABLE_TYPE = "drawable";              //$NON-NLS-1$
+    public static final String MENU_TYPE = "menu";                      //$NON-NLS-1$
+
+    // Packages
+    public static final String ANDROID_PKG_PREFIX = "android.";         //$NON-NLS-1$
+    public static final String WIDGET_PKG_PREFIX = "android.widget.";   //$NON-NLS-1$
+    public static final String VIEW_PKG_PREFIX = "android.view.";       //$NON-NLS-1$
+
+    // Project properties
+    public static final String ANDROID_LIBRARY = "android.library";     //$NON-NLS-1$
+    public static final String PROGUARD_CONFIG = "proguard.config";     //$NON-NLS-1$
+    public static final String ANDROID_LIBRARY_REFERENCE_FORMAT = "android.library.reference.%1$d";//$NON-NLS-1$
+    public static final String PROJECT_PROPERTIES = "project.properties";//$NON-NLS-1$
+
+    // Java References
+    public static final String ATTR_REF_PREFIX = "?attr/";               //$NON-NLS-1$
+    public static final String R_PREFIX = "R.";                          //$NON-NLS-1$
+    public static final String R_ID_PREFIX = "R.id.";                    //$NON-NLS-1$
+    public static final String R_LAYOUT_RESOURCE_PREFIX = "R.layout.";   //$NON-NLS-1$
+    public static final String R_DRAWABLE_PREFIX = "R.drawable.";        //$NON-NLS-1$
+    public static final String R_ATTR_PREFIX = "R.attr.";                //$NON-NLS-1$
+
+    // Attributes related to tools
+    public static final String ATTR_IGNORE = "ignore";                   //$NON-NLS-1$
+    public static final String ATTR_LOCALE = "locale";                   //$NON-NLS-1$
+
+    // SuppressLint
+    public static final String SUPPRESS_ALL = "all";                     //$NON-NLS-1$
+    public static final String SUPPRESS_LINT = "SuppressLint";           //$NON-NLS-1$
+    public static final String TARGET_API = "TargetApi";                 //$NON-NLS-1$
+    public static final String ATTR_TARGET_API = "targetApi";            //$NON-NLS-1$
+    public static final String FQCN_SUPPRESS_LINT = "android.annotation." + SUPPRESS_LINT; //$NON-NLS-1$
+    public static final String FQCN_TARGET_API = "android.annotation." + TARGET_API; //$NON-NLS-1$
+
+    // Class Names
+    public static final String CONSTRUCTOR_NAME = "<init>";                          //$NON-NLS-1$
+    public static final String CLASS_CONSTRUCTOR = "<clinit>";                       //$NON-NLS-1$
+    public static final String FRAGMENT = "android/app/Fragment";                    //$NON-NLS-1$
+    public static final String FRAGMENT_V4 = "android/support/v4/app/Fragment";      //$NON-NLS-1$
+    public static final String ANDROID_APP_ACTIVITY = "android/app/Activity";        //$NON-NLS-1$
+    public static final String ANDROID_APP_SERVICE = "android/app/Service";          //$NON-NLS-1$
+    public static final String ANDROID_CONTENT_CONTENT_PROVIDER =
+            "android/content/ContentProvider";                                       //$NON-NLS-1$
+    public static final String ANDROID_CONTENT_BROADCAST_RECEIVER =
+            "android/content/BroadcastReceiver";                                     //$NON-NLS-1$
+
+    // Method Names
+    public static final String FORMAT_METHOD = "format";                             //$NON-NLS-1$
+    public static final String GET_STRING_METHOD = "getString";                      //$NON-NLS-1$
+
+
+
+
+    public static final String ATTR_TAG = "tag";                        //$NON-NLS-1$
+    public static final String ATTR_NUM_COLUMNS = "numColumns";         //$NON-NLS-1$
+
+    // Some common layout element names
+    public static final String CALENDAR_VIEW = "CalendarView";          //$NON-NLS-1$
+    public static final String SPACE = "Space";                         //$NON-NLS-1$
+    public static final String GESTURE_OVERLAY_VIEW = "GestureOverlayView";//$NON-NLS-1$
+
+    public static final String ATTR_HANDLE = "handle";                  //$NON-NLS-1$
+    public static final String ATTR_CONTENT = "content";                //$NON-NLS-1$
+    public static final String ATTR_CHECKED = "checked";                //$NON-NLS-1$
+
+    // TextView
+    public static final String ATTR_DRAWABLE_RIGHT = "drawableRight";              //$NON-NLS-1$
+    public static final String ATTR_DRAWABLE_LEFT = "drawableLeft";                //$NON-NLS-1$
+    public static final String ATTR_DRAWABLE_BOTTOM = "drawableBottom";            //$NON-NLS-1$
+    public static final String ATTR_DRAWABLE_TOP = "drawableTop";                  //$NON-NLS-1$
+    public static final String ATTR_DRAWABLE_PADDING = "drawablePadding";          //$NON-NLS-1$
+
+    public static final String ATTR_USE_DEFAULT_MARGINS = "useDefaultMargins";      //$NON-NLS-1$
+    public static final String ATTR_MARGINS_INCLUDED_IN_ALIGNMENT = "marginsIncludedInAlignment"; //$NON-NLS-1$
+
+    public static final String VALUE_WRAP_CONTENT = "wrap_content";             //$NON-NLS-1$
+    public static final String VALUE_FALSE= "false";                            //$NON-NLS-1$
+    public static final String VALUE_N_DP = "%ddp";                             //$NON-NLS-1$
+    public static final String VALUE_ZERO_DP = "0dp";                           //$NON-NLS-1$
+    public static final String VALUE_ONE_DP = "1dp";                            //$NON-NLS-1$
+    public static final String VALUE_TOP = "top";                               //$NON-NLS-1$
+    public static final String VALUE_BOTTOM = "bottom";                         //$NON-NLS-1$
+    public static final String VALUE_CENTER_VERTICAL = "center_vertical";       //$NON-NLS-1$
+    public static final String VALUE_CENTER_HORIZONTAL = "center_horizontal";   //$NON-NLS-1$
+    public static final String VALUE_FILL_HORIZONTAL = "fill_horizontal";       //$NON-NLS-1$
+    public static final String VALUE_FILL_VERTICAL = "fill_vertical";           //$NON-NLS-1$
+    public static final String VALUE_0 = "0";                                   //$NON-NLS-1$
+    public static final String VALUE_1 = "1";                                   //$NON-NLS-1$
+
+    // Gravity values. These have the GRAVITY_ prefix in front of value because we already
+    // have VALUE_CENTER_HORIZONTAL defined for layouts, and its definition conflicts
+    // (centerHorizontal versus center_horizontal)
+    public static final String GRAVITY_VALUE_ = "center";                             //$NON-NLS-1$
+    public static final String GRAVITY_VALUE_CENTER = "center";                       //$NON-NLS-1$
+    public static final String GRAVITY_VALUE_LEFT = "left";                           //$NON-NLS-1$
+    public static final String GRAVITY_VALUE_RIGHT = "right";                         //$NON-NLS-1$
+    public static final String GRAVITY_VALUE_START = "start";                         //$NON-NLS-1$
+    public static final String GRAVITY_VALUE_END = "end";                             //$NON-NLS-1$
+    public static final String GRAVITY_VALUE_BOTTOM = "bottom";                       //$NON-NLS-1$
+    public static final String GRAVITY_VALUE_TOP = "top";                             //$NON-NLS-1$
+    public static final String GRAVITY_VALUE_FILL_HORIZONTAL = "fill_horizontal";     //$NON-NLS-1$
+    public static final String GRAVITY_VALUE_FILL_VERTICAL = "fill_vertical";         //$NON-NLS-1$
+    public static final String GRAVITY_VALUE_CENTER_HORIZONTAL = "center_horizontal"; //$NON-NLS-1$
+    public static final String GRAVITY_VALUE_CENTER_VERTICAL = "center_vertical";     //$NON-NLS-1$
+    public static final String GRAVITY_VALUE_FILL = "fill";                           //$NON-NLS-1$
+
+    /**
+     * The top level android package as a prefix, "android.".
+     */
+    public static final String ANDROID_SUPPORT_PKG_PREFIX = ANDROID_PKG_PREFIX + "support."; //$NON-NLS-1$
+
+    /** The android.view. package prefix */
+    public static final String ANDROID_VIEW_PKG = ANDROID_PKG_PREFIX + "view."; //$NON-NLS-1$
+
+    /** The android.widget. package prefix */
+    public static final String ANDROID_WIDGET_PREFIX = ANDROID_PKG_PREFIX + "widget."; //$NON-NLS-1$
+
+    /** The android.webkit. package prefix */
+    public static final String ANDROID_WEBKIT_PKG = ANDROID_PKG_PREFIX + "webkit."; //$NON-NLS-1$
+
+    /** The LayoutParams inner-class name suffix, .LayoutParams */
+    public static final String DOT_LAYOUT_PARAMS = ".LayoutParams"; //$NON-NLS-1$
+
+    /** The fully qualified class name of an EditText view */
+    public static final String FQCN_EDIT_TEXT = "android.widget.EditText"; //$NON-NLS-1$
+
+    /** The fully qualified class name of a LinearLayout view */
+    public static final String FQCN_LINEAR_LAYOUT = "android.widget.LinearLayout"; //$NON-NLS-1$
+
+    /** The fully qualified class name of a RelativeLayout view */
+    public static final String FQCN_RELATIVE_LAYOUT = "android.widget.RelativeLayout"; //$NON-NLS-1$
+
+    /** The fully qualified class name of a RelativeLayout view */
+    public static final String FQCN_GRID_LAYOUT = "android.widget.GridLayout"; //$NON-NLS-1$
+    public static final String FQCN_GRID_LAYOUT_V7 = "android.support.v7.widget.GridLayout"; //$NON-NLS-1$
+
+    /** The fully qualified class name of a FrameLayout view */
+    public static final String FQCN_FRAME_LAYOUT = "android.widget.FrameLayout"; //$NON-NLS-1$
+
+    /** The fully qualified class name of a TableRow view */
+    public static final String FQCN_TABLE_ROW = "android.widget.TableRow"; //$NON-NLS-1$
+
+    /** The fully qualified class name of a TableLayout view */
+    public static final String FQCN_TABLE_LAYOUT = "android.widget.TableLayout"; //$NON-NLS-1$
+
+    /** The fully qualified class name of a GridView view */
+    public static final String FQCN_GRID_VIEW = "android.widget.GridView"; //$NON-NLS-1$
+
+    /** The fully qualified class name of a TabWidget view */
+    public static final String FQCN_TAB_WIDGET = "android.widget.TabWidget"; //$NON-NLS-1$
+
+    /** The fully qualified class name of a Button view */
+    public static final String FQCN_BUTTON = "android.widget.Button"; //$NON-NLS-1$
+
+    /** The fully qualified class name of a RadioButton view */
+    public static final String FQCN_RADIO_BUTTON = "android.widget.RadioButton"; //$NON-NLS-1$
+
+    /** The fully qualified class name of a ToggleButton view */
+    public static final String FQCN_TOGGLE_BUTTON = "android.widget.ToggleButton"; //$NON-NLS-1$
+
+    /** The fully qualified class name of a Spinner view */
+    public static final String FQCN_SPINNER = "android.widget.Spinner"; //$NON-NLS-1$
+
+    /** The fully qualified class name of an AdapterView */
+    public static final String FQCN_ADAPTER_VIEW = "android.widget.AdapterView"; //$NON-NLS-1$
+
+    /** The fully qualified class name of a ListView */
+    public static final String FQCN_LIST_VIEW = "android.widget.ListView"; //$NON-NLS-1$
+
+    /** The fully qualified class name of an ExpandableListView */
+    public static final String FQCN_EXPANDABLE_LIST_VIEW = "android.widget.ExpandableListView"; //$NON-NLS-1$
+
+    /** The fully qualified class name of a GestureOverlayView */
+    public static final String FQCN_GESTURE_OVERLAY_VIEW = "android.gesture.GestureOverlayView"; //$NON-NLS-1$
+
+    /** The fully qualified class name of a DatePicker */
+    public static final String FQCN_DATE_PICKER = "android.widget.DatePicker"; //$NON-NLS-1$
+
+    /** The fully qualified class name of a TimePicker */
+    public static final String FQCN_TIME_PICKER = "android.widget.TimePicker"; //$NON-NLS-1$
+
+    /** The fully qualified class name of a RadioGroup */
+    public static final String FQCN_RADIO_GROUP = "android.widgets.RadioGroup";  //$NON-NLS-1$
+
+    /** The fully qualified class name of a Space */
+    public static final String FQCN_SPACE = "android.widget.Space"; //$NON-NLS-1$
+    public static final String FQCN_SPACE_V7 = "android.support.v7.widget.Space"; //$NON-NLS-1$
+
+    /** The fully qualified class name of a TextView view */
+    public static final String FQCN_TEXT_VIEW = "android.widget.TextView"; //$NON-NLS-1$
+
+    /** The fully qualified class name of an ImageView view */
+    public static final String FQCN_IMAGE_VIEW = "android.widget.ImageView"; //$NON-NLS-1$
+
+    public static final String ATTR_SRC = "src"; //$NON-NLS-1$
+
+    public static final String ATTR_GRAVITY = "gravity"; //$NON-NLS-1$
+    public static final String ATTR_WEIGHT_SUM = "weightSum"; //$NON-NLS-1$
+    public static final String ATTR_EMS = "ems"; //$NON-NLS-1$
+
+    public static final String VALUE_HORIZONTAL = "horizontal"; //$NON-NLS-1$
+}
diff --git a/common/src/main/java/com/android/annotations/NonNull.java b/common/src/main/java/com/android/annotations/NonNull.java
new file mode 100644
index 0000000..973ebb6
--- /dev/null
+++ b/common/src/main/java/com/android/annotations/NonNull.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.annotations;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Denotes that a parameter, field or method return value can never be null.
+ * <p/>
+ * This is a marker annotation and it has no specific attributes.
+ */
+ at Documented
+ at Retention(RetentionPolicy.CLASS)
+ at Target({METHOD,PARAMETER,LOCAL_VARIABLE,FIELD})
+public @interface NonNull {
+}
diff --git a/common/src/main/java/com/android/annotations/NonNullByDefault.java b/common/src/main/java/com/android/annotations/NonNullByDefault.java
new file mode 100644
index 0000000..3db891c
--- /dev/null
+++ b/common/src/main/java/com/android/annotations/NonNullByDefault.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.annotations;
+
+import static java.lang.annotation.ElementType.PACKAGE;
+import static java.lang.annotation.ElementType.TYPE;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Denotes that all parameters, fields or methods within a class or method by
+ * default can not be null. This can be overridden by adding specific
+ * {@link com.android.annotations.Nullable} annotations on fields, parameters or
+ * methods that should not use the default.
+ * <p/>
+ * NOTE: Eclipse does not yet handle defaults well (in particular, if
+ * you add this on a class which implements Comparable, then it will insist
+ * that your compare method is changing the nullness of the compare parameter,
+ * so you'll need to add @Nullable on it, which also is not right (since
+ * the method should have implied @NonNull and you do not need to check
+ * the parameter.). For now, it's best to individually annotate methods,
+ * parameters and fields.
+ * <p/>
+ * This is a marker annotation and it has no specific attributes.
+ */
+ at Documented
+ at Retention(RetentionPolicy.CLASS)
+ at Target({PACKAGE, TYPE})
+public @interface NonNullByDefault {
+}
diff --git a/common/src/main/java/com/android/annotations/Nullable.java b/common/src/main/java/com/android/annotations/Nullable.java
new file mode 100755
index 0000000..d9c3861
--- /dev/null
+++ b/common/src/main/java/com/android/annotations/Nullable.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.annotations;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Denotes that a parameter, field or method return value can be null.
+ * <b>Note</b>: this is the default assumption for most Java APIs and the
+ * default assumption made by most static code checking tools, so usually you
+ * don't need to use this annotation; its primary use is to override a default
+ * wider annotation like {@link NonNullByDefault}.
+ * <p/>
+ * When decorating a method call parameter, this denotes the parameter can
+ * legitimately be null and the method will gracefully deal with it. Typically
+ * used on optional parameters.
+ * <p/>
+ * When decorating a method, this denotes the method might legitimately return
+ * null.
+ * <p/>
+ * This is a marker annotation and it has no specific attributes.
+ */
+ at Documented
+ at Retention(RetentionPolicy.CLASS)
+ at Target({METHOD, PARAMETER, LOCAL_VARIABLE, FIELD})
+public @interface Nullable {
+}
diff --git a/common/src/main/java/com/android/annotations/VisibleForTesting.java b/common/src/main/java/com/android/annotations/VisibleForTesting.java
new file mode 100755
index 0000000..7f41d70
--- /dev/null
+++ b/common/src/main/java/com/android/annotations/VisibleForTesting.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.annotations;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Denotes that the class, method or field has its visibility relaxed so
+ * that unit tests can access it.
+ * <p/>
+ * The <code>visibility</code> argument can be used to specific what the original
+ * visibility should have been if it had not been made public or package-private for testing.
+ * The default is to consider the element private.
+ */
+ at Retention(RetentionPolicy.SOURCE)
+public @interface VisibleForTesting {
+    /**
+     * Intended visibility if the element had not been made public or package-private for
+     * testing.
+     */
+    enum Visibility {
+        /** The element should be considered protected. */
+        PROTECTED,
+        /** The element should be considered package-private. */
+        PACKAGE,
+        /** The element should be considered private. */
+        PRIVATE
+    }
+
+    /**
+     * Intended visibility if the element had not been made public or package-private for testing.
+     * If not specified, one should assume the element originally intended to be private.
+     */
+    Visibility visibility() default Visibility.PRIVATE;
+}
diff --git a/common/src/main/java/com/android/annotations/concurrency/GuardedBy.java b/common/src/main/java/com/android/annotations/concurrency/GuardedBy.java
new file mode 100644
index 0000000..9489bb1
--- /dev/null
+++ b/common/src/main/java/com/android/annotations/concurrency/GuardedBy.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2013 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.annotations.concurrency;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates that the target field or method should only be accessed
+ * with the specified lock being held.
+ */
+ at Documented
+ at Retention(RetentionPolicy.CLASS)
+ at Target({ElementType.METHOD, ElementType.FIELD})
+public @interface GuardedBy {
+    String value();
+}
diff --git a/common/src/main/java/com/android/annotations/concurrency/Immutable.java b/common/src/main/java/com/android/annotations/concurrency/Immutable.java
new file mode 100644
index 0000000..d6c9a4a
--- /dev/null
+++ b/common/src/main/java/com/android/annotations/concurrency/Immutable.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2013 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.annotations.concurrency;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates that the target class to which this annotation is applied
+ * is immutable.
+ */
+ at Documented
+ at Retention(RetentionPolicy.CLASS)
+ at Target(ElementType.TYPE)
+public @interface Immutable {
+}
diff --git a/common/src/main/java/com/android/io/FileWrapper.java b/common/src/main/java/com/android/io/FileWrapper.java
new file mode 100644
index 0000000..8be7859
--- /dev/null
+++ b/common/src/main/java/com/android/io/FileWrapper.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2008 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.io;
+
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+
+/**
+ * An implementation of {@link IAbstractFile} extending {@link File}.
+ */
+public class FileWrapper extends File implements IAbstractFile {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Creates a new File instance matching a given {@link File} object.
+     * @param file the file to match
+     */
+    public FileWrapper(File file) {
+        super(file.getAbsolutePath());
+    }
+
+    /**
+     * Creates a new File instance from a parent abstract pathname and a child pathname string.
+     * @param parent the parent pathname
+     * @param child the child name
+     *
+     * @see File#File(File, String)
+     */
+    public FileWrapper(File parent, String child) {
+        super(parent, child);
+    }
+
+    /**
+     * Creates a new File instance by converting the given pathname string into an abstract
+     * pathname.
+     * @param osPathname the OS pathname
+     *
+     * @see File#File(String)
+     */
+    public FileWrapper(String osPathname) {
+        super(osPathname);
+    }
+
+    /**
+     * Creates a new File instance from a parent abstract pathname and a child pathname string.
+     * @param parent the parent pathname
+     * @param child the child name
+     *
+     * @see File#File(String, String)
+     */
+    public FileWrapper(String parent, String child) {
+        super(parent, child);
+    }
+
+    /**
+     * Creates a new File instance by converting the given <code>file:</code> URI into an
+     * abstract pathname.
+     * @param uri An absolute, hierarchical URI with a scheme equal to "file", a non-empty path
+     * component, and undefined authority, query, and fragment components
+     *
+     * @see File#File(URI)
+     */
+    public FileWrapper(URI uri) {
+        super(uri);
+    }
+
+    @Override
+    public InputStream getContents() throws StreamException {
+        try {
+            return new FileInputStream(this);
+        } catch (FileNotFoundException e) {
+            throw new StreamException(e, this, StreamException.Error.FILENOTFOUND);
+        }
+    }
+
+    @Override
+    public void setContents(InputStream source) throws StreamException {
+        FileOutputStream fos = null;
+        try {
+            fos = new FileOutputStream(this);
+
+            byte[] buffer = new byte[1024];
+            int count = 0;
+            while ((count = source.read(buffer)) != -1) {
+                fos.write(buffer, 0, count);
+            }
+        } catch (IOException e) {
+            throw new StreamException(e, this);
+        } finally {
+            if (fos != null) {
+                try {
+                    fos.close();
+                } catch (IOException e) {
+                    throw new StreamException(e, this);
+                }
+            }
+        }
+    }
+
+    @Override
+    public OutputStream getOutputStream() throws StreamException {
+        try {
+            return new FileOutputStream(this);
+        } catch (FileNotFoundException e) {
+            throw new StreamException(e, this);
+        }
+    }
+
+    @Override
+    public PreferredWriteMode getPreferredWriteMode() {
+        return PreferredWriteMode.OUTPUTSTREAM;
+    }
+
+    @Override
+    public String getOsLocation() {
+        return getAbsolutePath();
+    }
+
+    @Override
+    public boolean exists() {
+        return isFile();
+    }
+
+    @Override
+    public long getModificationStamp() {
+        return lastModified();
+    }
+
+    @Override
+    public IAbstractFolder getParentFolder() {
+        String p = this.getParent();
+        if (p == null) {
+            return null;
+        }
+        return new FolderWrapper(p);
+    }
+}
diff --git a/common/src/main/java/com/android/io/FolderWrapper.java b/common/src/main/java/com/android/io/FolderWrapper.java
new file mode 100644
index 0000000..c29c934
--- /dev/null
+++ b/common/src/main/java/com/android/io/FolderWrapper.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2008 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.io;
+
+
+import java.io.File;
+import java.net.URI;
+import java.util.ArrayList;
+
+/**
+ * An implementation of {@link IAbstractFolder} extending {@link File}.
+ */
+public class FolderWrapper extends File implements IAbstractFolder {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Creates a new File instance from a parent abstract pathname and a child pathname string.
+     * @param parent the parent pathname
+     * @param child the child name
+     *
+     * @see File#File(File, String)
+     */
+    public FolderWrapper(File parent, String child) {
+        super(parent, child);
+    }
+
+    /**
+     * Creates a new File instance by converting the given pathname string into an abstract
+     * pathname.
+     * @param pathname the pathname
+     *
+     * @see File#File(String)
+     */
+    public FolderWrapper(String pathname) {
+        super(pathname);
+    }
+
+    /**
+     * Creates a new File instance from a parent abstract pathname and a child pathname string.
+     * @param parent the parent pathname
+     * @param child the child name
+     *
+     * @see File#File(String, String)
+     */
+    public FolderWrapper(String parent, String child) {
+        super(parent, child);
+    }
+
+    /**
+     * Creates a new File instance by converting the given <code>file:</code> URI into an
+     * abstract pathname.
+     * @param uri An absolute, hierarchical URI with a scheme equal to "file", a non-empty path
+     * component, and undefined authority, query, and fragment components
+     *
+     * @see File#File(URI)
+     */
+    public FolderWrapper(URI uri) {
+        super(uri);
+    }
+
+    /**
+     * Creates a new File instance matching a give {@link File} object.
+     * @param file the file to match
+     */
+    public FolderWrapper(File file) {
+        super(file.getAbsolutePath());
+    }
+
+    @Override
+    public IAbstractResource[] listMembers() {
+        File[] files = listFiles();
+        final int count = files == null ? 0 : files.length;
+        IAbstractResource[] afiles = new IAbstractResource[count];
+
+        if (files != null) {
+            for (int i = 0 ; i < count ; i++) {
+                File f = files[i];
+                if (f.isFile()) {
+                    afiles[i] = new FileWrapper(f);
+                } else if (f.isDirectory()) {
+                    afiles[i] = new FolderWrapper(f);
+                }
+            }
+        }
+
+        return afiles;
+    }
+
+    @Override
+    public boolean hasFile(final String name) {
+        String[] match = list(new FilenameFilter() {
+            @Override
+            public boolean accept(IAbstractFolder dir, String filename) {
+                return name.equals(filename);
+            }
+        });
+
+        return match.length > 0;
+    }
+
+    @Override
+    public IAbstractFile getFile(String name) {
+        return new FileWrapper(this, name);
+    }
+
+    @Override
+    public IAbstractFolder getFolder(String name) {
+        return new FolderWrapper(this, name);
+    }
+
+    @Override
+    public IAbstractFolder getParentFolder() {
+        String p = this.getParent();
+        if (p == null) {
+            return null;
+        }
+        return new FolderWrapper(p);
+    }
+
+    @Override
+    public String getOsLocation() {
+        return getAbsolutePath();
+    }
+
+    @Override
+    public boolean exists() {
+        return isDirectory();
+    }
+
+    @Override
+    public String[] list(FilenameFilter filter) {
+        File[] files = listFiles();
+        if (files != null && files.length > 0) {
+            ArrayList<String> list = new ArrayList<String>();
+
+            for (File file : files) {
+                if (filter.accept(this, file.getName())) {
+                    list.add(file.getName());
+                }
+            }
+
+            return list.toArray(new String[list.size()]);
+        }
+
+        return new String[0];
+    }
+}
diff --git a/common/src/main/java/com/android/io/IAbstractFile.java b/common/src/main/java/com/android/io/IAbstractFile.java
new file mode 100644
index 0000000..285df1f
--- /dev/null
+++ b/common/src/main/java/com/android/io/IAbstractFile.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2008 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.io;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * A file.
+ */
+public interface IAbstractFile extends IAbstractResource {
+    public static enum PreferredWriteMode {
+        INPUTSTREAM, OUTPUTSTREAM
+    }
+
+    /**
+     * Returns an {@link InputStream} object on the file content.
+     * @throws StreamException
+     */
+    InputStream getContents() throws StreamException;
+
+    /**
+     * Sets the content of the file.
+     * @param source the content
+     * @throws StreamException
+     */
+    void setContents(InputStream source) throws StreamException;
+
+    /**
+     * Returns an {@link OutputStream} to write into the file.
+     * @throws StreamException
+     */
+    OutputStream getOutputStream() throws StreamException;
+
+    /**
+     * Returns the preferred mode to write into the file.
+     */
+    PreferredWriteMode getPreferredWriteMode();
+
+    /**
+     * Returns the last modification timestamp
+     */
+    long getModificationStamp();
+}
diff --git a/common/src/main/java/com/android/io/IAbstractFolder.java b/common/src/main/java/com/android/io/IAbstractFolder.java
new file mode 100644
index 0000000..8335ef9
--- /dev/null
+++ b/common/src/main/java/com/android/io/IAbstractFolder.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2008 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.io;
+
+import java.io.File;
+
+/**
+ *  A folder.
+ */
+public interface IAbstractFolder extends IAbstractResource {
+    /**
+     * Instances of classes that implement this interface are used to
+     * filter filenames.
+     */
+    public interface FilenameFilter {
+        /**
+         * Tests if a specified file should be included in a file list.
+         *
+         * @param   dir    the directory in which the file was found.
+         * @param   name   the name of the file.
+         * @return  <code>true</code> if and only if the name should be
+         * included in the file list; <code>false</code> otherwise.
+         */
+        boolean accept(IAbstractFolder dir, String name);
+    }
+
+    /**
+     * Returns true if the receiver contains a file with a given name
+     * @param name the name of the file. This is the name without the path leading to the
+     * parent folder.
+     */
+    boolean hasFile(String name);
+
+    /**
+     * Returns an {@link IAbstractFile} representing a child of the current folder with the
+     * given name. The file may not actually exist.
+     * @param name the name of the file.
+     */
+    IAbstractFile getFile(String name);
+
+    /**
+     * Returns an {@link IAbstractFolder} representing a child of the current folder with the
+     * given name. The folder may not actually exist.
+     * @param name the name of the folder.
+     */
+    IAbstractFolder getFolder(String name);
+
+    /**
+     * Returns a list of all existing file and directory members in this folder.
+     * The returned array can be empty but is never null.
+     */
+    IAbstractResource[] listMembers();
+
+    /**
+     * Returns a list of all existing file and directory members in this folder
+     * that satisfy the specified filter.
+     *
+     * @param filter A filename filter instance. Must not be null.
+     * @return An array of file names (generated using {@link File#getName()}).
+     *         The array can be empty but is never null.
+     */
+    String[] list(FilenameFilter filter);
+}
diff --git a/common/src/main/java/com/android/io/IAbstractResource.java b/common/src/main/java/com/android/io/IAbstractResource.java
new file mode 100644
index 0000000..e6358ec
--- /dev/null
+++ b/common/src/main/java/com/android/io/IAbstractResource.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2008 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.io;
+
+/**
+ * Base representation of a file system resource.<p/>
+ * This somewhat limited interface is designed to let classes use file-system resources, without
+ * having the manually handle either the standard Java file or the Eclipse file API..
+ */
+public interface IAbstractResource {
+
+    /**
+     * Returns the name of the resource.
+     */
+    String getName();
+
+    /**
+     * Returns the OS path of the folder location.
+     */
+    String getOsLocation();
+
+    /**
+     * Returns whether the resource actually exists.
+     */
+    boolean exists();
+
+    /**
+     * Returns the parent folder or null if there is no parent.
+     */
+    IAbstractFolder getParentFolder();
+
+    /**
+     * Deletes the resource.
+     */
+    boolean delete();
+}
diff --git a/common/src/main/java/com/android/io/StreamException.java b/common/src/main/java/com/android/io/StreamException.java
new file mode 100644
index 0000000..6736b86
--- /dev/null
+++ b/common/src/main/java/com/android/io/StreamException.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2010 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.io;
+
+
+/**
+ * Exception thrown when {@link IAbstractFile#getContents()} fails.
+ */
+public class StreamException extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    public static enum Error {
+        DEFAULT, OUTOFSYNC, FILENOTFOUND
+    }
+
+    private final  Error mError;
+    private final IAbstractFile mFile;
+
+    public StreamException(Exception e, IAbstractFile file) {
+        this(e, file, Error.DEFAULT);
+    }
+
+    public StreamException(Exception e, IAbstractFile file, Error error) {
+        super(e);
+        mFile = file;
+        mError = error;
+    }
+
+    public Error getError() {
+        return mError;
+    }
+
+    public IAbstractFile getFile() {
+        return mFile;
+    }
+}
diff --git a/common/src/main/java/com/android/prefs/AndroidLocation.java b/common/src/main/java/com/android/prefs/AndroidLocation.java
new file mode 100644
index 0000000..11c1540
--- /dev/null
+++ b/common/src/main/java/com/android/prefs/AndroidLocation.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2008 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.prefs;
+
+import com.android.annotations.NonNull;
+
+import java.io.File;
+
+/**
+ * Manages the location of the android files (including emulator files, ddms config, debug keystore)
+ */
+public final class AndroidLocation {
+
+    /**
+     * The name of the .android folder returned by {@link #getFolder()}.
+     */
+    public static final String FOLDER_DOT_ANDROID = ".android";
+
+    /**
+     * Virtual Device folder inside the path returned by {@link #getFolder()}
+     */
+    public static final String FOLDER_AVD = "avd";
+
+    /**
+     * Throw when the location of the android folder couldn't be found.
+     */
+    public static final class AndroidLocationException extends Exception {
+        private static final long serialVersionUID = 1L;
+
+        public AndroidLocationException(String string) {
+            super(string);
+        }
+    }
+
+    private static String sPrefsLocation = null;
+
+    /**
+     * Returns the folder used to store android related files.
+     * @return an OS specific path, terminated by a separator.
+     * @throws AndroidLocationException
+     */
+    @NonNull
+    public static final String getFolder() throws AndroidLocationException {
+        if (sPrefsLocation == null) {
+            String home = findValidPath("ANDROID_SDK_HOME", "user.home", "HOME");
+
+            // if the above failed, we throw an exception.
+            if (home == null) {
+                throw new AndroidLocationException(
+                        "Unable to get the Android SDK home directory.\n" +
+                        "Make sure the environment variable ANDROID_SDK_HOME is set up.");
+            } else {
+                sPrefsLocation = home;
+                if (!sPrefsLocation.endsWith(File.separator)) {
+                    sPrefsLocation += File.separator;
+                }
+                sPrefsLocation += FOLDER_DOT_ANDROID + File.separator;
+            }
+        }
+
+        // make sure the folder exists!
+        File f = new File(sPrefsLocation);
+        if (f.exists() == false) {
+            try {
+                f.mkdir();
+            } catch (SecurityException e) {
+                AndroidLocationException e2 = new AndroidLocationException(String.format(
+                        "Unable to create folder '%1$s'. " +
+                        "This is the path of preference folder expected by the Android tools.",
+                        sPrefsLocation));
+                e2.initCause(e);
+                throw e2;
+            }
+        } else if (f.isFile()) {
+            throw new AndroidLocationException(sPrefsLocation +
+                    " is not a directory! " +
+                    "This is the path of preference folder expected by the Android tools.");
+        }
+
+        return sPrefsLocation;
+    }
+
+    /**
+     * Resets the folder used to store android related files. For testing.
+     */
+    public static final void resetFolder() {
+        sPrefsLocation = null;
+    }
+
+    /**
+     * Checks a list of system properties and/or system environment variables for validity, and
+     * existing director, and returns the first one.
+     * @param names
+     * @return the content of the first property/variable.
+     */
+    private static String findValidPath(String... names) {
+        for (String name : names) {
+            String path;
+            if (name.indexOf('.') != -1) {
+                path = System.getProperty(name);
+            } else {
+                path = System.getenv(name);
+            }
+
+            if (path != null) {
+                File f = new File(path);
+                if (f.isDirectory()) {
+                    return path;
+                }
+            }
+        }
+
+        return null;
+    }
+}
diff --git a/common/src/main/java/com/android/utils/ILogger.java b/common/src/main/java/com/android/utils/ILogger.java
new file mode 100644
index 0000000..9b9e45b
--- /dev/null
+++ b/common/src/main/java/com/android/utils/ILogger.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2012 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.utils;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+
+import java.util.Formatter;
+
+/**
+ * Interface used to display warnings/errors while parsing the SDK content.
+ * <p/>
+ * There are a few default implementations available:
+ * <ul>
+ * <li> {@link NullLogger} is an implementation that does <em>nothing</em> with the log.
+ *  Useful for limited cases where you need to call a class that requires a non-null logging
+ *  yet the calling code does not have any mean of reporting logs itself. It can be
+ *  acceptable for use as a temporary implementation but most of the time that means the caller
+ *  code needs to be reworked to take a logger object from its own caller.
+ * </li>
+ * <li> {@link StdLogger} is an implementation that dumps the log to {@link System#out} or
+ *  {@link System#err}. This is useful for unit tests or code that does not have any GUI.
+ *  GUI based apps based should not use it and should provide a better way to report to the user.
+ * </li>
+ * </ul>
+ */
+public interface ILogger {
+
+    /**
+     * Prints an error message.
+     *
+     * @param t is an optional {@link Throwable} or {@link Exception}. If non-null, its
+     *          message will be printed out.
+     * @param msgFormat is an optional error format. If non-null, it will be printed
+     *          using a {@link Formatter} with the provided arguments.
+     * @param args provides the arguments for errorFormat.
+     */
+    void error(@Nullable Throwable t, @Nullable String msgFormat, Object... args);
+
+    /**
+     * Prints a warning message.
+     *
+     * @param msgFormat is a string format to be used with a {@link Formatter}. Cannot be null.
+     * @param args provides the arguments for warningFormat.
+     */
+    void warning(@NonNull String msgFormat, Object... args);
+
+    /**
+     * Prints an information message.
+     *
+     * @param msgFormat is a string format to be used with a {@link Formatter}. Cannot be null.
+     * @param args provides the arguments for msgFormat.
+     */
+    void info(@NonNull String msgFormat, Object... args);
+
+    /**
+     * Prints a verbose message.
+     *
+     * @param msgFormat is a string format to be used with a {@link Formatter}. Cannot be null.
+     * @param args provides the arguments for msgFormat.
+     */
+    void verbose(@NonNull String msgFormat, Object... args);
+
+}
diff --git a/common/src/main/java/com/android/utils/IReaderLogger.java b/common/src/main/java/com/android/utils/IReaderLogger.java
new file mode 100755
index 0000000..2682598
--- /dev/null
+++ b/common/src/main/java/com/android/utils/IReaderLogger.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2013 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.utils;
+
+import com.android.annotations.NonNull;
+
+import java.io.IOException;
+
+/**
+ * Interface to read a line from the {@link System#in} input stream.
+ * <p/>
+ * The interface also implements {@link ILogger} since code that needs to ask for
+ * a command-line input will most likely also want to use {@link ILogger#info(String, Object...)}
+ * to print information such as an input prompt.
+ */
+public interface IReaderLogger extends ILogger {
+
+    /**
+     * Reads a line from {@link System#in}.
+     * <p/>
+     * This call is blocking and should only be called from command-line enabled applications.
+     *
+     * @param inputBuffer A non-null buffer where to place the input.
+     * @return The number of bytes read into the buffer.
+     * @throws IOException as returned by {code System.in.read()}.
+     */
+    int readLine(@NonNull byte[] inputBuffer) throws IOException;
+}
diff --git a/common/src/main/java/com/android/utils/NullLogger.java b/common/src/main/java/com/android/utils/NullLogger.java
new file mode 100644
index 0000000..ac60aee
--- /dev/null
+++ b/common/src/main/java/com/android/utils/NullLogger.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2012 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.utils;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+
+/**
+ * Dummy implementation of an {@link ILogger}.
+ * <p/>
+ * Use {@link #getLogger()} to get a default instance of this {@link NullLogger}.
+ */
+public class NullLogger implements ILogger {
+
+    private static final ILogger sThis = new NullLogger();
+
+    public static ILogger getLogger() {
+        return sThis;
+    }
+
+    @Override
+    public void error(@Nullable Throwable t, @Nullable String errorFormat, Object... args) {
+        // ignore
+    }
+
+    @Override
+    public void warning(@NonNull String warningFormat, Object... args) {
+        // ignore
+    }
+
+    @Override
+    public void info(@NonNull String msgFormat, Object... args) {
+        // ignore
+    }
+
+    @Override
+    public void verbose(@NonNull String msgFormat, Object... args) {
+        // ignore
+    }
+
+}
diff --git a/common/src/main/java/com/android/utils/Pair.java b/common/src/main/java/com/android/utils/Pair.java
new file mode 100644
index 0000000..63694de
--- /dev/null
+++ b/common/src/main/java/com/android/utils/Pair.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2010 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.utils;
+
+/**
+ * A Pair class is simply a 2-tuple for use in this package. We might want to
+ * think about adding something like this to a more central utility place, or
+ * replace it by a common tuple class if one exists, or even rewrite the layout
+ * classes using this Pair by a more dedicated data structure (so we don't have
+ * to pass around generic signatures as is currently done, though at least the
+ * construction is helped a bit by the {@link #of} factory method.
+ *
+ * @param <S> The type of the first value
+ * @param <T> The type of the second value
+ */
+public class Pair<S,T> {
+    private final S mFirst;
+    private final T mSecond;
+
+    // Use {@link Pair#of} factory instead since it infers generic types
+    private Pair(S first, T second) {
+        this.mFirst = first;
+        this.mSecond = second;
+    }
+
+    /**
+     * Return the first item in the pair
+     *
+     * @return the first item in the pair
+     */
+    public S getFirst() {
+        return mFirst;
+    }
+
+    /**
+     * Return the second item in the pair
+     *
+     * @return the second item in the pair
+     */
+    public T getSecond() {
+        return mSecond;
+    }
+
+    /**
+     * Constructs a new pair of the given two objects, inferring generic types.
+     *
+     * @param first the first item to store in the pair
+     * @param second the second item to store in the pair
+     * @param <S> the type of the first item
+     * @param <T> the type of the second item
+     * @return a new pair wrapping the two items
+     */
+    public static <S,T> Pair<S,T> of(S first, T second) {
+        return new Pair<S,T>(first,second);
+    }
+
+    @Override
+    public String toString() {
+        return "Pair [first=" + mFirst + ", second=" + mSecond + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode());
+        result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode());
+        return result;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        Pair other = (Pair) obj;
+        if (mFirst == null) {
+            if (other.mFirst != null)
+                return false;
+        } else if (!mFirst.equals(other.mFirst))
+            return false;
+        if (mSecond == null) {
+            if (other.mSecond != null)
+                return false;
+        } else if (!mSecond.equals(other.mSecond))
+            return false;
+        return true;
+    }
+}
diff --git a/common/src/main/java/com/android/utils/PositionXmlParser.java b/common/src/main/java/com/android/utils/PositionXmlParser.java
new file mode 100644
index 0000000..acbd86c
--- /dev/null
+++ b/common/src/main/java/com/android/utils/PositionXmlParser.java
@@ -0,0 +1,729 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.utils;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.Text;
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.Locator;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+/**
+ * A simple DOM XML parser which can retrieve exact beginning and end offsets
+ * (and line and column numbers) for element nodes as well as attribute nodes.
+ */
+public class PositionXmlParser {
+    private static final String UTF_8 = "UTF-8";                 //$NON-NLS-1$
+    private static final String UTF_16 = "UTF_16";               //$NON-NLS-1$
+    private static final String UTF_16LE = "UTF_16LE";           //$NON-NLS-1$
+    private static final String CONTENT_KEY = "contents";        //$NON-NLS-1$
+    private static final String POS_KEY = "offsets";             //$NON-NLS-1$
+    private static final String NAMESPACE_PREFIX_FEATURE =
+            "http://xml.org/sax/features/namespace-prefixes";    //$NON-NLS-1$
+    private static final String NAMESPACE_FEATURE =
+            "http://xml.org/sax/features/namespaces";            //$NON-NLS-1$
+    /** See http://www.w3.org/TR/REC-xml/#NT-EncodingDecl */
+    private static final Pattern ENCODING_PATTERN =
+            Pattern.compile("encoding=['\"](\\S*)['\"]");//$NON-NLS-1$
+
+    /**
+     * Parses the XML content from the given input stream.
+     *
+     * @param input the input stream containing the XML to be parsed
+     * @return the corresponding document
+     * @throws ParserConfigurationException if a SAX parser is not available
+     * @throws SAXException if the document contains a parsing error
+     * @throws IOException if something is seriously wrong. This should not
+     *             happen since the input source is known to be constructed from
+     *             a string.
+     */
+    @Nullable
+    public Document parse(@NonNull InputStream input)
+            throws ParserConfigurationException, SAXException, IOException {
+        // Read in all the data
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        byte[] buf = new byte[1024];
+        while (true) {
+          int r = input.read(buf);
+          if (r == -1) {
+            break;
+          }
+          out.write(buf, 0, r);
+        }
+        input.close();
+        return parse(out.toByteArray());
+    }
+
+    /**
+     * Parses the XML content from the given byte array
+     *
+     * @param data the raw XML data (with unknown encoding)
+     * @return the corresponding document
+     * @throws ParserConfigurationException if a SAX parser is not available
+     * @throws SAXException if the document contains a parsing error
+     * @throws IOException if something is seriously wrong. This should not
+     *             happen since the input source is known to be constructed from
+     *             a string.
+     */
+    @Nullable
+    public Document parse(@NonNull byte[] data)
+            throws ParserConfigurationException, SAXException, IOException {
+        String xml = getXmlString(data);
+        return parse(xml, new InputSource(new StringReader(xml)), true);
+    }
+
+    /**
+     * Parses the given XML content.
+     *
+     * @param xml the XML string to be parsed. This must be in the correct
+     *     encoding already.
+     * @return the corresponding document
+     * @throws ParserConfigurationException if a SAX parser is not available
+     * @throws SAXException if the document contains a parsing error
+     * @throws IOException if something is seriously wrong. This should not
+     *             happen since the input source is known to be constructed from
+     *             a string.
+     */
+    @Nullable
+    public Document parse(@NonNull String xml)
+            throws ParserConfigurationException, SAXException, IOException {
+        return parse(xml, new InputSource(new StringReader(xml)), true);
+    }
+
+    @NonNull
+    private Document parse(@NonNull String xml, @NonNull InputSource input, boolean checkBom)
+            throws ParserConfigurationException, SAXException, IOException {
+        try {
+            SAXParserFactory factory = SAXParserFactory.newInstance();
+            factory.setFeature(NAMESPACE_FEATURE, true);
+            factory.setFeature(NAMESPACE_PREFIX_FEATURE, true);
+            SAXParser parser = factory.newSAXParser();
+            DomBuilder handler = new DomBuilder(xml);
+            parser.parse(input, handler);
+            return handler.getDocument();
+        } catch (SAXException e) {
+            if (checkBom && e.getMessage().contains("Content is not allowed in prolog")) {
+                // Byte order mark in the string? Skip it. There are many markers
+                // (see http://en.wikipedia.org/wiki/Byte_order_mark) so here we'll
+                // just skip those up to the XML prolog beginning character, <
+                xml = xml.replaceFirst("^([\\W]+)<","<");  //$NON-NLS-1$ //$NON-NLS-2$
+                return parse(xml, new InputSource(new StringReader(xml)), false);
+            }
+            throw e;
+        }
+    }
+
+    /**
+     * Returns the String corresponding to the given byte array of XML data
+     * (with unknown encoding). This method attempts to guess the encoding based
+     * on the XML prologue.
+     * @param data the XML data to be decoded into a string
+     * @return a string corresponding to the XML data
+     */
+    public static String getXmlString(byte[] data) {
+        int offset = 0;
+
+        String defaultCharset = UTF_8;
+        String charset = null;
+        // Look for the byte order mark, to see if we need to remove bytes from
+        // the input stream (and to determine whether files are big endian or little endian) etc
+        // for files which do not specify the encoding.
+        // See http://unicode.org/faq/utf_bom.html#BOM for more.
+        if (data.length > 4) {
+            if (data[0] == (byte)0xef && data[1] == (byte)0xbb && data[2] == (byte)0xbf) {
+                // UTF-8
+                defaultCharset = charset = UTF_8;
+                offset += 3;
+            } else if (data[0] == (byte)0xfe && data[1] == (byte)0xff) {
+                //  UTF-16, big-endian
+                defaultCharset = charset = UTF_16;
+                offset += 2;
+            } else if (data[0] == (byte)0x0 && data[1] == (byte)0x0
+                    && data[2] == (byte)0xfe && data[3] == (byte)0xff) {
+                // UTF-32, big-endian
+                defaultCharset = charset = "UTF_32";    //$NON-NLS-1$
+                offset += 4;
+            } else if (data[0] == (byte)0xff && data[1] == (byte)0xfe
+                    && data[2] == (byte)0x0 && data[3] == (byte)0x0) {
+                // UTF-32, little-endian. We must check for this *before* looking for
+                // UTF_16LE since UTF_32LE has the same prefix!
+                defaultCharset = charset = "UTF_32LE";  //$NON-NLS-1$
+                offset += 4;
+            } else if (data[0] == (byte)0xff && data[1] == (byte)0xfe) {
+                //  UTF-16, little-endian
+                defaultCharset = charset = UTF_16LE;
+                offset += 2;
+            }
+        }
+        int length = data.length - offset;
+
+        // Guess encoding by searching for an encoding= entry in the first line.
+        // The prologue, and the encoding names, will always be in ASCII - which means
+        // we don't need to worry about strange character encodings for the prologue characters.
+        // However, one wrinkle is that the whole file may be encoded in something like UTF-16
+        // where there are two bytes per character, so we can't just look for
+        //  ['e','n','c','o','d','i','n','g'] etc in the byte array since there could be
+        // multiple bytes for each character. However, since again the prologue is in ASCII,
+        // we can just drop the zeroes.
+        boolean seenOddZero = false;
+        boolean seenEvenZero = false;
+        int prologueStart = -1;
+        for (int lineEnd = offset; lineEnd < data.length; lineEnd++) {
+            if (data[lineEnd] == 0) {
+                if ((lineEnd - offset) % 2 == 0) {
+                    seenEvenZero = true;
+                } else {
+                    seenOddZero = true;
+                }
+            } else if (data[lineEnd] == '\n' || data[lineEnd] == '\r') {
+                break;
+            } else if (data[lineEnd] == '<') {
+                prologueStart = lineEnd;
+            } else if (data[lineEnd] == '>') {
+                // End of prologue. Quick check to see if this is a utf-8 file since that's
+                // common
+                for (int i = lineEnd - 4; i >= 0; i--) {
+                    if ((data[i] == 'u' || data[i] == 'U')
+                            && (data[i + 1] == 't' || data[i + 1] == 'T')
+                            && (data[i + 2] == 'f' || data[i + 2] == 'F')
+                            && (data[i + 3] == '-' || data[i + 3] == '_')
+                            && (data[i + 4] == '8')
+                            ) {
+                        charset = UTF_8;
+                        break;
+                    }
+                }
+
+                if (charset == null) {
+                    StringBuilder sb = new StringBuilder();
+                    for (int i = prologueStart; i <= lineEnd; i++) {
+                        if (data[i] != 0) {
+                            sb.append((char) data[i]);
+                        }
+                    }
+                    String prologue = sb.toString();
+                    int encodingIndex = prologue.indexOf("encoding"); //$NON-NLS-1$
+                    if (encodingIndex != -1) {
+                        Matcher matcher = ENCODING_PATTERN.matcher(prologue);
+                        if (matcher.find(encodingIndex)) {
+                            charset = matcher.group(1);
+                        }
+                    }
+                }
+
+                break;
+            }
+        }
+
+        // No prologue on the first line, and no byte order mark: Assume UTF-8/16
+        if (charset == null) {
+            charset = seenOddZero ? UTF_16LE : seenEvenZero ? UTF_16 : UTF_8;
+        }
+
+        String xml = null;
+        try {
+            xml = new String(data, offset, length, charset);
+        } catch (UnsupportedEncodingException e) {
+            try {
+                if (charset != defaultCharset) {
+                    xml = new String(data, offset, length, defaultCharset);
+                }
+            } catch (UnsupportedEncodingException u) {
+                // Just use the default encoding below
+            }
+        }
+        if (xml == null) {
+            xml = new String(data, offset, length);
+        }
+        return xml;
+    }
+
+    /**
+     * Returns the position for the given node. This is the start position. The
+     * end position can be obtained via {@link Position#getEnd()}.
+     *
+     * @param node the node to look up position for
+     * @return the position, or null if the node type is not supported for
+     *         position info
+     */
+    @Nullable
+    public Position getPosition(@NonNull Node node) {
+        return getPosition(node, -1, -1);
+    }
+
+    /**
+     * Returns the position for the given node. This is the start position. The
+     * end position can be obtained via {@link Position#getEnd()}. A specific
+     * range within the node can be specified with the {@code start} and
+     * {@code end} parameters.
+     *
+     * @param node the node to look up position for
+     * @param start the relative offset within the node range to use as the
+     *            starting position, inclusive, or -1 to not limit the range
+     * @param end the relative offset within the node range to use as the ending
+     *            position, or -1 to not limit the range
+     * @return the position, or null if the node type is not supported for
+     *         position info
+     */
+    @Nullable
+    public Position getPosition(@NonNull Node node, int start, int end) {
+        // Look up the position information stored while parsing for the given node.
+        // Note however that we only store position information for elements (because
+        // there is no SAX callback for individual attributes).
+        // Therefore, this method special cases this:
+        //  -- First, it looks at the owner element and uses its position
+        //     information as a first approximation.
+        //  -- Second, it uses that, as well as the original XML text, to search
+        //     within the node range for an exact text match on the attribute name
+        //     and if found uses that as the exact node offsets instead.
+        if (node instanceof Attr) {
+            Attr attr = (Attr) node;
+            Position pos = (Position) attr.getOwnerElement().getUserData(POS_KEY);
+            if (pos != null) {
+                int startOffset = pos.getOffset();
+                int endOffset = pos.getEnd().getOffset();
+                if (start != -1) {
+                    startOffset += start;
+                    if (end != -1) {
+                        endOffset = start + end;
+                    }
+                }
+
+                // Find attribute in the text
+                String contents = (String) node.getOwnerDocument().getUserData(CONTENT_KEY);
+                if (contents == null) {
+                    return null;
+                }
+
+                // Locate the name=value attribute in the source text
+                // Fast string check first for the common occurrence
+                String name = attr.getName();
+                Pattern pattern = Pattern.compile(
+                        String.format("%1$s\\s*=\\s*[\"'].*[\"']", name)); //$NON-NLS-1$
+                Matcher matcher = pattern.matcher(contents);
+                if (matcher.find(startOffset) && matcher.start() <= endOffset) {
+                    int index = matcher.start();
+                    // Adjust the line and column to this new offset
+                    int line = pos.getLine();
+                    int column = pos.getColumn();
+                    for (int offset = pos.getOffset(); offset < index; offset++) {
+                        char t = contents.charAt(offset);
+                        if (t == '\n') {
+                            line++;
+                            column = 0;
+                        } else {
+                            column++;
+                        }
+                    }
+
+                    Position attributePosition = createPosition(line, column, index);
+                    // Also set end range for retrieval in getLocation
+                    attributePosition.setEnd(createPosition(line, column + matcher.end() - index,
+                            matcher.end()));
+                    return attributePosition;
+                } else {
+                    // No regexp match either: just fall back to element position
+                    return pos;
+                }
+            }
+        } else if (node instanceof Text) {
+            // Position of parent element, if any
+            Position pos = null;
+            if (node.getPreviousSibling() != null) {
+                pos = (Position) node.getPreviousSibling().getUserData(POS_KEY);
+            }
+            if (pos == null) {
+                pos = (Position) node.getParentNode().getUserData(POS_KEY);
+            }
+            if (pos != null) {
+                // Attempt to point forward to the actual text node
+                int startOffset = pos.getOffset();
+                int endOffset = pos.getEnd().getOffset();
+                int line = pos.getLine();
+                int column = pos.getColumn();
+
+                // Find attribute in the text
+                String contents = (String) node.getOwnerDocument().getUserData(CONTENT_KEY);
+                if (contents == null || contents.length() < endOffset) {
+                    return null;
+                }
+
+                boolean inAttribute = false;
+                for (int offset = startOffset; offset <= endOffset; offset++) {
+                    char c = contents.charAt(offset);
+                    if (c == '>' && !inAttribute) {
+                        // Found the end of the element open tag: this is where the
+                        // text begins.
+
+                        // Skip >
+                        offset++;
+                        column++;
+
+                        String text = node.getNodeValue();
+                        int textIndex = 0;
+                        int textLength = text.length();
+                        int newLine = line;
+                        int newColumn = column;
+                        if (start != -1) {
+                            textLength = Math.min(textLength, start);
+                            for (; textIndex < textLength; textIndex++) {
+                                char t = text.charAt(textIndex);
+                                if (t == '\n') {
+                                    newLine++;
+                                    newColumn = 0;
+                                } else {
+                                    newColumn++;
+                                }
+                            }
+                        } else {
+                            // Skip text whitespace prefix, if the text node contains
+                            // non-whitespace characters
+                            for (; textIndex < textLength; textIndex++) {
+                                char t = text.charAt(textIndex);
+                                if (t == '\n') {
+                                    newLine++;
+                                    newColumn = 0;
+                                } else if (!Character.isWhitespace(t)) {
+                                    break;
+                                } else {
+                                    newColumn++;
+                                }
+                            }
+                        }
+                        if (textIndex == text.length()) {
+                            textIndex = 0; // Whitespace node
+                        } else {
+                            line = newLine;
+                            column = newColumn;
+                        }
+
+                        Position attributePosition = createPosition(line, column,
+                                offset + textIndex);
+                        // Also set end range for retrieval in getLocation
+                        if (end != -1) {
+                            attributePosition.setEnd(createPosition(line, column,
+                                    offset + end));
+                        } else {
+                            attributePosition.setEnd(createPosition(line, column,
+                                    offset + textLength));
+                        }
+                        return attributePosition;
+                    } else if (c == '"') {
+                        inAttribute = !inAttribute;
+                    } else if (c == '\n') {
+                        line++;
+                        column = -1; // pre-subtract column added below
+                    }
+                    column++;
+                }
+
+                return pos;
+            }
+        }
+
+        return (Position) node.getUserData(POS_KEY);
+    }
+
+    /**
+     * SAX parser handler which incrementally builds up a DOM document as we go
+     * along, and updates position information along the way. Position
+     * information is attached to the DOM nodes by setting user data with the
+     * {@link POS_KEY} key.
+     */
+    private final class DomBuilder extends DefaultHandler {
+        private final String mXml;
+        private final Document mDocument;
+        private Locator mLocator;
+        private int mCurrentLine = 0;
+        private int mCurrentOffset;
+        private int mCurrentColumn;
+        private final List<Element> mStack = new ArrayList<Element>();
+        private final StringBuilder mPendingText = new StringBuilder();
+
+        private DomBuilder(String xml) throws ParserConfigurationException {
+            mXml = xml;
+
+            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+            factory.setNamespaceAware(true);
+            factory.setValidating(false);
+            DocumentBuilder docBuilder = factory.newDocumentBuilder();
+            mDocument = docBuilder.newDocument();
+            mDocument.setUserData(CONTENT_KEY, xml, null);
+        }
+
+        /** Returns the document parsed by the handler */
+        Document getDocument() {
+            return mDocument;
+        }
+
+        @Override
+        public void setDocumentLocator(Locator locator) {
+            this.mLocator = locator;
+        }
+
+        @Override
+        public void startElement(String uri, String localName, String qName,
+                Attributes attributes) throws SAXException {
+            try {
+                flushText();
+                Element element = mDocument.createElement(qName);
+                for (int i = 0; i < attributes.getLength(); i++) {
+                    if (attributes.getURI(i) != null && attributes.getURI(i).length() > 0) {
+                        Attr attr = mDocument.createAttributeNS(attributes.getURI(i),
+                                attributes.getQName(i));
+                        attr.setValue(attributes.getValue(i));
+                        element.setAttributeNodeNS(attr);
+                        assert attr.getOwnerElement() == element;
+                    } else {
+                        Attr attr = mDocument.createAttribute(attributes.getQName(i));
+                        attr.setValue(attributes.getValue(i));
+                        element.setAttributeNode(attr);
+                        assert attr.getOwnerElement() == element;
+                    }
+                }
+
+                Position pos = getCurrentPosition();
+
+                // The starting position reported to us by SAX is really the END of the
+                // open tag in an element, when all the attributes have been processed.
+                // We have to scan backwards to find the real beginning. We'll do that
+                // by scanning backwards.
+                // -1: Make sure that when we have <foo></foo> we don't consider </foo>
+                // the beginning since pos.offset will typically point to the first character
+                // AFTER the element open tag, which could be a closing tag or a child open
+                // tag
+
+                for (int offset = pos.getOffset() - 1; offset >= 0; offset--) {
+                    char c = mXml.charAt(offset);
+                    // < cannot appear in attribute values or anywhere else within
+                    // an element open tag, so we know the first occurrence is the real
+                    // element start
+                    if (c == '<') {
+                        // Adjust line position
+                        int line = pos.getLine();
+                        for (int i = offset, n = pos.getOffset(); i < n; i++) {
+                            if (mXml.charAt(i) == '\n') {
+                                line--;
+                            }
+                        }
+
+                        // Compute new column position
+                        int column = 0;
+                        for (int i = offset - 1; i >= 0; i--, column++) {
+                            if (mXml.charAt(i) == '\n') {
+                                break;
+                            }
+                        }
+
+                        pos = createPosition(line, column, offset);
+                        break;
+                    }
+                }
+
+                element.setUserData(POS_KEY, pos, null);
+                mStack.add(element);
+            } catch (Exception t) {
+                throw new SAXException(t);
+            }
+        }
+
+        @Override
+        public void endElement(String uri, String localName, String qName) {
+            flushText();
+            Element element = mStack.remove(mStack.size() - 1);
+
+            Position pos = (Position) element.getUserData(POS_KEY);
+            assert pos != null;
+            pos.setEnd(getCurrentPosition());
+
+            if (mStack.isEmpty()) {
+                mDocument.appendChild(element);
+            } else {
+                Element parent = mStack.get(mStack.size() - 1);
+                parent.appendChild(element);
+            }
+        }
+
+        /**
+         * Returns a position holder for the current position. The most
+         * important part of this function is to incrementally compute the
+         * offset as well, by counting forwards until it reaches the new line
+         * number and column position of the XML parser, counting characters as
+         * it goes along.
+         */
+        private Position getCurrentPosition() {
+            int line = mLocator.getLineNumber() - 1;
+            int column = mLocator.getColumnNumber() - 1;
+
+            // Compute offset incrementally now that we have the new line and column
+            // numbers
+            int xmlLength = mXml.length();
+            while (mCurrentLine < line && mCurrentOffset < xmlLength) {
+                char c = mXml.charAt(mCurrentOffset);
+                if (c == '\r' && mCurrentOffset < xmlLength - 1) {
+                    if (mXml.charAt(mCurrentOffset + 1) != '\n') {
+                        mCurrentLine++;
+                        mCurrentColumn = 0;
+                    }
+                } else if (c == '\n') {
+                    mCurrentLine++;
+                    mCurrentColumn = 0;
+                } else {
+                    mCurrentColumn++;
+                }
+                mCurrentOffset++;
+            }
+
+            mCurrentOffset += column - mCurrentColumn;
+            if (mCurrentOffset >= xmlLength) {
+                // The parser sometimes passes wrong column numbers at the
+                // end of the file: Ensure that the offset remains valid.
+                mCurrentOffset = xmlLength;
+            }
+            mCurrentColumn = column;
+
+            return createPosition(mCurrentLine, mCurrentColumn, mCurrentOffset);
+        }
+
+        @Override
+        public void characters(char c[], int start, int length) throws SAXException {
+            mPendingText.append(c, start, length);
+        }
+
+        private void flushText() {
+            if (mPendingText.length() > 0 && !mStack.isEmpty()) {
+                Element element = mStack.get(mStack.size() - 1);
+                Node textNode = mDocument.createTextNode(mPendingText.toString());
+                element.appendChild(textNode);
+                mPendingText.setLength(0);
+            }
+        }
+    }
+
+    /**
+     * Creates a position while constructing the DOM document. This method
+     * allows a subclass to create a custom implementation of the position
+     * class.
+     *
+     * @param line the line number for the position
+     * @param column the column number for the position
+     * @param offset the character offset
+     * @return a new position
+     */
+    @NonNull
+    protected Position createPosition(int line, int column, int offset) {
+        return new DefaultPosition(line, column, offset);
+    }
+
+    protected interface Position {
+        /**
+         * Linked position: for a begin position this will point to the
+         * corresponding end position. For an end position this will be null.
+         *
+         * @return the end position, or null
+         */
+        @Nullable
+        public Position getEnd();
+
+        /**
+         * Linked position: for a begin position this will point to the
+         * corresponding end position. For an end position this will be null.
+         *
+         * @param end the end position
+         */
+        public void setEnd(@NonNull Position end);
+
+        /** @return the line number, 0-based */
+        public int getLine();
+
+        /** @return the offset number, 0-based */
+        public int getOffset();
+
+        /** @return the column number, 0-based, and -1 if the column number if not known */
+        public int getColumn();
+    }
+
+    protected static class DefaultPosition implements Position {
+        /** The line number (0-based where the first line is line 0) */
+        private final int mLine;
+        private final int mColumn;
+        private final int mOffset;
+        private Position mEnd;
+
+        /**
+         * Creates a new {@link Position}
+         *
+         * @param line the 0-based line number, or -1 if unknown
+         * @param column the 0-based column number, or -1 if unknown
+         * @param offset the offset, or -1 if unknown
+         */
+        public DefaultPosition(int line, int column, int offset) {
+            this.mLine = line;
+            this.mColumn = column;
+            this.mOffset = offset;
+        }
+
+        @Override
+        public int getLine() {
+            return mLine;
+        }
+
+        @Override
+        public int getOffset() {
+            return mOffset;
+        }
+
+        @Override
+        public int getColumn() {
+            return mColumn;
+        }
+
+        @Override
+        public Position getEnd() {
+            return mEnd;
+        }
+
+        @Override
+        public void setEnd(@NonNull Position end) {
+            mEnd = end;
+        }
+    }
+}
diff --git a/common/src/main/java/com/android/utils/SdkUtils.java b/common/src/main/java/com/android/utils/SdkUtils.java
new file mode 100644
index 0000000..d610527
--- /dev/null
+++ b/common/src/main/java/com/android/utils/SdkUtils.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright (C) 2012 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.utils;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+
+import java.text.NumberFormat;
+import java.text.ParseException;
+
+/** Miscellaneous utilities used by the Android SDK tools */
+public class SdkUtils {
+    /**
+     * Returns true if the given string ends with the given suffix, using a
+     * case-insensitive comparison.
+     *
+     * @param string the full string to be checked
+     * @param suffix the suffix to be checked for
+     * @return true if the string case-insensitively ends with the given suffix
+     */
+    public static boolean endsWithIgnoreCase(String string, String suffix) {
+        return string.regionMatches(true /* ignoreCase */, string.length() - suffix.length(),
+                suffix, 0, suffix.length());
+    }
+
+    /**
+     * Returns true if the given sequence ends with the given suffix (case
+     * sensitive).
+     *
+     * @param sequence the character sequence to be checked
+     * @param suffix the suffix to look for
+     * @return true if the given sequence ends with the given suffix
+     */
+    public static boolean endsWith(CharSequence sequence, CharSequence suffix) {
+        return endsWith(sequence, sequence.length(), suffix);
+    }
+
+    /**
+     * Returns true if the given sequence ends at the given offset with the given suffix (case
+     * sensitive)
+     *
+     * @param sequence the character sequence to be checked
+     * @param endOffset the offset at which the sequence is considered to end
+     * @param suffix the suffix to look for
+     * @return true if the given sequence ends with the given suffix
+     */
+    public static boolean endsWith(CharSequence sequence, int endOffset, CharSequence suffix) {
+        if (endOffset < suffix.length()) {
+            return false;
+        }
+
+        for (int i = endOffset - 1, j = suffix.length() - 1; j >= 0; i--, j--) {
+            if (sequence.charAt(i) != suffix.charAt(j)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns true if the given string starts with the given prefix, using a
+     * case-insensitive comparison.
+     *
+     * @param string the full string to be checked
+     * @param prefix the prefix to be checked for
+     * @return true if the string case-insensitively starts with the given prefix
+     */
+    public static boolean startsWithIgnoreCase(String string, String prefix) {
+        return string.regionMatches(true /* ignoreCase */, 0, prefix, 0, prefix.length());
+    }
+
+    /**
+     * Returns true if the given string starts at the given offset with the
+     * given prefix, case insensitively.
+     *
+     * @param string the full string to be checked
+     * @param offset the offset in the string to start looking
+     * @param prefix the prefix to be checked for
+     * @return true if the string case-insensitively starts at the given offset
+     *         with the given prefix
+     */
+    public static boolean startsWith(String string, int offset, String prefix) {
+        return string.regionMatches(true /* ignoreCase */, offset, prefix, 0, prefix.length());
+    }
+
+    /**
+     * Strips the whitespace from the given string
+     *
+     * @param string the string to be cleaned up
+     * @return the string, without whitespace
+     */
+    public static String stripWhitespace(String string) {
+        StringBuilder sb = new StringBuilder(string.length());
+        for (int i = 0, n = string.length(); i < n; i++) {
+            char c = string.charAt(i);
+            if (!Character.isWhitespace(c)) {
+                sb.append(c);
+            }
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * Returns true if the given string has an upper case character.
+     *
+     * @param s the string to check
+     * @return true if it contains uppercase characters
+     */
+    public static boolean hasUpperCaseCharacter(String s) {
+        for (int i = 0; i < s.length(); i++) {
+            if (Character.isUpperCase(s.charAt(i))) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /** For use by {@link #getLineSeparator()} */
+    private static String sLineSeparator;
+
+    /**
+     * Returns the default line separator to use.
+     * <p>
+     * NOTE: If you have an associated IDocument (Eclipse), it is better to call
+     * TextUtilities#getDefaultLineDelimiter(IDocument) since that will
+     * allow (for example) editing a \r\n-delimited document on a \n-delimited
+     * platform and keep a consistent usage of delimiters in the file.
+     *
+     * @return the delimiter string to use
+     */
+    @NonNull
+    public static String getLineSeparator() {
+        if (sLineSeparator == null) {
+            // This is guaranteed to exist:
+            sLineSeparator = System.getProperty("line.separator"); //$NON-NLS-1$
+        }
+
+        return sLineSeparator;
+    }
+
+    /**
+     * Wraps the given text at the given line width, with an optional hanging
+     * indent.
+     *
+     * @param text the text to be wrapped
+     * @param lineWidth the number of characters to wrap the text to
+     * @param hangingIndent the hanging indent (to be used for the second and
+     *            subsequent lines in each paragraph, or null if not known
+     * @return the string, wrapped
+     */
+    @NonNull
+    public static String wrap(
+            @NonNull String text,
+            int lineWidth,
+            @Nullable String hangingIndent) {
+        if (hangingIndent == null) {
+            hangingIndent = "";
+        }
+        int explanationLength = text.length();
+        StringBuilder sb = new StringBuilder(explanationLength * 2);
+        int index = 0;
+
+        while (index < explanationLength) {
+            int lineEnd = text.indexOf('\n', index);
+            int next;
+
+            if (lineEnd != -1 && (lineEnd - index) < lineWidth) {
+                next = lineEnd + 1;
+            } else {
+                // Line is longer than available width; grab as much as we can
+                lineEnd = Math.min(index + lineWidth, explanationLength);
+                if (lineEnd - index < lineWidth) {
+                    next = explanationLength;
+                } else {
+                    // then back up to the last space
+                    int lastSpace = text.lastIndexOf(' ', lineEnd);
+                    if (lastSpace > index) {
+                        lineEnd = lastSpace;
+                        next = lastSpace + 1;
+                    } else {
+                        // No space anywhere on the line: it contains something wider than
+                        // can fit (like a long URL) so just hard break it
+                        next = lineEnd + 1;
+                    }
+                }
+            }
+
+            if (sb.length() > 0) {
+                sb.append(hangingIndent);
+            } else {
+                lineWidth -= hangingIndent.length();
+            }
+
+            sb.append(text.substring(index, lineEnd));
+            sb.append('\n');
+            index = next;
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * Returns the given localized string as an int. For example, in the
+     * US locale, "1,000", will return 1000. In the French locale, "1.000" will return
+     * 1000. It will return 0 for empty strings.
+     * <p>
+     * To parse a string without catching parser exceptions, call
+     * {@link #parseLocalizedInt(String, int)} instead, passing the
+     * default value to be returned if the format is invalid.
+     *
+     * @param string the string to be parsed
+     * @return the integer value
+     * @throws ParseException if the format is not correct
+     */
+    public static int parseLocalizedInt(@NonNull String string) throws ParseException {
+        if (string.isEmpty()) {
+            return 0;
+        }
+        return NumberFormat.getIntegerInstance().parse(string).intValue();
+    }
+
+    /**
+     * Returns the given localized string as an int. For example, in the
+     * US locale, "1,000", will return 1000. In the French locale, "1.000" will return
+     * 1000.  If the format is invalid, returns the supplied default value instead.
+     *
+     * @param string the string to be parsed
+     * @param defaultValue the value to be returned if there is a parsing error
+     * @return the integer value
+     */
+    public static int parseLocalizedInt(@NonNull String string, int defaultValue) {
+        try {
+            return parseLocalizedInt(string);
+        } catch (ParseException e) {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * Returns the given localized string as a double. For example, in the
+     * US locale, "3.14", will return 3.14. In the French locale, "3,14" will return
+     * 3.14. It will return 0 for empty strings.
+     * <p>
+     * To parse a string without catching parser exceptions, call
+     * {@link #parseLocalizedDouble(String, double)} instead, passing the
+     * default value to be returned if the format is invalid.
+     *
+     * @param string the string to be parsed
+     * @return the double value
+     * @throws ParseException if the format is not correct
+     */
+    public static double parseLocalizedDouble(@NonNull String string) throws ParseException {
+        if (string.isEmpty()) {
+            return 0.0;
+        }
+        return NumberFormat.getNumberInstance().parse(string).doubleValue();
+    }
+
+    /**
+     * Returns the given localized string as a double. For example, in the
+     * US locale, "3.14", will return 3.14. In the French locale, "3,14" will return
+     * 3.14. If the format is invalid, returns the supplied default value instead.
+     *
+     * @param string the string to be parsed
+     * @param defaultValue the value to be returned if there is a parsing error
+     * @return the double value
+     */
+    public static double parseLocalizedDouble(@NonNull String string, double defaultValue) {
+        try {
+            return parseLocalizedDouble(string);
+        } catch (ParseException e) {
+            return defaultValue;
+        }
+    }
+}
diff --git a/common/src/main/java/com/android/utils/StdLogger.java b/common/src/main/java/com/android/utils/StdLogger.java
new file mode 100644
index 0000000..2138863
--- /dev/null
+++ b/common/src/main/java/com/android/utils/StdLogger.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2012 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.utils;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+
+import java.io.PrintStream;
+import java.util.Formatter;
+
+
+/**
+ * An implementation of {@link ILogger} that prints to {@link System#out} and {@link System#err}.
+ * <p/>
+ *
+ */
+public class StdLogger implements ILogger {
+
+    private final Level mLevel;
+
+    public enum Level {
+        VERBOSE(0),
+        INFO(1),
+        WARNING(2),
+        ERROR(3);
+
+        private final int mLevel;
+
+        Level(int level) {
+            mLevel = level;
+        }
+    }
+
+    /**
+     * Creates the {@link StdLogger} with a given log {@link Level}.
+     * @param level the log Level.
+     */
+    public StdLogger(@NonNull Level level) {
+        if (level == null) {
+            throw new IllegalArgumentException("level cannot be null");
+        }
+
+        mLevel = level;
+    }
+
+    /**
+     * Returns the logger's log {@link Level}.
+     * @return the log level.
+     */
+    public Level getLevel() {
+        return mLevel;
+    }
+
+    /**
+     * Prints an error message.
+     * <p/>
+     * The message will be tagged with "Error" on the output so the caller does not
+     * need to put such a prefix in the format string.
+     * <p/>
+     * The output is done on {@link System#err}.
+     * <p/>
+     * This is always displayed, independent of the logging {@link Level}.
+     *
+     * @param t is an optional {@link Throwable} or {@link Exception}. If non-null, it's
+     *          message will be printed out.
+     * @param errorFormat is an optional error format. If non-null, it will be printed
+     *          using a {@link Formatter} with the provided arguments.
+     * @param args provides the arguments for errorFormat.
+     */
+    @Override
+    public void error(@Nullable Throwable t, @Nullable String errorFormat, Object... args) {
+        if (errorFormat != null) {
+            String msg = String.format("Error: " + errorFormat, args);
+
+            printMessage(msg, System.err);
+        }
+        if (t != null) {
+            System.err.println(String.format("Error: %1$s", t.getMessage()));
+        }
+    }
+
+    /**
+     * Prints a warning message.
+     * <p/>
+     * The message will be tagged with "Warning" on the output so the caller does not
+     * need to put such a prefix in the format string.
+     * <p/>
+     * The output is done on {@link System#out}.
+     * <p/>
+     * This is displayed only if the logging {@link Level} is {@link Level#WARNING} or higher.
+     *
+     * @param warningFormat is a string format to be used with a {@link Formatter}. Cannot be null.
+     * @param args provides the arguments for warningFormat.
+     */
+    @Override
+    public void warning(@NonNull String warningFormat, Object... args) {
+        if (mLevel.mLevel > Level.WARNING.mLevel) {
+            return;
+        }
+
+        String msg = String.format("Warning: " + warningFormat, args);
+
+        printMessage(msg, System.out);
+    }
+
+    /**
+     * Prints an info message.
+     * <p/>
+     * The output is done on {@link System#out}.
+     * <p/>
+     * This is displayed only if the logging {@link Level} is {@link Level#INFO} or higher.
+     *
+     * @param msgFormat is a string format to be used with a {@link Formatter}. Cannot be null.
+     * @param args provides the arguments for msgFormat.
+     */
+    @Override
+    public void info(@NonNull String msgFormat, Object... args) {
+        if (mLevel.mLevel > Level.INFO.mLevel) {
+            return;
+        }
+
+        String msg = String.format(msgFormat, args);
+
+        printMessage(msg, System.out);
+    }
+
+    /**
+     * Prints a verbose message.
+     * <p/>
+     * The output is done on {@link System#out}.
+     * <p/>
+     * This is displayed only if the logging {@link Level} is {@link Level#VERBOSE} or higher.
+     *
+     * @param msgFormat is a string format to be used with a {@link Formatter}. Cannot be null.
+     * @param args provides the arguments for msgFormat.
+     */
+    @Override
+    public void verbose(@NonNull String msgFormat, Object... args) {
+        if (mLevel.mLevel > Level.VERBOSE.mLevel) {
+            return;
+        }
+
+        String msg = String.format(msgFormat, args);
+
+        printMessage(msg, System.out);
+    }
+
+    private void printMessage(String msg, PrintStream stream) {
+        if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS &&
+                !msg.endsWith("\r\n") &&
+                msg.endsWith("\n")) {
+            // remove last \n so that println can use \r\n as needed.
+            msg = msg.substring(0, msg.length() - 1);
+        }
+
+        stream.print(msg);
+
+        if (!msg.endsWith("\n")) {
+            stream.println();
+        }
+    }
+
+}
diff --git a/common/src/main/java/com/android/utils/XmlUtils.java b/common/src/main/java/com/android/utils/XmlUtils.java
new file mode 100644
index 0000000..999375f
--- /dev/null
+++ b/common/src/main/java/com/android/utils/XmlUtils.java
@@ -0,0 +1,432 @@
+/*
+ * Copyright (C) 2012 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.utils;
+
+import static com.android.SdkConstants.AMP_ENTITY;
+import static com.android.SdkConstants.ANDROID_NS_NAME;
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.APOS_ENTITY;
+import static com.android.SdkConstants.APP_PREFIX;
+import static com.android.SdkConstants.LT_ENTITY;
+import static com.android.SdkConstants.QUOT_ENTITY;
+import static com.android.SdkConstants.XMLNS;
+import static com.android.SdkConstants.XMLNS_PREFIX;
+import static com.android.SdkConstants.XMLNS_URI;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+import java.io.StringReader;
+import java.util.HashSet;
+import java.util.Locale;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+/** XML Utilities */
+public class XmlUtils {
+    public static final String XML_COMMENT_BEGIN = "<!--"; //$NON-NLS-1$
+    public static final String XML_COMMENT_END = "-->";    //$NON-NLS-1$
+    public static final String XML_PROLOG =
+            "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";  //$NON-NLS-1$
+
+    /**
+     * Returns the namespace prefix matching the requested namespace URI.
+     * If no such declaration is found, returns the default "android" prefix for
+     * the Android URI, and "app" for other URI's. By default the app namespace
+     * will be created. If this is not desirable, call
+     * {@link #lookupNamespacePrefix(Node, String, boolean)} instead.
+     *
+     * @param node The current node. Must not be null.
+     * @param nsUri The namespace URI of which the prefix is to be found,
+     *              e.g. {@link SdkConstants#ANDROID_URI}
+     * @return The first prefix declared or the default "android" prefix
+     *              (or "app" for non-Android URIs)
+     */
+    @NonNull
+    public static String lookupNamespacePrefix(@NonNull Node node, @NonNull String nsUri) {
+        String defaultPrefix = ANDROID_URI.equals(nsUri) ? ANDROID_NS_NAME : APP_PREFIX;
+        return lookupNamespacePrefix(node, nsUri, defaultPrefix, true /*create*/);
+    }
+
+    /**
+     * Returns the namespace prefix matching the requested namespace URI. If no
+     * such declaration is found, returns the default "android" prefix for the
+     * Android URI, and "app" for other URI's.
+     *
+     * @param node The current node. Must not be null.
+     * @param nsUri The namespace URI of which the prefix is to be found, e.g.
+     *            {@link SdkConstants#ANDROID_URI}
+     * @param create whether the namespace declaration should be created, if
+     *            necessary
+     * @return The first prefix declared or the default "android" prefix (or
+     *         "app" for non-Android URIs)
+     */
+    @NonNull
+    public static String lookupNamespacePrefix(@NonNull Node node, @NonNull String nsUri,
+            boolean create) {
+        String defaultPrefix = ANDROID_URI.equals(nsUri) ? ANDROID_NS_NAME : APP_PREFIX;
+        return lookupNamespacePrefix(node, nsUri, defaultPrefix, create);
+    }
+
+    /**
+     * Returns the namespace prefix matching the requested namespace URI. If no
+     * such declaration is found, returns the default "android" prefix.
+     *
+     * @param node The current node. Must not be null.
+     * @param nsUri The namespace URI of which the prefix is to be found, e.g.
+     *            {@link SdkConstants#ANDROID_URI}
+     * @param defaultPrefix The default prefix (root) to use if the namespace is
+     *            not found. If null, do not create a new namespace if this URI
+     *            is not defined for the document.
+     * @param create whether the namespace declaration should be created, if
+     *            necessary
+     * @return The first prefix declared or the provided prefix (possibly with a
+     *            number appended to avoid conflicts with existing prefixes.
+     */
+    public static String lookupNamespacePrefix(
+            @Nullable Node node, @Nullable String nsUri, @Nullable String defaultPrefix,
+            boolean create) {
+        // Note: Node.lookupPrefix is not implemented in wst/xml/core NodeImpl.java
+        // The following code emulates this simple call:
+        //   String prefix = node.lookupPrefix(NS_RESOURCES);
+
+        // if the requested URI is null, it denotes an attribute with no namespace.
+        if (nsUri == null) {
+            return null;
+        }
+
+        // per XML specification, the "xmlns" URI is reserved
+        if (XMLNS_URI.equals(nsUri)) {
+            return XMLNS;
+        }
+
+        HashSet<String> visited = new HashSet<String>();
+        Document doc = node == null ? null : node.getOwnerDocument();
+
+        // Ask the document about it. This method may not be implemented by the Document.
+        String nsPrefix = null;
+        try {
+            nsPrefix = doc != null ? doc.lookupPrefix(nsUri) : null;
+            if (nsPrefix != null) {
+                return nsPrefix;
+            }
+        } catch (Throwable t) {
+            // ignore
+        }
+
+        // If that failed, try to look it up manually.
+        // This also gathers prefixed in use in the case we want to generate a new one below.
+        for (; node != null && node.getNodeType() == Node.ELEMENT_NODE;
+               node = node.getParentNode()) {
+            NamedNodeMap attrs = node.getAttributes();
+            for (int n = attrs.getLength() - 1; n >= 0; --n) {
+                Node attr = attrs.item(n);
+                if (XMLNS.equals(attr.getPrefix())) {
+                    String uri = attr.getNodeValue();
+                    nsPrefix = attr.getLocalName();
+                    // Is this the URI we are looking for? If yes, we found its prefix.
+                    if (nsUri.equals(uri)) {
+                        return nsPrefix;
+                    }
+                    visited.add(nsPrefix);
+                }
+            }
+        }
+
+        // Failed the find a prefix. Generate a new sensible default prefix, unless
+        // defaultPrefix was null in which case the caller does not want the document
+        // modified.
+        if (defaultPrefix == null) {
+            return null;
+        }
+
+        //
+        // We need to make sure the prefix is not one that was declared in the scope
+        // visited above. Pick a unique prefix from the provided default prefix.
+        String prefix = defaultPrefix;
+        String base = prefix;
+        for (int i = 1; visited.contains(prefix); i++) {
+            prefix = base + Integer.toString(i);
+        }
+        // Also create & define this prefix/URI in the XML document as an attribute in the
+        // first element of the document.
+        if (doc != null) {
+            node = doc.getFirstChild();
+            while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
+                node = node.getNextSibling();
+            }
+            if (node != null && create) {
+                // This doesn't work:
+                //Attr attr = doc.createAttributeNS(XMLNS_URI, prefix);
+                //attr.setPrefix(XMLNS);
+                //
+                // Xerces throws
+                //org.w3c.dom.DOMException: NAMESPACE_ERR: An attempt is made to create or
+                // change an object in a way which is incorrect with regard to namespaces.
+                //
+                // Instead pass in the concatenated prefix. (This is covered by
+                // the UiElementNodeTest#testCreateNameSpace() test.)
+                Attr attr = doc.createAttributeNS(XMLNS_URI, XMLNS_PREFIX + prefix);
+                attr.setValue(nsUri);
+                node.getAttributes().setNamedItemNS(attr);
+            }
+        }
+
+        return prefix;
+    }
+
+    /**
+     * Converts the given attribute value to an XML-attribute-safe value, meaning that
+     * single and double quotes are replaced with their corresponding XML entities.
+     *
+     * @param attrValue the value to be escaped
+     * @return the escaped value
+     */
+    @NonNull
+    public static String toXmlAttributeValue(@NonNull String attrValue) {
+        for (int i = 0, n = attrValue.length(); i < n; i++) {
+            char c = attrValue.charAt(i);
+            if (c == '"' || c == '\'' || c == '<' || c == '&') {
+                StringBuilder sb = new StringBuilder(2 * attrValue.length());
+                appendXmlAttributeValue(sb, attrValue);
+                return sb.toString();
+            }
+        }
+
+        return attrValue;
+    }
+
+    /**
+     * Converts the given attribute value to an XML-text-safe value, meaning that
+     * less than and ampersand characters are escaped.
+     *
+     * @param textValue the text value to be escaped
+     * @return the escaped value
+     */
+    @NonNull
+    public static String toXmlTextValue(@NonNull String textValue) {
+        for (int i = 0, n = textValue.length(); i < n; i++) {
+            char c = textValue.charAt(i);
+            if (c == '<' || c == '&') {
+                StringBuilder sb = new StringBuilder(2 * textValue.length());
+                appendXmlTextValue(sb, textValue);
+                return sb.toString();
+            }
+        }
+
+        return textValue;
+    }
+
+    /**
+     * Appends text to the given {@link StringBuilder} and escapes it as required for a
+     * DOM attribute node.
+     *
+     * @param sb the string builder
+     * @param attrValue the attribute value to be appended and escaped
+     */
+    public static void appendXmlAttributeValue(@NonNull StringBuilder sb,
+            @NonNull String attrValue) {
+        int n = attrValue.length();
+        // &, ", ' and < are illegal in attributes; see http://www.w3.org/TR/REC-xml/#NT-AttValue
+        // (' legal in a " string and " is legal in a ' string but here we'll stay on the safe
+        // side)
+        for (int i = 0; i < n; i++) {
+            char c = attrValue.charAt(i);
+            if (c == '"') {
+                sb.append(QUOT_ENTITY);
+            } else if (c == '<') {
+                sb.append(LT_ENTITY);
+            } else if (c == '\'') {
+                sb.append(APOS_ENTITY);
+            } else if (c == '&') {
+                sb.append(AMP_ENTITY);
+            } else {
+                sb.append(c);
+            }
+        }
+    }
+
+    /**
+     * Appends text to the given {@link StringBuilder} and escapes it as required for a
+     * DOM text node.
+     *
+     * @param sb the string builder
+     * @param textValue the text value to be appended and escaped
+     */
+    public static void appendXmlTextValue(@NonNull StringBuilder sb, @NonNull String textValue) {
+        for (int i = 0, n = textValue.length(); i < n; i++) {
+            char c = textValue.charAt(i);
+            if (c == '<') {
+                sb.append(LT_ENTITY);
+            } else if (c == '&') {
+                sb.append(AMP_ENTITY);
+            } else {
+                sb.append(c);
+            }
+        }
+    }
+
+    /**
+     * Returns true if the given node has one or more element children
+     *
+     * @param node the node to test for element children
+     * @return true if the node has one or more element children
+     */
+    public static boolean hasElementChildren(@NonNull Node node) {
+        NodeList children = node.getChildNodes();
+        for (int i = 0, n = children.getLength(); i < n; i++) {
+            if (children.item(i).getNodeType() == Node.ELEMENT_NODE) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Parses the given XML string as a DOM document, using the JDK parser. The parser does not
+     * validate, and is namespace aware.
+     *
+     * @param xml            the XML content to be parsed (must be well formed)
+     * @param namespaceAware whether the parser is namespace aware
+     * @return the DOM document, or null
+     */
+    @Nullable
+    public static Document parseDocumentSilently(@NonNull String xml, boolean namespaceAware) {
+        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        InputSource is = new InputSource(new StringReader(xml));
+        factory.setNamespaceAware(namespaceAware);
+        factory.setValidating(false);
+        try {
+            DocumentBuilder builder = factory.newDocumentBuilder();
+            return builder.parse(is);
+        } catch (Exception e) {
+            // pass
+            // This method is deliberately silent; will return null
+        }
+
+        return null;
+    }
+
+    /**
+     * Dump an XML tree to string. This does not perform any pretty printing.
+     * To perform pretty printing, use {@code XmlPrettyPrinter.prettyPrint(node)} in
+     * {@code sdk-common}.
+     */
+    public static String toXml(Node node, boolean preserveWhitespace) {
+        StringBuilder sb = new StringBuilder(1000);
+        append(sb, node, 0);
+        return sb.toString();
+    }
+
+    /** Dump node to string without indentation adjustments */
+    private static void append(
+            @NonNull StringBuilder sb,
+            @NonNull Node node,
+            int indent) {
+        short nodeType = node.getNodeType();
+        switch (nodeType) {
+            case Node.DOCUMENT_NODE:
+            case Node.DOCUMENT_FRAGMENT_NODE: {
+                sb.append(XML_PROLOG);
+                NodeList children = node.getChildNodes();
+                for (int i = 0, n = children.getLength(); i < n; i++) {
+                    append(sb, children.item(i), indent);
+                }
+                break;
+            }
+            case Node.COMMENT_NODE:
+            case Node.TEXT_NODE: {
+                if (nodeType == Node.COMMENT_NODE) {
+                    sb.append(XML_COMMENT_BEGIN);
+                }
+                String text = node.getNodeValue();
+                sb.append(toXmlTextValue(text));
+                if (nodeType == Node.COMMENT_NODE) {
+                    sb.append(XML_COMMENT_END);
+                }
+                break;
+            }
+            case Node.ELEMENT_NODE: {
+                sb.append('<');
+                Element element = (Element) node;
+                sb.append(element.getTagName());
+
+                NamedNodeMap attributes = element.getAttributes();
+                NodeList children = element.getChildNodes();
+                int childCount = children.getLength();
+                int attributeCount = attributes.getLength();
+
+                if (attributeCount > 0) {
+                    for (int i = 0; i < attributeCount; i++) {
+                        Node attribute = attributes.item(i);
+                        sb.append(' ');
+                        sb.append(attribute.getNodeName());
+                        sb.append('=').append('"');
+                        sb.append(toXmlAttributeValue(attribute.getNodeValue()));
+                        sb.append('"');
+                    }
+                }
+
+                if (childCount == 0) {
+                    sb.append('/');
+                }
+                sb.append('>');
+                if (childCount > 0) {
+                    for (int i = 0; i < childCount; i++) {
+                        Node child = children.item(i);
+                        append(sb, child, indent + 1);
+                    }
+                    sb.append('<').append('/');
+                    sb.append(element.getTagName());
+                    sb.append('>');
+                }
+                break;
+            }
+
+            default:
+                throw new UnsupportedOperationException(
+                        "Unsupported node type " + nodeType + ": not yet implemented");
+        }
+    }
+
+    /**
+     * Format the given floating value into an XML string, omitting decimals if
+     * 0
+     *
+     * @param value the value to be formatted
+     * @return the corresponding XML string for the value
+     */
+    public static String formatFloatAttribute(double value) {
+        if (value != (int) value) {
+            // Run String.format without a locale, because we don't want locale-specific
+            // conversions here like separating the decimal part with a comma instead of a dot!
+            return String.format((Locale) null, "%.2f", value); //$NON-NLS-1$
+        } else {
+            return Integer.toString((int) value);
+        }
+    }
+}
diff --git a/common/src/main/java/com/android/xml/AndroidManifest.java b/common/src/main/java/com/android/xml/AndroidManifest.java
new file mode 100644
index 0000000..e2532c7
--- /dev/null
+++ b/common/src/main/java/com/android/xml/AndroidManifest.java
@@ -0,0 +1,371 @@
+/*
+ * Copyright (C) 2009 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.xml;
+
+import com.android.SdkConstants;
+import com.android.io.IAbstractFile;
+import com.android.io.IAbstractFolder;
+import com.android.io.StreamException;
+
+import org.w3c.dom.Node;
+import org.xml.sax.InputSource;
+
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpressionException;
+
+/**
+ * Helper and Constants for the AndroidManifest.xml file.
+ *
+ */
+public final class AndroidManifest {
+
+    public static final String NODE_MANIFEST = "manifest";
+    public static final String NODE_APPLICATION = "application";
+    public static final String NODE_ACTIVITY = "activity";
+    public static final String NODE_ACTIVITY_ALIAS = "activity-alias";
+    public static final String NODE_SERVICE = "service";
+    public static final String NODE_RECEIVER = "receiver";
+    public static final String NODE_PROVIDER = "provider";
+    public static final String NODE_INTENT = "intent-filter";
+    public static final String NODE_ACTION = "action";
+    public static final String NODE_CATEGORY = "category";
+    public static final String NODE_USES_SDK = "uses-sdk";
+    public static final String NODE_PERMISSION = "permission";
+    public static final String NODE_PERMISSION_TREE = "permission-tree";
+    public static final String NODE_PERMISSION_GROUP = "permission-group";
+    public static final String NODE_USES_PERMISSION = "uses-permission";
+    public static final String NODE_INSTRUMENTATION = "instrumentation";
+    public static final String NODE_USES_LIBRARY = "uses-library";
+    public static final String NODE_SUPPORTS_SCREENS = "supports-screens";
+    public static final String NODE_COMPATIBLE_SCREENS = "compatible-screens";
+    public static final String NODE_USES_CONFIGURATION = "uses-configuration";
+    public static final String NODE_USES_FEATURE = "uses-feature";
+    public static final String NODE_METADATA = "meta-data";
+    public static final String NODE_DATA = "data";
+    public static final String NODE_GRANT_URI_PERMISSION = "grant-uri-permission";
+    public static final String NODE_PATH_PERMISSION = "path-permission";
+    public static final String NODE_SUPPORTS_GL_TEXTURE = "supports-gl-texture";
+
+    public static final String ATTRIBUTE_PACKAGE = "package";
+    public static final String ATTRIBUTE_VERSIONCODE = "versionCode";
+    public static final String ATTRIBUTE_NAME = "name";
+    public static final String ATTRIBUTE_REQUIRED = "required";
+    public static final String ATTRIBUTE_GLESVERSION = "glEsVersion";
+    public static final String ATTRIBUTE_PROCESS = "process";
+    public static final String ATTRIBUTE_DEBUGGABLE = "debuggable";
+    public static final String ATTRIBUTE_LABEL = "label";
+    public static final String ATTRIBUTE_ICON = "icon";
+    public static final String ATTRIBUTE_MIN_SDK_VERSION = "minSdkVersion";
+    public static final String ATTRIBUTE_TARGET_SDK_VERSION = "targetSdkVersion";
+    public static final String ATTRIBUTE_TARGET_PACKAGE = "targetPackage";
+    public static final String ATTRIBUTE_TARGET_ACTIVITY = "targetActivity";
+    public static final String ATTRIBUTE_MANAGE_SPACE_ACTIVITY = "manageSpaceActivity";
+    public static final String ATTRIBUTE_EXPORTED = "exported";
+    public static final String ATTRIBUTE_RESIZEABLE = "resizeable";
+    public static final String ATTRIBUTE_ANYDENSITY = "anyDensity";
+    public static final String ATTRIBUTE_SMALLSCREENS = "smallScreens";
+    public static final String ATTRIBUTE_NORMALSCREENS = "normalScreens";
+    public static final String ATTRIBUTE_LARGESCREENS = "largeScreens";
+    public static final String ATTRIBUTE_REQ_5WAYNAV = "reqFiveWayNav";
+    public static final String ATTRIBUTE_REQ_NAVIGATION = "reqNavigation";
+    public static final String ATTRIBUTE_REQ_HARDKEYBOARD = "reqHardKeyboard";
+    public static final String ATTRIBUTE_REQ_KEYBOARDTYPE = "reqKeyboardType";
+    public static final String ATTRIBUTE_REQ_TOUCHSCREEN = "reqTouchScreen";
+    public static final String ATTRIBUTE_THEME = "theme";
+    public static final String ATTRIBUTE_BACKUP_AGENT = "backupAgent";
+    public static final String ATTRIBUTE_PARENT_ACTIVITY_NAME = "parentActivityName";
+
+    /**
+     * Returns an {@link IAbstractFile} object representing the manifest for the given project.
+     *
+     * @param projectFolder The project containing the manifest file.
+     * @return An IAbstractFile object pointing to the manifest or null if the manifest
+     *         is missing.
+     */
+    public static IAbstractFile getManifest(IAbstractFolder projectFolder) {
+        IAbstractFile file = projectFolder.getFile(SdkConstants.FN_ANDROID_MANIFEST_XML);
+        if (file != null && file.exists()) {
+            return file;
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns the package for a given project.
+     * @param projectFolder the folder of the project.
+     * @return the package info or null (or empty) if not found.
+     * @throws XPathExpressionException
+     * @throws StreamException If any error happens when reading the manifest.
+     */
+    public static String getPackage(IAbstractFolder projectFolder)
+            throws XPathExpressionException, StreamException {
+        IAbstractFile file = getManifest(projectFolder);
+        if (file != null) {
+            return getPackage(file);
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns the package for a given manifest.
+     * @param manifestFile the manifest to parse.
+     * @return the package info or null (or empty) if not found.
+     * @throws XPathExpressionException
+     * @throws StreamException If any error happens when reading the manifest.
+     */
+    public static String getPackage(IAbstractFile manifestFile)
+            throws XPathExpressionException, StreamException {
+        XPath xPath = AndroidXPathFactory.newXPath();
+
+        return xPath.evaluate(
+                "/"  + NODE_MANIFEST +
+                "/@" + ATTRIBUTE_PACKAGE,
+                new InputSource(manifestFile.getContents()));
+    }
+
+    /**
+     * Returns whether the manifest is set to make the application debuggable.
+     *
+     * If the give manifest does not contain the debuggable attribute then the application
+     * is considered to not be debuggable.
+     *
+     * @param manifestFile the manifest to parse.
+     * @return true if the application is debuggable.
+     * @throws XPathExpressionException
+     * @throws StreamException If any error happens when reading the manifest.
+     */
+    public static boolean getDebuggable(IAbstractFile manifestFile)
+            throws XPathExpressionException, StreamException {
+        XPath xPath = AndroidXPathFactory.newXPath();
+
+        String value = xPath.evaluate(
+                "/"  + NODE_MANIFEST +
+                "/"  + NODE_APPLICATION +
+                "/@" + AndroidXPathFactory.DEFAULT_NS_PREFIX +
+                ":"  + ATTRIBUTE_DEBUGGABLE,
+                new InputSource(manifestFile.getContents()));
+
+        // default is not debuggable, which is the same behavior as parseBoolean
+        return Boolean.parseBoolean(value);
+    }
+
+    /**
+     * Returns the value of the versionCode attribute or -1 if the value is not set.
+     * @param manifestFile the manifest file to read the attribute from.
+     * @return the integer value or -1 if not set.
+     * @throws XPathExpressionException
+     * @throws StreamException If any error happens when reading the manifest.
+     */
+    public static int getVersionCode(IAbstractFile manifestFile)
+            throws XPathExpressionException, StreamException {
+        XPath xPath = AndroidXPathFactory.newXPath();
+
+        String result = xPath.evaluate(
+                "/"  + NODE_MANIFEST +
+                "/@" + AndroidXPathFactory.DEFAULT_NS_PREFIX +
+                ":"  + ATTRIBUTE_VERSIONCODE,
+                new InputSource(manifestFile.getContents()));
+
+        try {
+            return Integer.parseInt(result);
+        } catch (NumberFormatException e) {
+            return -1;
+        }
+    }
+
+    /**
+     * Returns whether the version Code attribute is set in a given manifest.
+     * @param manifestFile the manifest to check
+     * @return true if the versionCode attribute is present and its value is not empty.
+     * @throws XPathExpressionException
+     * @throws StreamException If any error happens when reading the manifest.
+     */
+    public static boolean hasVersionCode(IAbstractFile manifestFile)
+            throws XPathExpressionException, StreamException {
+        XPath xPath = AndroidXPathFactory.newXPath();
+
+        Object result = xPath.evaluate(
+                "/"  + NODE_MANIFEST +
+                "/@" + AndroidXPathFactory.DEFAULT_NS_PREFIX +
+                ":"  + ATTRIBUTE_VERSIONCODE,
+                new InputSource(manifestFile.getContents()),
+                XPathConstants.NODE);
+
+        if (result != null) {
+            Node node  = (Node)result;
+            if (node.getNodeValue().length() > 0) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns the value of the minSdkVersion attribute.
+     * <p/>
+     * If the attribute is set with an int value, the method returns an Integer object.
+     * <p/>
+     * If the attribute is set with a codename, it returns the codename as a String object.
+     * <p/>
+     * If the attribute is not set, it returns null.
+     *
+     * @param manifestFile the manifest file to read the attribute from.
+     * @return the attribute value.
+     * @throws XPathExpressionException
+     * @throws StreamException If any error happens when reading the manifest.
+     */
+    public static Object getMinSdkVersion(IAbstractFile manifestFile)
+            throws XPathExpressionException, StreamException {
+        XPath xPath = AndroidXPathFactory.newXPath();
+
+        String result = xPath.evaluate(
+                "/"  + NODE_MANIFEST +
+                "/"  + NODE_USES_SDK +
+                "/@" + AndroidXPathFactory.DEFAULT_NS_PREFIX +
+                ":"  + ATTRIBUTE_MIN_SDK_VERSION,
+                new InputSource(manifestFile.getContents()));
+
+        try {
+            return Integer.valueOf(result);
+        } catch (NumberFormatException e) {
+            return result.length() > 0 ? result : null;
+        }
+    }
+
+    /**
+     * Returns the value of the targetSdkVersion attribute (defaults to 1 if the attribute is
+     * not set), or -1 if the value is a codename.
+     * @param manifestFile the manifest file to read the attribute from.
+     * @return the integer value or -1 if not set.
+     * @throws XPathExpressionException
+     * @throws StreamException If any error happens when reading the manifest.
+     */
+    public static Integer getTargetSdkVersion(IAbstractFile manifestFile)
+            throws XPathExpressionException, StreamException {
+        XPath xPath = AndroidXPathFactory.newXPath();
+
+        String result = xPath.evaluate(
+                "/"  + NODE_MANIFEST +
+                "/"  + NODE_USES_SDK +
+                "/@" + AndroidXPathFactory.DEFAULT_NS_PREFIX +
+                ":"  + ATTRIBUTE_TARGET_SDK_VERSION,
+                new InputSource(manifestFile.getContents()));
+
+        try {
+            return Integer.valueOf(result);
+        } catch (NumberFormatException e) {
+            return result.length() > 0 ? -1 : null;
+        }
+    }
+
+    /**
+     * Returns the application icon  for a given manifest.
+     * @param manifestFile the manifest to parse.
+     * @return the icon or null (or empty) if not found.
+     * @throws XPathExpressionException
+     * @throws StreamException If any error happens when reading the manifest.
+     */
+    public static String getApplicationIcon(IAbstractFile manifestFile)
+            throws XPathExpressionException, StreamException {
+        XPath xPath = AndroidXPathFactory.newXPath();
+
+        return xPath.evaluate(
+                "/"  + NODE_MANIFEST +
+                "/"  + NODE_APPLICATION +
+                "/@" + AndroidXPathFactory.DEFAULT_NS_PREFIX +
+                ":"  + ATTRIBUTE_ICON,
+                new InputSource(manifestFile.getContents()));
+    }
+
+    /**
+     * Returns the application label  for a given manifest.
+     * @param manifestFile the manifest to parse.
+     * @return the label or null (or empty) if not found.
+     * @throws XPathExpressionException
+     * @throws StreamException If any error happens when reading the manifest.
+     */
+    public static String getApplicationLabel(IAbstractFile manifestFile)
+            throws XPathExpressionException, StreamException {
+        XPath xPath = AndroidXPathFactory.newXPath();
+
+        return xPath.evaluate(
+                "/"  + NODE_MANIFEST +
+                "/"  + NODE_APPLICATION +
+                "/@" + AndroidXPathFactory.DEFAULT_NS_PREFIX +
+                ":"  + ATTRIBUTE_LABEL,
+                new InputSource(manifestFile.getContents()));
+    }
+
+    /**
+     * Combines a java package, with a class value from the manifest to make a fully qualified
+     * class name
+     * @param javaPackage the java package from the manifest.
+     * @param className the class name from the manifest.
+     * @return the fully qualified class name.
+     */
+    public static String combinePackageAndClassName(String javaPackage, String className) {
+        if (className == null || className.length() == 0) {
+            return javaPackage;
+        }
+        if (javaPackage == null || javaPackage.length() == 0) {
+            return className;
+        }
+
+        // the class name can be a subpackage (starts with a '.'
+        // char), a simple class name (no dot), or a full java package
+        boolean startWithDot = (className.charAt(0) == '.');
+        boolean hasDot = (className.indexOf('.') != -1);
+        if (startWithDot || hasDot == false) {
+
+            // add the concatenation of the package and class name
+            if (startWithDot) {
+                return javaPackage + className;
+            } else {
+                return javaPackage + '.' + className;
+            }
+        } else {
+            // just add the class as it should be a fully qualified java name.
+            return className;
+        }
+    }
+
+    /**
+     * Given a fully qualified activity name (e.g. com.foo.test.MyClass) and given a project
+     * package base name (e.g. com.foo), returns the relative activity name that would be used
+     * the "name" attribute of an "activity" element.
+     *
+     * @param fullActivityName a fully qualified activity class name, e.g. "com.foo.test.MyClass"
+     * @param packageName The project base package name, e.g. "com.foo"
+     * @return The relative activity name if it can be computed or the original fullActivityName.
+     */
+    public static String extractActivityName(String fullActivityName, String packageName) {
+        if (packageName != null && fullActivityName != null) {
+            if (packageName.length() > 0 && fullActivityName.startsWith(packageName)) {
+                String name = fullActivityName.substring(packageName.length());
+                if (name.length() > 0 && name.charAt(0) == '.') {
+                    return name;
+                }
+            }
+        }
+
+        return fullActivityName;
+    }
+}
diff --git a/common/src/main/java/com/android/xml/AndroidXPathFactory.java b/common/src/main/java/com/android/xml/AndroidXPathFactory.java
new file mode 100644
index 0000000..87788be
--- /dev/null
+++ b/common/src/main/java/com/android/xml/AndroidXPathFactory.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2009 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.xml;
+
+import com.android.SdkConstants;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.xml.XMLConstants;
+import javax.xml.namespace.NamespaceContext;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathFactory;
+
+/**
+ * XPath factory with automatic support for the android name space.
+ */
+public class AndroidXPathFactory {
+    /** Default prefix for android name space: 'android' */
+    public static final String DEFAULT_NS_PREFIX = "android"; //$NON-NLS-1$
+
+    private static final XPathFactory sFactory = XPathFactory.newInstance();
+
+    /** Name space context for Android resource XML files. */
+    private static class AndroidNamespaceContext implements NamespaceContext {
+        private static final AndroidNamespaceContext sThis = new AndroidNamespaceContext(
+                DEFAULT_NS_PREFIX);
+
+        private final String mAndroidPrefix;
+        private final List<String> mAndroidPrefixes;
+
+        /**
+         * Returns the default {@link AndroidNamespaceContext}.
+         */
+        private static AndroidNamespaceContext getDefault() {
+            return sThis;
+        }
+
+        /**
+         * Construct the context with the prefix associated with the android namespace.
+         * @param androidPrefix the Prefix
+         */
+        public AndroidNamespaceContext(String androidPrefix) {
+            mAndroidPrefix = androidPrefix;
+            mAndroidPrefixes = Collections.singletonList(mAndroidPrefix);
+        }
+
+        @Override
+        public String getNamespaceURI(String prefix) {
+            if (prefix != null) {
+                if (prefix.equals(mAndroidPrefix)) {
+                    return SdkConstants.NS_RESOURCES;
+                }
+            }
+
+            return XMLConstants.NULL_NS_URI;
+        }
+
+        @Override
+        public String getPrefix(String namespaceURI) {
+            if (SdkConstants.NS_RESOURCES.equals(namespaceURI)) {
+                return mAndroidPrefix;
+            }
+
+            return null;
+        }
+
+        @Override
+        public Iterator<?> getPrefixes(String namespaceURI) {
+            if (SdkConstants.NS_RESOURCES.equals(namespaceURI)) {
+                return mAndroidPrefixes.iterator();
+            }
+
+            return null;
+        }
+    }
+
+    /**
+     * Creates a new XPath object, specifying which prefix in the query is used for the
+     * android namespace.
+     * @param androidPrefix The namespace prefix.
+     */
+    public static XPath newXPath(String androidPrefix) {
+        XPath xpath = sFactory.newXPath();
+        xpath.setNamespaceContext(new AndroidNamespaceContext(androidPrefix));
+        return xpath;
+    }
+
+    /**
+     * Creates a new XPath object using the default prefix for the android namespace.
+     * @see #DEFAULT_NS_PREFIX
+     */
+    public static XPath newXPath() {
+        XPath xpath = sFactory.newXPath();
+        xpath.setNamespaceContext(AndroidNamespaceContext.getDefault());
+        return xpath;
+    }
+}
diff --git a/ddmlib/.classpath b/ddmlib/.classpath
new file mode 100644
index 0000000..4329a33
--- /dev/null
+++ b/ddmlib/.classpath
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry excluding="Android.mk" kind="src" path="src/main/java"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+	<classpathentry combineaccessrules="false" exported="true" kind="src" path="/common"/>
+	<classpathentry exported="true" kind="var" path="ANDROID_SRC/prebuilts/tools/common/m2/repository/net/sf/kxml/kxml2/2.3.0/kxml2-2.3.0.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/m2/repository/net/sf/kxml/kxml2/2.3.0/kxml2-2.3.0-sources.jar"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/ddmlib/.gitignore b/ddmlib/.gitignore
new file mode 100644
index 0000000..81631c6
--- /dev/null
+++ b/ddmlib/.gitignore
@@ -0,0 +1,2 @@
+/bin
+/build
diff --git a/ddmlib/.project b/ddmlib/.project
new file mode 100644
index 0000000..fea25c7
--- /dev/null
+++ b/ddmlib/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>ddmlib</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>
diff --git a/ddmlib/.settings/org.eclipse.jdt.core.prefs b/ddmlib/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..9dbff07
--- /dev/null
+++ b/ddmlib/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,98 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
+org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault
+org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled
+org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
+org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
+org.eclipse.jdt.core.compiler.problem.deadCode=warning
+org.eclipse.jdt.core.compiler.problem.deprecation=warning
+org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
+org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
+org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
+org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning
+org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled
+org.eclipse.jdt.core.compiler.problem.fieldHiding=warning
+org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
+org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=error
+org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
+org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled
+org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
+org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning
+org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning
+org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
+org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
+org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
+org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
+org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
+org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
+org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
+org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
+org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
+org.eclipse.jdt.core.compiler.problem.nullReference=error
+org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning
+org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore
+org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
+org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning
+org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
+org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
+org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
+org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled
+org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
+org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
+org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
+org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled
+org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning
+org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error
+org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning
+org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.unusedImport=warning
+org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
+org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
+org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning
+org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled
+org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
+org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
+org.eclipse.jdt.core.compiler.source=1.6
diff --git a/ddmlib/NOTICE b/ddmlib/NOTICE
new file mode 100644
index 0000000..c5b1efa
--- /dev/null
+++ b/ddmlib/NOTICE
@@ -0,0 +1,190 @@
+
+   Copyright (c) 2005-2008, 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.
+
+   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.
+
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
diff --git a/ddmlib/ddmlib.iml b/ddmlib/ddmlib.iml
new file mode 100644
index 0000000..3860023
--- /dev/null
+++ b/ddmlib/ddmlib.iml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4">
+  <component name="NewModuleRootManager" inherit-compiler-output="true">
+    <exclude-output />
+    <content url="file://$MODULE_DIR$">
+      <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
+      <excludeFolder url="file://$MODULE_DIR$/.settings" />
+      <excludeFolder url="file://$MODULE_DIR$/build" />
+    </content>
+    <orderEntry type="sourceFolder" forTests="false" />
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="module" module-name="common" exported="" />
+    <orderEntry type="library" exported="" name="kxml2" level="project" />
+    <orderEntry type="library" scope="TEST" name="easymock-tools" level="project" />
+    <orderEntry type="library" scope="TEST" name="JUnit3" level="project" />
+  </component>
+</module>
+
diff --git a/ddmlib/src/main/java/com/android/ddmlib/AdbCommandRejectedException.java b/ddmlib/src/main/java/com/android/ddmlib/AdbCommandRejectedException.java
new file mode 100644
index 0000000..ae7d014
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/AdbCommandRejectedException.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2010 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.ddmlib;
+
+
+/**
+ * Exception thrown when adb refuses a command.
+ */
+public class AdbCommandRejectedException extends Exception {
+    private static final long serialVersionUID = 1L;
+    private final boolean mIsDeviceOffline;
+    private final boolean mErrorDuringDeviceSelection;
+
+    AdbCommandRejectedException(String message) {
+        super(message);
+        mIsDeviceOffline = "device offline".equals(message);
+        mErrorDuringDeviceSelection = false;
+    }
+
+    AdbCommandRejectedException(String message, boolean errorDuringDeviceSelection) {
+        super(message);
+        mErrorDuringDeviceSelection = errorDuringDeviceSelection;
+        mIsDeviceOffline = "device offline".equals(message);
+    }
+
+    /**
+     * Returns true if the error is due to the device being offline.
+     */
+    public boolean isDeviceOffline() {
+        return mIsDeviceOffline;
+    }
+
+    /**
+     * Returns whether adb refused to target a given device for the command.
+     * <p/>If false, adb refused the command itself, if true, it refused to target the given
+     * device.
+     */
+    public boolean wasErrorDuringDeviceSelection() {
+        return mErrorDuringDeviceSelection;
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/AdbHelper.java b/ddmlib/src/main/java/com/android/ddmlib/AdbHelper.java
new file mode 100644
index 0000000..8bc42ca
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/AdbHelper.java
@@ -0,0 +1,791 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import com.android.ddmlib.log.LogReceiver;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.channels.SocketChannel;
+
+/**
+ * Helper class to handle requests and connections to adb.
+ * <p/>{@link DebugBridgeServer} is the public API to connection to adb, while {@link AdbHelper}
+ * does the low level stuff.
+ * <p/>This currently uses spin-wait non-blocking I/O. A Selector would be more efficient,
+ * but seems like overkill for what we're doing here.
+ */
+final class AdbHelper {
+
+    // public static final long kOkay = 0x59414b4fL;
+    // public static final long kFail = 0x4c494146L;
+
+    static final int WAIT_TIME = 5; // spin-wait sleep, in ms
+
+    static final String DEFAULT_ENCODING = "ISO-8859-1"; //$NON-NLS-1$
+
+    /** do not instantiate */
+    private AdbHelper() {
+    }
+
+    /**
+     * Response from ADB.
+     */
+    static class AdbResponse {
+        public AdbResponse() {
+            message = "";
+        }
+
+        public boolean okay; // first 4 bytes in response were "OKAY"?
+
+        public String message; // diagnostic string if #okay is false
+    }
+
+    /**
+     * Create and connect a new pass-through socket, from the host to a port on
+     * the device.
+     *
+     * @param adbSockAddr
+     * @param device the device to connect to. Can be null in which case the connection will be
+     * to the first available device.
+     * @param devicePort the port we're opening
+     * @throws TimeoutException in case of timeout on the connection.
+     * @throws IOException in case of I/O error on the connection.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     */
+    public static SocketChannel open(InetSocketAddress adbSockAddr,
+            Device device, int devicePort)
+            throws IOException, TimeoutException, AdbCommandRejectedException {
+
+        SocketChannel adbChan = SocketChannel.open(adbSockAddr);
+        try {
+            adbChan.socket().setTcpNoDelay(true);
+            adbChan.configureBlocking(false);
+
+            // if the device is not -1, then we first tell adb we're looking to
+            // talk to a specific device
+            setDevice(adbChan, device);
+
+            byte[] req = createAdbForwardRequest(null, devicePort);
+            // Log.hexDump(req);
+
+            write(adbChan, req);
+
+            AdbResponse resp = readAdbResponse(adbChan, false);
+            if (!resp.okay) {
+                throw new AdbCommandRejectedException(resp.message);
+            }
+
+            adbChan.configureBlocking(true);
+        } catch (TimeoutException e) {
+            adbChan.close();
+            throw e;
+        } catch (IOException e) {
+            adbChan.close();
+            throw e;
+        }
+
+        return adbChan;
+    }
+
+    /**
+     * Creates and connects a new pass-through socket, from the host to a port on
+     * the device.
+     *
+     * @param adbSockAddr
+     * @param device the device to connect to. Can be null in which case the connection will be
+     * to the first available device.
+     * @param pid the process pid to connect to.
+     * @throws TimeoutException in case of timeout on the connection.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     * @throws IOException in case of I/O error on the connection.
+     */
+    public static SocketChannel createPassThroughConnection(InetSocketAddress adbSockAddr,
+            Device device, int pid)
+            throws TimeoutException, AdbCommandRejectedException, IOException {
+
+        SocketChannel adbChan = SocketChannel.open(adbSockAddr);
+        try {
+            adbChan.socket().setTcpNoDelay(true);
+            adbChan.configureBlocking(false);
+
+            // if the device is not -1, then we first tell adb we're looking to
+            // talk to a specific device
+            setDevice(adbChan, device);
+
+            byte[] req = createJdwpForwardRequest(pid);
+            // Log.hexDump(req);
+
+            write(adbChan, req);
+
+            AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
+            if (!resp.okay) {
+                throw new AdbCommandRejectedException(resp.message);
+            }
+
+            adbChan.configureBlocking(true);
+        } catch (TimeoutException e) {
+            adbChan.close();
+            throw e;
+        } catch (IOException e) {
+            adbChan.close();
+            throw e;
+        }
+
+        return adbChan;
+    }
+
+    /**
+     * Creates a port forwarding request for adb. This returns an array
+     * containing "####tcp:{port}:{addStr}".
+     * @param addrStr the host. Can be null.
+     * @param port the port on the device. This does not need to be numeric.
+     */
+    private static byte[] createAdbForwardRequest(String addrStr, int port) {
+        String reqStr;
+
+        if (addrStr == null)
+            reqStr = "tcp:" + port;
+        else
+            reqStr = "tcp:" + port + ":" + addrStr;
+        return formAdbRequest(reqStr);
+    }
+
+    /**
+     * Creates a port forwarding request to a jdwp process. This returns an array
+     * containing "####jwdp:{pid}".
+     * @param pid the jdwp process pid on the device.
+     */
+    private static byte[] createJdwpForwardRequest(int pid) {
+        String reqStr = String.format("jdwp:%1$d", pid); //$NON-NLS-1$
+        return formAdbRequest(reqStr);
+    }
+
+    /**
+     * Create an ASCII string preceded by four hex digits. The opening "####"
+     * is the length of the rest of the string, encoded as ASCII hex (case
+     * doesn't matter). "port" and "host" are what we want to forward to. If
+     * we're on the host side connecting into the device, "addrStr" should be
+     * null.
+     */
+    static byte[] formAdbRequest(String req) {
+        String resultStr = String.format("%04X%s", req.length(), req); //$NON-NLS-1$
+        byte[] result;
+        try {
+            result = resultStr.getBytes(DEFAULT_ENCODING);
+        } catch (UnsupportedEncodingException uee) {
+            uee.printStackTrace(); // not expected
+            return null;
+        }
+        assert result.length == req.length() + 4;
+        return result;
+    }
+
+    /**
+     * Reads the response from ADB after a command.
+     * @param chan The socket channel that is connected to adb.
+     * @param readDiagString If true, we're expecting an OKAY response to be
+     *      followed by a diagnostic string. Otherwise, we only expect the
+     *      diagnostic string to follow a FAIL.
+     * @throws TimeoutException in case of timeout on the connection.
+     * @throws IOException in case of I/O error on the connection.
+     */
+    static AdbResponse readAdbResponse(SocketChannel chan, boolean readDiagString)
+            throws TimeoutException, IOException {
+
+        AdbResponse resp = new AdbResponse();
+
+        byte[] reply = new byte[4];
+        read(chan, reply);
+
+        if (isOkay(reply)) {
+            resp.okay = true;
+        } else {
+            readDiagString = true; // look for a reason after the FAIL
+            resp.okay = false;
+        }
+
+        // not a loop -- use "while" so we can use "break"
+        try {
+            while (readDiagString) {
+                // length string is in next 4 bytes
+                byte[] lenBuf = new byte[4];
+                read(chan, lenBuf);
+
+                String lenStr = replyToString(lenBuf);
+
+                int len;
+                try {
+                    len = Integer.parseInt(lenStr, 16);
+                } catch (NumberFormatException nfe) {
+                    Log.w("ddms", "Expected digits, got '" + lenStr + "': "
+                            + lenBuf[0] + " " + lenBuf[1] + " " + lenBuf[2] + " "
+                            + lenBuf[3]);
+                    Log.w("ddms", "reply was " + replyToString(reply));
+                    break;
+                }
+
+                byte[] msg = new byte[len];
+                read(chan, msg);
+
+                resp.message = replyToString(msg);
+                Log.v("ddms", "Got reply '" + replyToString(reply) + "', diag='"
+                        + resp.message + "'");
+
+                break;
+            }
+        } catch (Exception e) {
+            // ignore those, since it's just reading the diagnose string, the response will
+            // contain okay==false anyway.
+        }
+
+        return resp;
+    }
+
+    /**
+     * Retrieve the frame buffer from the device.
+     * @throws TimeoutException in case of timeout on the connection.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     * @throws IOException in case of I/O error on the connection.
+     */
+    static RawImage getFrameBuffer(InetSocketAddress adbSockAddr, Device device)
+            throws TimeoutException, AdbCommandRejectedException, IOException {
+
+        RawImage imageParams = new RawImage();
+        byte[] request = formAdbRequest("framebuffer:"); //$NON-NLS-1$
+        byte[] nudge = {
+            0
+        };
+        byte[] reply;
+
+        SocketChannel adbChan = null;
+        try {
+            adbChan = SocketChannel.open(adbSockAddr);
+            adbChan.configureBlocking(false);
+
+            // if the device is not -1, then we first tell adb we're looking to talk
+            // to a specific device
+            setDevice(adbChan, device);
+
+            write(adbChan, request);
+
+            AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
+            if (!resp.okay) {
+                throw new AdbCommandRejectedException(resp.message);
+            }
+
+            // first the protocol version.
+            reply = new byte[4];
+            read(adbChan, reply);
+
+            ByteBuffer buf = ByteBuffer.wrap(reply);
+            buf.order(ByteOrder.LITTLE_ENDIAN);
+
+            int version = buf.getInt();
+
+            // get the header size (this is a count of int)
+            int headerSize = RawImage.getHeaderSize(version);
+
+            // read the header
+            reply = new byte[headerSize * 4];
+            read(adbChan, reply);
+
+            buf = ByteBuffer.wrap(reply);
+            buf.order(ByteOrder.LITTLE_ENDIAN);
+
+            // fill the RawImage with the header
+            if (!imageParams.readHeader(version, buf)) {
+                Log.e("Screenshot", "Unsupported protocol: " + version);
+                return null;
+            }
+
+            Log.d("ddms", "image params: bpp=" + imageParams.bpp + ", size="
+                    + imageParams.size + ", width=" + imageParams.width
+                    + ", height=" + imageParams.height);
+
+            write(adbChan, nudge);
+
+            reply = new byte[imageParams.size];
+            read(adbChan, reply);
+
+            imageParams.data = reply;
+        } finally {
+            if (adbChan != null) {
+                adbChan.close();
+            }
+        }
+
+        return imageParams;
+    }
+
+    /**
+     * Executes a shell command on the device and retrieve the output. The output is
+     * handed to <var>rcvr</var> as it arrives.
+     *
+     * @param adbSockAddr the {@link InetSocketAddress} to adb.
+     * @param command the shell command to execute
+     * @param device the {@link IDevice} on which to execute the command.
+     * @param rcvr the {@link IShellOutputReceiver} that will receives the output of the shell
+     *            command
+     * @param maxTimeToOutputResponse max time between command output. If more time passes
+     *            between command output, the method will throw
+     *            {@link ShellCommandUnresponsiveException}. A value of 0 means the method will
+     *            wait forever for command output and never throw.
+     * @throws TimeoutException in case of timeout on the connection when sending the command.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     * @throws ShellCommandUnresponsiveException in case the shell command doesn't send any output
+     *            for a period longer than <var>maxTimeToOutputResponse</var>.
+     * @throws IOException in case of I/O error on the connection.
+     *
+     * @see DdmPreferences#getTimeOut()
+     */
+    static void executeRemoteCommand(InetSocketAddress adbSockAddr,
+            String command, IDevice device, IShellOutputReceiver rcvr, int maxTimeToOutputResponse)
+            throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException,
+            IOException {
+        Log.v("ddms", "execute: running " + command);
+
+        SocketChannel adbChan = null;
+        try {
+            adbChan = SocketChannel.open(adbSockAddr);
+            adbChan.configureBlocking(false);
+
+            // if the device is not -1, then we first tell adb we're looking to
+            // talk
+            // to a specific device
+            setDevice(adbChan, device);
+
+            byte[] request = formAdbRequest("shell:" + command); //$NON-NLS-1$
+            write(adbChan, request);
+
+            AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
+            if (!resp.okay) {
+                Log.e("ddms", "ADB rejected shell command (" + command + "): " + resp.message);
+                throw new AdbCommandRejectedException(resp.message);
+            }
+
+            byte[] data = new byte[16384];
+            ByteBuffer buf = ByteBuffer.wrap(data);
+            int timeToResponseCount = 0;
+            while (true) {
+                int count;
+
+                if (rcvr != null && rcvr.isCancelled()) {
+                    Log.v("ddms", "execute: cancelled");
+                    break;
+                }
+
+                count = adbChan.read(buf);
+                if (count < 0) {
+                    // we're at the end, we flush the output
+                    rcvr.flush();
+                    Log.v("ddms", "execute '" + command + "' on '" + device + "' : EOF hit. Read: "
+                            + count);
+                    break;
+                } else if (count == 0) {
+                    try {
+                        int wait = WAIT_TIME * 5;
+                        timeToResponseCount += wait;
+                        if (maxTimeToOutputResponse > 0 &&
+                                timeToResponseCount > maxTimeToOutputResponse) {
+                            throw new ShellCommandUnresponsiveException();
+                        }
+                        Thread.sleep(wait);
+                    } catch (InterruptedException ie) {
+                    }
+                } else {
+                    // reset timeout
+                    timeToResponseCount = 0;
+
+                    // send data to receiver if present
+                    if (rcvr != null) {
+                        rcvr.addOutput(buf.array(), buf.arrayOffset(), buf.position());
+                    }
+                    buf.rewind();
+                }
+            }
+        } finally {
+            if (adbChan != null) {
+                adbChan.close();
+            }
+            Log.v("ddms", "execute: returning");
+        }
+    }
+
+    /**
+     * Runs the Event log service on the {@link Device}, and provides its output to the
+     * {@link LogReceiver}.
+     * <p/>This call is blocking until {@link LogReceiver#isCancelled()} returns true.
+     * @param adbSockAddr the socket address to connect to adb
+     * @param device the Device on which to run the service
+     * @param rcvr the {@link LogReceiver} to receive the log output
+     * @throws TimeoutException in case of timeout on the connection.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     * @throws IOException in case of I/O error on the connection.
+     */
+    public static void runEventLogService(InetSocketAddress adbSockAddr, Device device,
+            LogReceiver rcvr) throws TimeoutException, AdbCommandRejectedException, IOException {
+        runLogService(adbSockAddr, device, "events", rcvr); //$NON-NLS-1$
+    }
+
+    /**
+     * Runs a log service on the {@link Device}, and provides its output to the {@link LogReceiver}.
+     * <p/>This call is blocking until {@link LogReceiver#isCancelled()} returns true.
+     * @param adbSockAddr the socket address to connect to adb
+     * @param device the Device on which to run the service
+     * @param logName the name of the log file to output
+     * @param rcvr the {@link LogReceiver} to receive the log output
+     * @throws TimeoutException in case of timeout on the connection.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     * @throws IOException in case of I/O error on the connection.
+     */
+    public static void runLogService(InetSocketAddress adbSockAddr, Device device, String logName,
+            LogReceiver rcvr) throws TimeoutException, AdbCommandRejectedException, IOException {
+        SocketChannel adbChan = null;
+
+        try {
+            adbChan = SocketChannel.open(adbSockAddr);
+            adbChan.configureBlocking(false);
+
+            // if the device is not -1, then we first tell adb we're looking to talk
+            // to a specific device
+            setDevice(adbChan, device);
+
+            byte[] request = formAdbRequest("log:" + logName);
+            write(adbChan, request);
+
+            AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
+            if (!resp.okay) {
+                throw new AdbCommandRejectedException(resp.message);
+            }
+
+            byte[] data = new byte[16384];
+            ByteBuffer buf = ByteBuffer.wrap(data);
+            while (true) {
+                int count;
+
+                if (rcvr != null && rcvr.isCancelled()) {
+                    break;
+                }
+
+                count = adbChan.read(buf);
+                if (count < 0) {
+                    break;
+                } else if (count == 0) {
+                    try {
+                        Thread.sleep(WAIT_TIME * 5);
+                    } catch (InterruptedException ie) {
+                    }
+                } else {
+                    if (rcvr != null) {
+                        rcvr.parseNewData(buf.array(), buf.arrayOffset(), buf.position());
+                    }
+                    buf.rewind();
+                }
+            }
+        } finally {
+            if (adbChan != null) {
+                adbChan.close();
+            }
+        }
+    }
+
+    /**
+     * Creates a port forwarding between a local and a remote port.
+     * @param adbSockAddr the socket address to connect to adb
+     * @param device the device on which to do the port forwarding
+     * @param localPortSpec specification of the local port to forward, should be of format
+     *                             tcp:<port number>
+     * @param remotePortSpec specification of the remote port to forward to, one of:
+     *                             tcp:<port>
+     *                             localabstract:<unix domain socket name>
+     *                             localreserved:<unix domain socket name>
+     *                             localfilesystem:<unix domain socket name>
+     *                             dev:<character device name>
+     *                             jdwp:<process pid> (remote only)
+     * @throws TimeoutException in case of timeout on the connection.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     * @throws IOException in case of I/O error on the connection.
+     */
+    public static void createForward(InetSocketAddress adbSockAddr, Device device,
+            String localPortSpec, String remotePortSpec)
+                    throws TimeoutException, AdbCommandRejectedException, IOException {
+
+        SocketChannel adbChan = null;
+        try {
+            adbChan = SocketChannel.open(adbSockAddr);
+            adbChan.configureBlocking(false);
+
+            byte[] request = formAdbRequest(String.format(
+                    "host-serial:%1$s:forward:%2$s;%3$s", //$NON-NLS-1$
+                    device.getSerialNumber(), localPortSpec, remotePortSpec));
+
+            write(adbChan, request);
+
+            AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
+            if (!resp.okay) {
+                Log.w("create-forward", "Error creating forward: " + resp.message);
+                throw new AdbCommandRejectedException(resp.message);
+            }
+        } finally {
+            if (adbChan != null) {
+                adbChan.close();
+            }
+        }
+    }
+
+    /**
+     * Remove a port forwarding between a local and a remote port.
+     * @param adbSockAddr the socket address to connect to adb
+     * @param device the device on which to remove the port forwarding
+     * @param localPortSpec specification of the local port that was forwarded, should be of format
+     *                             tcp:<port number>
+     * @param remotePortSpec specification of the remote port forwarded to, one of:
+     *                             tcp:<port>
+     *                             localabstract:<unix domain socket name>
+     *                             localreserved:<unix domain socket name>
+     *                             localfilesystem:<unix domain socket name>
+     *                             dev:<character device name>
+     *                             jdwp:<process pid> (remote only)
+     * @throws TimeoutException in case of timeout on the connection.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     * @throws IOException in case of I/O error on the connection.
+     */
+    public static void removeForward(InetSocketAddress adbSockAddr, Device device,
+            String localPortSpec, String remotePortSpec)
+                    throws TimeoutException, AdbCommandRejectedException, IOException {
+
+        SocketChannel adbChan = null;
+        try {
+            adbChan = SocketChannel.open(adbSockAddr);
+            adbChan.configureBlocking(false);
+
+            byte[] request = formAdbRequest(String.format(
+                    "host-serial:%1$s:killforward:%2$s;%3$s", //$NON-NLS-1$
+                    device.getSerialNumber(), localPortSpec, remotePortSpec));
+
+            write(adbChan, request);
+
+            AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
+            if (!resp.okay) {
+                Log.w("remove-forward", "Error creating forward: " + resp.message);
+                throw new AdbCommandRejectedException(resp.message);
+            }
+        } finally {
+            if (adbChan != null) {
+                adbChan.close();
+            }
+        }
+    }
+
+    /**
+     * Checks to see if the first four bytes in "reply" are OKAY.
+     */
+    static boolean isOkay(byte[] reply) {
+        return reply[0] == (byte)'O' && reply[1] == (byte)'K'
+                && reply[2] == (byte)'A' && reply[3] == (byte)'Y';
+    }
+
+    /**
+     * Converts an ADB reply to a string.
+     */
+    static String replyToString(byte[] reply) {
+        String result;
+        try {
+            result = new String(reply, DEFAULT_ENCODING);
+        } catch (UnsupportedEncodingException uee) {
+            uee.printStackTrace(); // not expected
+            result = "";
+        }
+        return result;
+    }
+
+    /**
+     * Reads from the socket until the array is filled, or no more data is coming (because
+     * the socket closed or the timeout expired).
+     * <p/>This uses the default time out value.
+     *
+     * @param chan the opened socket to read from. It must be in non-blocking
+     *      mode for timeouts to work
+     * @param data the buffer to store the read data into.
+     * @throws TimeoutException in case of timeout on the connection.
+     * @throws IOException in case of I/O error on the connection.
+     */
+    static void read(SocketChannel chan, byte[] data) throws TimeoutException, IOException {
+        read(chan, data, -1, DdmPreferences.getTimeOut());
+    }
+
+    /**
+     * Reads from the socket until the array is filled, the optional length
+     * is reached, or no more data is coming (because the socket closed or the
+     * timeout expired). After "timeout" milliseconds since the
+     * previous successful read, this will return whether or not new data has
+     * been found.
+     *
+     * @param chan the opened socket to read from. It must be in non-blocking
+     *      mode for timeouts to work
+     * @param data the buffer to store the read data into.
+     * @param length the length to read or -1 to fill the data buffer completely
+     * @param timeout The timeout value. A timeout of zero means "wait forever".
+     */
+    static void read(SocketChannel chan, byte[] data, int length, int timeout)
+            throws TimeoutException, IOException {
+        ByteBuffer buf = ByteBuffer.wrap(data, 0, length != -1 ? length : data.length);
+        int numWaits = 0;
+
+        while (buf.position() != buf.limit()) {
+            int count;
+
+            count = chan.read(buf);
+            if (count < 0) {
+                Log.d("ddms", "read: channel EOF");
+                throw new IOException("EOF");
+            } else if (count == 0) {
+                // TODO: need more accurate timeout?
+                if (timeout != 0 && numWaits * WAIT_TIME > timeout) {
+                    Log.d("ddms", "read: timeout");
+                    throw new TimeoutException();
+                }
+                // non-blocking spin
+                try {
+                    Thread.sleep(WAIT_TIME);
+                } catch (InterruptedException ie) {
+                }
+                numWaits++;
+            } else {
+                numWaits = 0;
+            }
+        }
+    }
+
+    /**
+     * Write until all data in "data" is written or the connection fails or times out.
+     * <p/>This uses the default time out value.
+     * @param chan the opened socket to write to.
+     * @param data the buffer to send.
+     * @throws TimeoutException in case of timeout on the connection.
+     * @throws IOException in case of I/O error on the connection.
+     */
+    static void write(SocketChannel chan, byte[] data) throws TimeoutException, IOException {
+        write(chan, data, -1, DdmPreferences.getTimeOut());
+    }
+
+    /**
+     * Write until all data in "data" is written, the optional length is reached,
+     * the timeout expires, or the connection fails. Returns "true" if all
+     * data was written.
+     * @param chan the opened socket to write to.
+     * @param data the buffer to send.
+     * @param length the length to write or -1 to send the whole buffer.
+     * @param timeout The timeout value. A timeout of zero means "wait forever".
+     * @throws TimeoutException in case of timeout on the connection.
+     * @throws IOException in case of I/O error on the connection.
+     */
+    static void write(SocketChannel chan, byte[] data, int length, int timeout)
+            throws TimeoutException, IOException {
+        ByteBuffer buf = ByteBuffer.wrap(data, 0, length != -1 ? length : data.length);
+        int numWaits = 0;
+
+        while (buf.position() != buf.limit()) {
+            int count;
+
+            count = chan.write(buf);
+            if (count < 0) {
+                Log.d("ddms", "write: channel EOF");
+                throw new IOException("channel EOF");
+            } else if (count == 0) {
+                // TODO: need more accurate timeout?
+                if (timeout != 0 && numWaits * WAIT_TIME > timeout) {
+                    Log.d("ddms", "write: timeout");
+                    throw new TimeoutException();
+                }
+                // non-blocking spin
+                try {
+                    Thread.sleep(WAIT_TIME);
+                } catch (InterruptedException ie) {
+                }
+                numWaits++;
+            } else {
+                numWaits = 0;
+            }
+        }
+    }
+
+    /**
+     * tells adb to talk to a specific device
+     *
+     * @param adbChan the socket connection to adb
+     * @param device The device to talk to.
+     * @throws TimeoutException in case of timeout on the connection.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     * @throws IOException in case of I/O error on the connection.
+     */
+    static void setDevice(SocketChannel adbChan, IDevice device)
+            throws TimeoutException, AdbCommandRejectedException, IOException {
+        // if the device is not -1, then we first tell adb we're looking to talk
+        // to a specific device
+        if (device != null) {
+            String msg = "host:transport:" + device.getSerialNumber(); //$NON-NLS-1$
+            byte[] device_query = formAdbRequest(msg);
+
+            write(adbChan, device_query);
+
+            AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
+            if (!resp.okay) {
+                throw new AdbCommandRejectedException(resp.message,
+                        true/*errorDuringDeviceSelection*/);
+            }
+        }
+    }
+
+    /**
+     * Reboot the device.
+     *
+     * @param into what to reboot into (recovery, bootloader).  Or null to just reboot.
+     * @throws TimeoutException in case of timeout on the connection.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     * @throws IOException in case of I/O error on the connection.
+     */
+    public static void reboot(String into, InetSocketAddress adbSockAddr,
+            Device device) throws TimeoutException, AdbCommandRejectedException, IOException {
+        byte[] request;
+        if (into == null) {
+            request = formAdbRequest("reboot:"); //$NON-NLS-1$
+        } else {
+            request = formAdbRequest("reboot:" + into); //$NON-NLS-1$
+        }
+
+        SocketChannel adbChan = null;
+        try {
+            adbChan = SocketChannel.open(adbSockAddr);
+            adbChan.configureBlocking(false);
+
+            // if the device is not -1, then we first tell adb we're looking to talk
+            // to a specific device
+            setDevice(adbChan, device);
+
+            write(adbChan, request);
+        } finally {
+            if (adbChan != null) {
+                adbChan.close();
+            }
+        }
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/AllocationInfo.java b/ddmlib/src/main/java/com/android/ddmlib/AllocationInfo.java
new file mode 100644
index 0000000..110a715
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/AllocationInfo.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2008 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.ddmlib;
+
+import java.util.Comparator;
+import java.util.Locale;
+
+/**
+ * Holds an Allocation information.
+ */
+public class AllocationInfo implements IStackTraceInfo {
+    private final String mAllocatedClass;
+    private final int mAllocNumber;
+    private final int mAllocationSize;
+    private final short mThreadId;
+    private final StackTraceElement[] mStackTrace;
+
+    public static enum SortMode {
+        NUMBER, SIZE, CLASS, THREAD, IN_CLASS, IN_METHOD
+    }
+
+    public static final class AllocationSorter implements Comparator<AllocationInfo> {
+
+        private SortMode mSortMode = SortMode.SIZE;
+        private boolean mDescending = true;
+
+        public AllocationSorter() {
+        }
+
+        public void setSortMode(SortMode mode) {
+            if (mSortMode == mode) {
+                mDescending = !mDescending;
+            } else {
+                mSortMode = mode;
+            }
+        }
+
+        public SortMode getSortMode() {
+            return mSortMode;
+        }
+
+        public boolean isDescending() {
+            return mDescending;
+        }
+
+        @Override
+        public int compare(AllocationInfo o1, AllocationInfo o2) {
+            int diff = 0;
+            switch (mSortMode) {
+                case NUMBER:
+                    diff = o1.mAllocNumber - o2.mAllocNumber;
+                    break;
+                case SIZE:
+                    // pass, since diff is init with 0, we'll use SIZE compare below
+                    // as a back up anyway.
+                    break;
+                case CLASS:
+                    diff = o1.mAllocatedClass.compareTo(o2.mAllocatedClass);
+                    break;
+                case THREAD:
+                    diff = o1.mThreadId - o2.mThreadId;
+                    break;
+                case IN_CLASS:
+                    String class1 = o1.getFirstTraceClassName();
+                    String class2 = o2.getFirstTraceClassName();
+                    diff = compareOptionalString(class1, class2);
+                    break;
+                case IN_METHOD:
+                    String method1 = o1.getFirstTraceMethodName();
+                    String method2 = o2.getFirstTraceMethodName();
+                    diff = compareOptionalString(method1, method2);
+                    break;
+            }
+
+            if (diff == 0) {
+                // same? compare on size
+                diff = o1.mAllocationSize - o2.mAllocationSize;
+            }
+
+            if (mDescending) {
+                diff = -diff;
+            }
+
+            return diff;
+        }
+
+        /** compares two strings that could be null */
+        private int compareOptionalString(String str1, String str2) {
+            if (str1 != null) {
+                if (str2 == null) {
+                    return -1;
+                } else {
+                    return str1.compareTo(str2);
+                }
+            } else {
+                if (str2 == null) {
+                    return 0;
+                } else {
+                    return 1;
+                }
+            }
+        }
+    }
+
+    /*
+     * Simple constructor.
+     */
+    AllocationInfo(int allocNumber, String allocatedClass, int allocationSize,
+        short threadId, StackTraceElement[] stackTrace) {
+        mAllocNumber = allocNumber;
+        mAllocatedClass = allocatedClass;
+        mAllocationSize = allocationSize;
+        mThreadId = threadId;
+        mStackTrace = stackTrace;
+    }
+
+    /**
+     * Returns the allocation number. Allocations are numbered as they happen with the most
+     * recent one having the highest number
+     */
+    public int getAllocNumber() {
+        return mAllocNumber;
+    }
+
+    /**
+     * Returns the name of the allocated class.
+     */
+    public String getAllocatedClass() {
+        return mAllocatedClass;
+    }
+
+    /**
+     * Returns the size of the allocation.
+     */
+    public int getSize() {
+        return mAllocationSize;
+    }
+
+    /**
+     * Returns the id of the thread that performed the allocation.
+     */
+    public short getThreadId() {
+        return mThreadId;
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.android.ddmlib.IStackTraceInfo#getStackTrace()
+     */
+    @Override
+    public StackTraceElement[] getStackTrace() {
+        return mStackTrace;
+    }
+
+    public int compareTo(AllocationInfo otherAlloc) {
+        return otherAlloc.mAllocationSize - mAllocationSize;
+    }
+
+    public String getFirstTraceClassName() {
+        if (mStackTrace.length > 0) {
+            return mStackTrace[0].getClassName();
+        }
+
+        return null;
+    }
+
+    public String getFirstTraceMethodName() {
+        if (mStackTrace.length > 0) {
+            return mStackTrace[0].getMethodName();
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns true if the given filter matches case insensitively (according to
+     * the given locale) this allocation info.
+     */
+    public boolean filter(String filter, boolean fullTrace, Locale locale) {
+        if (mAllocatedClass.toLowerCase(locale).contains(filter)) {
+            return true;
+        }
+
+        if (mStackTrace.length > 0) {
+            // check the top of the stack trace always
+            final int length = fullTrace ? mStackTrace.length : 1;
+
+            for (int i = 0 ; i < length ; i++) {
+                if (mStackTrace[i].getClassName().toLowerCase(locale).contains(filter)) {
+                    return true;
+                }
+
+                if (mStackTrace[i].getMethodName().toLowerCase(locale).contains(filter)) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/AndroidDebugBridge.java b/ddmlib/src/main/java/com/android/ddmlib/AndroidDebugBridge.java
new file mode 100644
index 0000000..1edc383
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/AndroidDebugBridge.java
@@ -0,0 +1,1179 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import com.android.ddmlib.Log.LogLevel;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.lang.Thread.State;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.security.InvalidParameterException;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A connection to the host-side android debug bridge (adb)
+ * <p/>This is the central point to communicate with any devices, emulators, or the applications
+ * running on them.
+ * <p/><b>{@link #init(boolean)} must be called before anything is done.</b>
+ */
+public final class AndroidDebugBridge {
+
+    /*
+     * Minimum and maximum version of adb supported. This correspond to
+     * ADB_SERVER_VERSION found in //device/tools/adb/adb.h
+     */
+
+    private static final int ADB_VERSION_MICRO_MIN = 20;
+    private static final int ADB_VERSION_MICRO_MAX = -1;
+
+    private static final Pattern sAdbVersion = Pattern.compile(
+            "^.*(\\d+)\\.(\\d+)\\.(\\d+)$"); //$NON-NLS-1$
+
+    private static final String ADB = "adb"; //$NON-NLS-1$
+    private static final String DDMS = "ddms"; //$NON-NLS-1$
+    private static final String SERVER_PORT_ENV_VAR = "ANDROID_ADB_SERVER_PORT"; //$NON-NLS-1$
+
+    // Where to find the ADB bridge.
+    static final String ADB_HOST = "127.0.0.1"; //$NON-NLS-1$
+    static final int ADB_PORT = 5037;
+
+    private static InetAddress sHostAddr;
+    private static InetSocketAddress sSocketAddr;
+
+    private static AndroidDebugBridge sThis;
+    private static boolean sInitialized = false;
+    private static boolean sClientSupport;
+
+    /** Full path to adb. */
+    private String mAdbOsLocation = null;
+
+    private boolean mVersionCheck;
+
+    private boolean mStarted = false;
+
+    private DeviceMonitor mDeviceMonitor;
+
+    private static final ArrayList<IDebugBridgeChangeListener> sBridgeListeners =
+        new ArrayList<IDebugBridgeChangeListener>();
+    private static final ArrayList<IDeviceChangeListener> sDeviceListeners =
+        new ArrayList<IDeviceChangeListener>();
+    private static final ArrayList<IClientChangeListener> sClientListeners =
+        new ArrayList<IClientChangeListener>();
+
+    // lock object for synchronization
+    private static final Object sLock = sBridgeListeners;
+
+    /**
+     * Classes which implement this interface provide a method that deals
+     * with {@link AndroidDebugBridge} changes.
+     */
+    public interface IDebugBridgeChangeListener {
+        /**
+         * Sent when a new {@link AndroidDebugBridge} is connected.
+         * <p/>
+         * This is sent from a non UI thread.
+         * @param bridge the new {@link AndroidDebugBridge} object.
+         */
+        public void bridgeChanged(AndroidDebugBridge bridge);
+    }
+
+    /**
+     * Classes which implement this interface provide methods that deal
+     * with {@link IDevice} addition, deletion, and changes.
+     */
+    public interface IDeviceChangeListener {
+        /**
+         * Sent when the a device is connected to the {@link AndroidDebugBridge}.
+         * <p/>
+         * This is sent from a non UI thread.
+         * @param device the new device.
+         */
+        public void deviceConnected(IDevice device);
+
+        /**
+         * Sent when the a device is connected to the {@link AndroidDebugBridge}.
+         * <p/>
+         * This is sent from a non UI thread.
+         * @param device the new device.
+         */
+        public void deviceDisconnected(IDevice device);
+
+        /**
+         * Sent when a device data changed, or when clients are started/terminated on the device.
+         * <p/>
+         * This is sent from a non UI thread.
+         * @param device the device that was updated.
+         * @param changeMask the mask describing what changed. It can contain any of the following
+         * values: {@link IDevice#CHANGE_BUILD_INFO}, {@link IDevice#CHANGE_STATE},
+         * {@link IDevice#CHANGE_CLIENT_LIST}
+         */
+        public void deviceChanged(IDevice device, int changeMask);
+    }
+
+    /**
+     * Classes which implement this interface provide methods that deal
+     * with {@link Client}  changes.
+     */
+    public interface IClientChangeListener {
+        /**
+         * Sent when an existing client information changed.
+         * <p/>
+         * This is sent from a non UI thread.
+         * @param client the updated client.
+         * @param changeMask the bit mask describing the changed properties. It can contain
+         * any of the following values: {@link Client#CHANGE_INFO},
+         * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE},
+         * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
+         * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
+         */
+        public void clientChanged(Client client, int changeMask);
+    }
+
+    /**
+     * Initialized the library only if needed.
+     *
+     * @param clientSupport Indicates whether the library should enable the monitoring and
+     *                      interaction with applications running on the devices.
+     *
+     * @see #init(boolean)
+     */
+    public static synchronized void initIfNeeded(boolean clientSupport) {
+        if (sInitialized) {
+            return;
+        }
+
+        init(clientSupport);
+    }
+
+    /**
+     * Initializes the <code>ddm</code> library.
+     * <p/>This must be called once <b>before</b> any call to
+     * {@link #createBridge(String, boolean)}.
+     * <p>The library can be initialized in 2 ways:
+     * <ul>
+     * <li>Mode 1: <var>clientSupport</var> == <code>true</code>.<br>The library monitors the
+     * devices and the applications running on them. It will connect to each application, as a
+     * debugger of sort, to be able to interact with them through JDWP packets.</li>
+     * <li>Mode 2: <var>clientSupport</var> == <code>false</code>.<br>The library only monitors
+     * devices. The applications are left untouched, letting other tools built on
+     * <code>ddmlib</code> to connect a debugger to them.</li>
+     * </ul>
+     * <p/><b>Only one tool can run in mode 1 at the same time.</b>
+     * <p/>Note that mode 1 does not prevent debugging of applications running on devices. Mode 1
+     * lets debuggers connect to <code>ddmlib</code> which acts as a proxy between the debuggers and
+     * the applications to debug. See {@link Client#getDebuggerListenPort()}.
+     * <p/>The preferences of <code>ddmlib</code> should also be initialized with whatever default
+     * values were changed from the default values.
+     * <p/>When the application quits, {@link #terminate()} should be called.
+     * @param clientSupport Indicates whether the library should enable the monitoring and
+     *                      interaction with applications running on the devices.
+     * @see AndroidDebugBridge#createBridge(String, boolean)
+     * @see DdmPreferences
+     */
+    public static synchronized void init(boolean clientSupport) {
+        if (sInitialized) {
+            throw new IllegalStateException("AndroidDebugBridge.init() has already been called.");
+        }
+        sInitialized = true;
+        sClientSupport = clientSupport;
+
+        // Determine port and instantiate socket address.
+        initAdbSocketAddr();
+
+        MonitorThread monitorThread = MonitorThread.createInstance();
+        monitorThread.start();
+
+        HandleHello.register(monitorThread);
+        HandleAppName.register(monitorThread);
+        HandleTest.register(monitorThread);
+        HandleThread.register(monitorThread);
+        HandleHeap.register(monitorThread);
+        HandleWait.register(monitorThread);
+        HandleProfiling.register(monitorThread);
+        HandleNativeHeap.register(monitorThread);
+        HandleViewDebug.register(monitorThread);
+    }
+
+    /**
+     * Terminates the ddm library. This must be called upon application termination.
+     */
+    public static synchronized void terminate() {
+        // kill the monitoring services
+        if (sThis != null && sThis.mDeviceMonitor != null) {
+            sThis.mDeviceMonitor.stop();
+            sThis.mDeviceMonitor = null;
+        }
+
+        MonitorThread monitorThread = MonitorThread.getInstance();
+        if (monitorThread != null) {
+            monitorThread.quit();
+        }
+
+        sInitialized = false;
+    }
+
+    /**
+     * Returns whether the ddmlib is setup to support monitoring and interacting with
+     * {@link Client}s running on the {@link IDevice}s.
+     */
+    static boolean getClientSupport() {
+        return sClientSupport;
+    }
+
+    /**
+     * Returns the socket address of the ADB server on the host.
+     */
+    public static InetSocketAddress getSocketAddress() {
+        return sSocketAddr;
+    }
+
+    /**
+     * Creates a {@link AndroidDebugBridge} that is not linked to any particular executable.
+     * <p/>This bridge will expect adb to be running. It will not be able to start/stop/restart
+     * adb.
+     * <p/>If a bridge has already been started, it is directly returned with no changes (similar
+     * to calling {@link #getBridge()}).
+     * @return a connected bridge.
+     */
+    public static AndroidDebugBridge createBridge() {
+        synchronized (sLock) {
+            if (sThis != null) {
+                return sThis;
+            }
+
+            try {
+                sThis = new AndroidDebugBridge();
+                sThis.start();
+            } catch (InvalidParameterException e) {
+                sThis = null;
+            }
+
+            // because the listeners could remove themselves from the list while processing
+            // their event callback, we make a copy of the list and iterate on it instead of
+            // the main list.
+            // This mostly happens when the application quits.
+            IDebugBridgeChangeListener[] listenersCopy = sBridgeListeners.toArray(
+                    new IDebugBridgeChangeListener[sBridgeListeners.size()]);
+
+            // notify the listeners of the change
+            for (IDebugBridgeChangeListener listener : listenersCopy) {
+                // we attempt to catch any exception so that a bad listener doesn't kill our
+                // thread
+                try {
+                    listener.bridgeChanged(sThis);
+                } catch (Exception e) {
+                    Log.e(DDMS, e);
+                }
+            }
+
+            return sThis;
+        }
+    }
+
+
+    /**
+     * Creates a new debug bridge from the location of the command line tool.
+     * <p/>
+     * Any existing server will be disconnected, unless the location is the same and
+     * <code>forceNewBridge</code> is set to false.
+     * @param osLocation the location of the command line tool 'adb'
+     * @param forceNewBridge force creation of a new bridge even if one with the same location
+     * already exists.
+     * @return a connected bridge.
+     */
+    public static AndroidDebugBridge createBridge(String osLocation, boolean forceNewBridge) {
+        synchronized (sLock) {
+            if (sThis != null) {
+                if (sThis.mAdbOsLocation != null && sThis.mAdbOsLocation.equals(osLocation) &&
+                        !forceNewBridge) {
+                    return sThis;
+                } else {
+                    // stop the current server
+                    sThis.stop();
+                }
+            }
+
+            try {
+                sThis = new AndroidDebugBridge(osLocation);
+                sThis.start();
+            } catch (InvalidParameterException e) {
+                sThis = null;
+            }
+
+            // because the listeners could remove themselves from the list while processing
+            // their event callback, we make a copy of the list and iterate on it instead of
+            // the main list.
+            // This mostly happens when the application quits.
+            IDebugBridgeChangeListener[] listenersCopy = sBridgeListeners.toArray(
+                    new IDebugBridgeChangeListener[sBridgeListeners.size()]);
+
+            // notify the listeners of the change
+            for (IDebugBridgeChangeListener listener : listenersCopy) {
+                // we attempt to catch any exception so that a bad listener doesn't kill our
+                // thread
+                try {
+                    listener.bridgeChanged(sThis);
+                } catch (Exception e) {
+                    Log.e(DDMS, e);
+                }
+            }
+
+            return sThis;
+        }
+    }
+
+    /**
+     * Returns the current debug bridge. Can be <code>null</code> if none were created.
+     */
+    public static AndroidDebugBridge getBridge() {
+        return sThis;
+    }
+
+    /**
+     * Disconnects the current debug bridge, and destroy the object.
+     * <p/>This also stops the current adb host server.
+     * <p/>
+     * A new object will have to be created with {@link #createBridge(String, boolean)}.
+     */
+    public static void disconnectBridge() {
+        synchronized (sLock) {
+            if (sThis != null) {
+                sThis.stop();
+                sThis = null;
+
+                // because the listeners could remove themselves from the list while processing
+                // their event callback, we make a copy of the list and iterate on it instead of
+                // the main list.
+                // This mostly happens when the application quits.
+                IDebugBridgeChangeListener[] listenersCopy = sBridgeListeners.toArray(
+                        new IDebugBridgeChangeListener[sBridgeListeners.size()]);
+
+                // notify the listeners.
+                for (IDebugBridgeChangeListener listener : listenersCopy) {
+                    // we attempt to catch any exception so that a bad listener doesn't kill our
+                    // thread
+                    try {
+                        listener.bridgeChanged(sThis);
+                    } catch (Exception e) {
+                        Log.e(DDMS, e);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Adds the listener to the collection of listeners who will be notified when a new
+     * {@link AndroidDebugBridge} is connected, by sending it one of the messages defined
+     * in the {@link IDebugBridgeChangeListener} interface.
+     * @param listener The listener which should be notified.
+     */
+    public static void addDebugBridgeChangeListener(IDebugBridgeChangeListener listener) {
+        synchronized (sLock) {
+            if (!sBridgeListeners.contains(listener)) {
+                sBridgeListeners.add(listener);
+                if (sThis != null) {
+                    // we attempt to catch any exception so that a bad listener doesn't kill our
+                    // thread
+                    try {
+                        listener.bridgeChanged(sThis);
+                    } catch (Exception e) {
+                        Log.e(DDMS, e);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Removes the listener from the collection of listeners who will be notified when a new
+     * {@link AndroidDebugBridge} is started.
+     * @param listener The listener which should no longer be notified.
+     */
+    public static void removeDebugBridgeChangeListener(IDebugBridgeChangeListener listener) {
+        synchronized (sLock) {
+            sBridgeListeners.remove(listener);
+        }
+    }
+
+    /**
+     * Adds the listener to the collection of listeners who will be notified when a {@link IDevice}
+     * is connected, disconnected, or when its properties or its {@link Client} list changed,
+     * by sending it one of the messages defined in the {@link IDeviceChangeListener} interface.
+     * @param listener The listener which should be notified.
+     */
+    public static void addDeviceChangeListener(IDeviceChangeListener listener) {
+        synchronized (sLock) {
+            if (!sDeviceListeners.contains(listener)) {
+                sDeviceListeners.add(listener);
+            }
+        }
+    }
+
+    /**
+     * Removes the listener from the collection of listeners who will be notified when a
+     * {@link IDevice} is connected, disconnected, or when its properties or its {@link Client}
+     * list changed.
+     * @param listener The listener which should no longer be notified.
+     */
+    public static void removeDeviceChangeListener(IDeviceChangeListener listener) {
+        synchronized (sLock) {
+            sDeviceListeners.remove(listener);
+        }
+    }
+
+    /**
+     * Adds the listener to the collection of listeners who will be notified when a {@link Client}
+     * property changed, by sending it one of the messages defined in the
+     * {@link IClientChangeListener} interface.
+     * @param listener The listener which should be notified.
+     */
+    public static void addClientChangeListener(IClientChangeListener listener) {
+        synchronized (sLock) {
+            if (!sClientListeners.contains(listener)) {
+                sClientListeners.add(listener);
+            }
+        }
+    }
+
+    /**
+     * Removes the listener from the collection of listeners who will be notified when a
+     * {@link Client} property changed.
+     * @param listener The listener which should no longer be notified.
+     */
+    public static void removeClientChangeListener(IClientChangeListener listener) {
+        synchronized (sLock) {
+            sClientListeners.remove(listener);
+        }
+    }
+
+
+    /**
+     * Returns the devices.
+     * @see #hasInitialDeviceList()
+     */
+    public IDevice[] getDevices() {
+        synchronized (sLock) {
+            if (mDeviceMonitor != null) {
+                return mDeviceMonitor.getDevices();
+            }
+        }
+
+        return new IDevice[0];
+    }
+
+    /**
+     * Returns whether the bridge has acquired the initial list from adb after being created.
+     * <p/>Calling {@link #getDevices()} right after {@link #createBridge(String, boolean)} will
+     * generally result in an empty list. This is due to the internal asynchronous communication
+     * mechanism with <code>adb</code> that does not guarantee that the {@link IDevice} list has been
+     * built before the call to {@link #getDevices()}.
+     * <p/>The recommended way to get the list of {@link IDevice} objects is to create a
+     * {@link IDeviceChangeListener} object.
+     */
+    public boolean hasInitialDeviceList() {
+        if (mDeviceMonitor != null) {
+            return mDeviceMonitor.hasInitialDeviceList();
+        }
+
+        return false;
+    }
+
+    /**
+     * Sets the client to accept debugger connection on the custom "Selected debug port".
+     * @param selectedClient the client. Can be null.
+     */
+    public void setSelectedClient(Client selectedClient) {
+        MonitorThread monitorThread = MonitorThread.getInstance();
+        if (monitorThread != null) {
+            monitorThread.setSelectedClient(selectedClient);
+        }
+    }
+
+    /**
+     * Returns whether the {@link AndroidDebugBridge} object is still connected to the adb daemon.
+     */
+    public boolean isConnected() {
+        MonitorThread monitorThread = MonitorThread.getInstance();
+        if (mDeviceMonitor != null && monitorThread != null) {
+            return mDeviceMonitor.isMonitoring() && monitorThread.getState() != State.TERMINATED;
+        }
+        return false;
+    }
+
+    /**
+     * Returns the number of times the {@link AndroidDebugBridge} object attempted to connect
+     * to the adb daemon.
+     */
+    public int getConnectionAttemptCount() {
+        if (mDeviceMonitor != null) {
+            return mDeviceMonitor.getConnectionAttemptCount();
+        }
+        return -1;
+    }
+
+    /**
+     * Returns the number of times the {@link AndroidDebugBridge} object attempted to restart
+     * the adb daemon.
+     */
+    public int getRestartAttemptCount() {
+        if (mDeviceMonitor != null) {
+            return mDeviceMonitor.getRestartAttemptCount();
+        }
+        return -1;
+    }
+
+    /**
+     * Creates a new bridge.
+     * @param osLocation the location of the command line tool
+     * @throws InvalidParameterException
+     */
+    private AndroidDebugBridge(String osLocation) throws InvalidParameterException {
+        if (osLocation == null || osLocation.isEmpty()) {
+            throw new InvalidParameterException();
+        }
+        mAdbOsLocation = osLocation;
+
+        checkAdbVersion();
+    }
+
+    /**
+     * Creates a new bridge not linked to any particular adb executable.
+     */
+    private AndroidDebugBridge() {
+    }
+
+    /**
+     * Queries adb for its version number and checks it against {@link #MIN_VERSION_NUMBER} and
+     * {@link #MAX_VERSION_NUMBER}
+     */
+    private void checkAdbVersion() {
+        // default is bad check
+        mVersionCheck = false;
+
+        if (mAdbOsLocation == null) {
+            return;
+        }
+
+        String[] command = new String[2];
+        command[0] = mAdbOsLocation;
+        command[1] = "version"; //$NON-NLS-1$
+        Log.d(DDMS, String.format("Checking '%1$s version'", mAdbOsLocation));
+        Process process = null;
+        try {
+            process = Runtime.getRuntime().exec(command);
+        } catch (IOException e) {
+            boolean exists = new File(mAdbOsLocation).exists();
+            String msg;
+            if (exists) {
+                msg = String.format(
+                        "Unexpected exception '%1$s' while attempting to get adb version from '%2$s'",
+                        e.getMessage(), mAdbOsLocation);
+            } else {
+                msg = "Unable to locate adb.\n" +
+                      "Please use SDK Manager and check if Android SDK platform-tools are installed.";
+            }
+            Log.logAndDisplay(LogLevel.ERROR, ADB, msg);
+            return;
+        }
+
+        ArrayList<String> errorOutput = new ArrayList<String>();
+        ArrayList<String> stdOutput = new ArrayList<String>();
+        int status;
+        try {
+            status = grabProcessOutput(process, errorOutput, stdOutput,
+                    true /* waitForReaders */);
+        } catch (InterruptedException e) {
+            return;
+        }
+
+        if (status != 0) {
+            StringBuilder builder = new StringBuilder("'adb version' failed!"); //$NON-NLS-1$
+            for (String error : errorOutput) {
+                builder.append('\n');
+                builder.append(error);
+            }
+            Log.logAndDisplay(LogLevel.ERROR, ADB, builder.toString());
+        }
+
+        // check both stdout and stderr
+        boolean versionFound = false;
+        for (String line : stdOutput) {
+            versionFound = scanVersionLine(line);
+            if (versionFound) {
+                break;
+            }
+        }
+        if (!versionFound) {
+            for (String line : errorOutput) {
+                versionFound = scanVersionLine(line);
+                if (versionFound) {
+                    break;
+                }
+            }
+        }
+
+        if (!versionFound) {
+            // if we get here, we failed to parse the output.
+            StringBuilder builder = new StringBuilder(
+                    "Failed to parse the output of 'adb version':\n"); //$NON-NLS-1$
+            builder.append("Standard Output was:\n"); //$NON-NLS-1$
+            for (String line : stdOutput) {
+                builder.append(line);
+                builder.append('\n');
+            }
+            builder.append("\nError Output was:\n"); //$NON-NLS-1$
+            for (String line : errorOutput) {
+                builder.append(line);
+                builder.append('\n');
+            }
+            Log.logAndDisplay(LogLevel.ERROR, ADB, builder.toString());
+        }
+    }
+
+    /**
+     * Scans a line resulting from 'adb version' for a potential version number.
+     * <p/>
+     * If a version number is found, it checks the version number against what is expected
+     * by this version of ddms.
+     * <p/>
+     * Returns true when a version number has been found so that we can stop scanning,
+     * whether the version number is in the acceptable range or not.
+     *
+     * @param line The line to scan.
+     * @return True if a version number was found (whether it is acceptable or not).
+     */
+    @SuppressWarnings("all") // With Eclipse 3.6, replace by @SuppressWarnings("unused")
+    private boolean scanVersionLine(String line) {
+        if (line != null) {
+            Matcher matcher = sAdbVersion.matcher(line);
+            if (matcher.matches()) {
+                int majorVersion = Integer.parseInt(matcher.group(1));
+                int minorVersion = Integer.parseInt(matcher.group(2));
+                int microVersion = Integer.parseInt(matcher.group(3));
+
+                // check only the micro version for now.
+                if (microVersion < ADB_VERSION_MICRO_MIN) {
+                    String message = String.format(
+                            "Required minimum version of adb: %1$d.%2$d.%3$d." //$NON-NLS-1$
+                            + "Current version is %1$d.%2$d.%4$d", //$NON-NLS-1$
+                            majorVersion, minorVersion, ADB_VERSION_MICRO_MIN,
+                            microVersion);
+                    Log.logAndDisplay(LogLevel.ERROR, ADB, message);
+                } else if (ADB_VERSION_MICRO_MAX != -1 &&
+                        microVersion > ADB_VERSION_MICRO_MAX) {
+                    String message = String.format(
+                            "Required maximum version of adb: %1$d.%2$d.%3$d." //$NON-NLS-1$
+                            + "Current version is %1$d.%2$d.%4$d", //$NON-NLS-1$
+                            majorVersion, minorVersion, ADB_VERSION_MICRO_MAX,
+                            microVersion);
+                    Log.logAndDisplay(LogLevel.ERROR, ADB, message);
+                } else {
+                    mVersionCheck = true;
+                }
+
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Starts the debug bridge.
+     * @return true if success.
+     */
+    boolean start() {
+        if (mAdbOsLocation != null && (!mVersionCheck || !startAdb())) {
+            return false;
+        }
+
+        mStarted = true;
+
+        // now that the bridge is connected, we start the underlying services.
+        mDeviceMonitor = new DeviceMonitor(this);
+        mDeviceMonitor.start();
+
+        return true;
+    }
+
+   /**
+     * Kills the debug bridge, and the adb host server.
+     * @return true if success
+     */
+    boolean stop() {
+        // if we haven't started we return false;
+        if (!mStarted) {
+            return false;
+        }
+
+        // kill the monitoring services
+        mDeviceMonitor.stop();
+        mDeviceMonitor = null;
+
+        if (!stopAdb()) {
+            return false;
+        }
+
+        mStarted = false;
+        return true;
+    }
+
+    /**
+     * Restarts adb, but not the services around it.
+     * @return true if success.
+     */
+    public boolean restart() {
+        if (mAdbOsLocation == null) {
+            Log.e(ADB,
+                    "Cannot restart adb when AndroidDebugBridge is created without the location of adb."); //$NON-NLS-1$
+            return false;
+        }
+
+        if (!mVersionCheck) {
+            Log.logAndDisplay(LogLevel.ERROR, ADB,
+                    "Attempting to restart adb, but version check failed!"); //$NON-NLS-1$
+            return false;
+        }
+        synchronized (this) {
+            stopAdb();
+
+            boolean restart = startAdb();
+
+            if (restart && mDeviceMonitor == null) {
+                mDeviceMonitor = new DeviceMonitor(this);
+                mDeviceMonitor.start();
+            }
+
+            return restart;
+        }
+    }
+
+    /**
+     * Notify the listener of a new {@link IDevice}.
+     * <p/>
+     * The notification of the listeners is done in a synchronized block. It is important to
+     * expect the listeners to potentially access various methods of {@link IDevice} as well as
+     * {@link #getDevices()} which use internal locks.
+     * <p/>
+     * For this reason, any call to this method from a method of {@link DeviceMonitor},
+     * {@link IDevice} which is also inside a synchronized block, should first synchronize on
+     * the {@link AndroidDebugBridge} lock. Access to this lock is done through {@link #getLock()}.
+     * @param device the new <code>IDevice</code>.
+     * @see #getLock()
+     */
+    void deviceConnected(IDevice device) {
+        // because the listeners could remove themselves from the list while processing
+        // their event callback, we make a copy of the list and iterate on it instead of
+        // the main list.
+        // This mostly happens when the application quits.
+        IDeviceChangeListener[] listenersCopy = null;
+        synchronized (sLock) {
+            listenersCopy = sDeviceListeners.toArray(
+                    new IDeviceChangeListener[sDeviceListeners.size()]);
+        }
+
+        // Notify the listeners
+        for (IDeviceChangeListener listener : listenersCopy) {
+            // we attempt to catch any exception so that a bad listener doesn't kill our
+            // thread
+            try {
+                listener.deviceConnected(device);
+            } catch (Exception e) {
+                Log.e(DDMS, e);
+            }
+        }
+    }
+
+    /**
+     * Notify the listener of a disconnected {@link IDevice}.
+     * <p/>
+     * The notification of the listeners is done in a synchronized block. It is important to
+     * expect the listeners to potentially access various methods of {@link IDevice} as well as
+     * {@link #getDevices()} which use internal locks.
+     * <p/>
+     * For this reason, any call to this method from a method of {@link DeviceMonitor},
+     * {@link IDevice} which is also inside a synchronized block, should first synchronize on
+     * the {@link AndroidDebugBridge} lock. Access to this lock is done through {@link #getLock()}.
+     * @param device the disconnected <code>IDevice</code>.
+     * @see #getLock()
+     */
+    void deviceDisconnected(IDevice device) {
+        // because the listeners could remove themselves from the list while processing
+        // their event callback, we make a copy of the list and iterate on it instead of
+        // the main list.
+        // This mostly happens when the application quits.
+        IDeviceChangeListener[] listenersCopy = null;
+        synchronized (sLock) {
+            listenersCopy = sDeviceListeners.toArray(
+                    new IDeviceChangeListener[sDeviceListeners.size()]);
+        }
+
+        // Notify the listeners
+        for (IDeviceChangeListener listener : listenersCopy) {
+            // we attempt to catch any exception so that a bad listener doesn't kill our
+            // thread
+            try {
+                listener.deviceDisconnected(device);
+            } catch (Exception e) {
+                Log.e(DDMS, e);
+            }
+        }
+    }
+
+    /**
+     * Notify the listener of a modified {@link IDevice}.
+     * <p/>
+     * The notification of the listeners is done in a synchronized block. It is important to
+     * expect the listeners to potentially access various methods of {@link IDevice} as well as
+     * {@link #getDevices()} which use internal locks.
+     * <p/>
+     * For this reason, any call to this method from a method of {@link DeviceMonitor},
+     * {@link IDevice} which is also inside a synchronized block, should first synchronize on
+     * the {@link AndroidDebugBridge} lock. Access to this lock is done through {@link #getLock()}.
+     * @param device the modified <code>IDevice</code>.
+     * @see #getLock()
+     */
+    void deviceChanged(IDevice device, int changeMask) {
+        // because the listeners could remove themselves from the list while processing
+        // their event callback, we make a copy of the list and iterate on it instead of
+        // the main list.
+        // This mostly happens when the application quits.
+        IDeviceChangeListener[] listenersCopy = null;
+        synchronized (sLock) {
+            listenersCopy = sDeviceListeners.toArray(
+                    new IDeviceChangeListener[sDeviceListeners.size()]);
+        }
+
+        // Notify the listeners
+        for (IDeviceChangeListener listener : listenersCopy) {
+            // we attempt to catch any exception so that a bad listener doesn't kill our
+            // thread
+            try {
+                listener.deviceChanged(device, changeMask);
+            } catch (Exception e) {
+                Log.e(DDMS, e);
+            }
+        }
+    }
+
+    /**
+     * Notify the listener of a modified {@link Client}.
+     * <p/>
+     * The notification of the listeners is done in a synchronized block. It is important to
+     * expect the listeners to potentially access various methods of {@link IDevice} as well as
+     * {@link #getDevices()} which use internal locks.
+     * <p/>
+     * For this reason, any call to this method from a method of {@link DeviceMonitor},
+     * {@link IDevice} which is also inside a synchronized block, should first synchronize on
+     * the {@link AndroidDebugBridge} lock. Access to this lock is done through {@link #getLock()}.
+     * @param device the modified <code>Client</code>.
+     * @param changeMask the mask indicating what changed in the <code>Client</code>
+     * @see #getLock()
+     */
+    void clientChanged(Client client, int changeMask) {
+        // because the listeners could remove themselves from the list while processing
+        // their event callback, we make a copy of the list and iterate on it instead of
+        // the main list.
+        // This mostly happens when the application quits.
+        IClientChangeListener[] listenersCopy = null;
+        synchronized (sLock) {
+            listenersCopy = sClientListeners.toArray(
+                    new IClientChangeListener[sClientListeners.size()]);
+
+        }
+
+        // Notify the listeners
+        for (IClientChangeListener listener : listenersCopy) {
+            // we attempt to catch any exception so that a bad listener doesn't kill our
+            // thread
+            try {
+                listener.clientChanged(client, changeMask);
+            } catch (Exception e) {
+                Log.e(DDMS, e);
+            }
+        }
+    }
+
+    /**
+     * Returns the {@link DeviceMonitor} object.
+     */
+    DeviceMonitor getDeviceMonitor() {
+        return mDeviceMonitor;
+    }
+
+    /**
+     * Starts the adb host side server.
+     * @return true if success
+     */
+    synchronized boolean startAdb() {
+        if (mAdbOsLocation == null) {
+            Log.e(ADB,
+                "Cannot start adb when AndroidDebugBridge is created without the location of adb."); //$NON-NLS-1$
+            return false;
+        }
+
+        Process proc;
+        int status = -1;
+
+        try {
+            String[] command = new String[2];
+            command[0] = mAdbOsLocation;
+            command[1] = "start-server"; //$NON-NLS-1$
+            Log.d(DDMS,
+                    String.format("Launching '%1$s %2$s' to ensure ADB is running.", //$NON-NLS-1$
+                    mAdbOsLocation, command[1]));
+            ProcessBuilder processBuilder = new ProcessBuilder(command);
+            if (DdmPreferences.getUseAdbHost()) {
+                String adbHostValue = DdmPreferences.getAdbHostValue();
+                if (adbHostValue != null && !adbHostValue.isEmpty()) {
+                    //TODO : check that the String is a valid IP address
+                    Map<String, String> env = processBuilder.environment();
+                    env.put("ADBHOST", adbHostValue);
+                }
+            }
+            proc = processBuilder.start();
+
+            ArrayList<String> errorOutput = new ArrayList<String>();
+            ArrayList<String> stdOutput = new ArrayList<String>();
+            status = grabProcessOutput(proc, errorOutput, stdOutput,
+                    false /* waitForReaders */);
+
+        } catch (IOException ioe) {
+            Log.d(DDMS, "Unable to run 'adb': " + ioe.getMessage()); //$NON-NLS-1$
+            // we'll return false;
+        } catch (InterruptedException ie) {
+            Log.d(DDMS, "Unable to run 'adb': " + ie.getMessage()); //$NON-NLS-1$
+            // we'll return false;
+        }
+
+        if (status != 0) {
+            Log.w(DDMS,
+                    "'adb start-server' failed -- run manually if necessary"); //$NON-NLS-1$
+            return false;
+        }
+
+        Log.d(DDMS, "'adb start-server' succeeded"); //$NON-NLS-1$
+
+        return true;
+    }
+
+    /**
+     * Stops the adb host side server.
+     * @return true if success
+     */
+    private synchronized boolean stopAdb() {
+        if (mAdbOsLocation == null) {
+            Log.e(ADB,
+                "Cannot stop adb when AndroidDebugBridge is created without the location of adb."); //$NON-NLS-1$
+            return false;
+        }
+
+        Process proc;
+        int status = -1;
+
+        try {
+            String[] command = new String[2];
+            command[0] = mAdbOsLocation;
+            command[1] = "kill-server"; //$NON-NLS-1$
+            proc = Runtime.getRuntime().exec(command);
+            status = proc.waitFor();
+        }
+        catch (IOException ioe) {
+            // we'll return false;
+        }
+        catch (InterruptedException ie) {
+            // we'll return false;
+        }
+
+        if (status != 0) {
+            Log.w(DDMS,
+                    "'adb kill-server' failed -- run manually if necessary"); //$NON-NLS-1$
+            return false;
+        }
+
+        Log.d(DDMS, "'adb kill-server' succeeded"); //$NON-NLS-1$
+        return true;
+    }
+
+    /**
+     * Get the stderr/stdout outputs of a process and return when the process is done.
+     * Both <b>must</b> be read or the process will block on windows.
+     * @param process The process to get the output from
+     * @param errorOutput The array to store the stderr output. cannot be null.
+     * @param stdOutput The array to store the stdout output. cannot be null.
+     * @param waitForReaders if true, this will wait for the reader threads.
+     * @return the process return code.
+     * @throws InterruptedException
+     */
+    private int grabProcessOutput(final Process process, final ArrayList<String> errorOutput,
+            final ArrayList<String> stdOutput, boolean waitForReaders)
+            throws InterruptedException {
+        assert errorOutput != null;
+        assert stdOutput != null;
+        // read the lines as they come. if null is returned, it's
+        // because the process finished
+        Thread t1 = new Thread("") { //$NON-NLS-1$
+            @Override
+            public void run() {
+                // create a buffer to read the stderr output
+                InputStreamReader is = new InputStreamReader(process.getErrorStream());
+                BufferedReader errReader = new BufferedReader(is);
+
+                try {
+                    while (true) {
+                        String line = errReader.readLine();
+                        if (line != null) {
+                            Log.e(ADB, line);
+                            errorOutput.add(line);
+                        } else {
+                            break;
+                        }
+                    }
+                } catch (IOException e) {
+                    // do nothing.
+                }
+            }
+        };
+
+        Thread t2 = new Thread("") { //$NON-NLS-1$
+            @Override
+            public void run() {
+                InputStreamReader is = new InputStreamReader(process.getInputStream());
+                BufferedReader outReader = new BufferedReader(is);
+
+                try {
+                    while (true) {
+                        String line = outReader.readLine();
+                        if (line != null) {
+                            Log.d(ADB, line);
+                            stdOutput.add(line);
+                        } else {
+                            break;
+                        }
+                    }
+                } catch (IOException e) {
+                    // do nothing.
+                }
+            }
+        };
+
+        t1.start();
+        t2.start();
+
+        // it looks like on windows process#waitFor() can return
+        // before the thread have filled the arrays, so we wait for both threads and the
+        // process itself.
+        if (waitForReaders) {
+            try {
+                t1.join();
+            } catch (InterruptedException e) {
+            }
+            try {
+                t2.join();
+            } catch (InterruptedException e) {
+            }
+        }
+
+        // get the return code from the process
+        return process.waitFor();
+    }
+
+    /**
+     * Returns the singleton lock used by this class to protect any access to the listener.
+     * <p/>
+     * This includes adding/removing listeners, but also notifying listeners of new bridges,
+     * devices, and clients.
+     */
+    static Object getLock() {
+        return sLock;
+    }
+
+    /**
+     * Instantiates sSocketAddr with the address of the host's adb process.
+     */
+    private static void initAdbSocketAddr() {
+        try {
+            int adb_port = determineAndValidateAdbPort();
+            sHostAddr = InetAddress.getByName(ADB_HOST);
+            sSocketAddr = new InetSocketAddress(sHostAddr, adb_port);
+        } catch (UnknownHostException e) {
+            // localhost should always be known.
+        }
+    }
+
+    /**
+     * Determines port where ADB is expected by looking at an env variable.
+     * <p/>
+     * The value for the environment variable ANDROID_ADB_SERVER_PORT is validated,
+     * IllegalArgumentException is thrown on illegal values.
+     * <p/>
+     * @return The port number where the host's adb should be expected or started.
+     * @throws IllegalArgumentException if ANDROID_ADB_SERVER_PORT has a non-numeric value.
+     */
+    private static int determineAndValidateAdbPort() {
+        String adb_env_var;
+        int result = ADB_PORT;
+        try {
+            adb_env_var = System.getenv(SERVER_PORT_ENV_VAR);
+
+            if (adb_env_var != null) {
+                adb_env_var = adb_env_var.trim();
+            }
+
+            if (adb_env_var != null && !adb_env_var.isEmpty()) {
+                // C tools (adb, emulator) accept hex and octal port numbers, so need to accept
+                // them too.
+                result = Integer.decode(adb_env_var);
+
+                if (result <= 0) {
+                    String errMsg = "env var " + SERVER_PORT_ENV_VAR //$NON-NLS-1$
+                            + ": must be >=0, got " //$NON-NLS-1$
+                            + System.getenv(SERVER_PORT_ENV_VAR);
+                    throw new IllegalArgumentException(errMsg);
+                }
+            }
+        } catch (NumberFormatException nfEx) {
+            String errMsg = "env var " + SERVER_PORT_ENV_VAR //$NON-NLS-1$
+                    + ": illegal value '" //$NON-NLS-1$
+                    + System.getenv(SERVER_PORT_ENV_VAR) + "'"; //$NON-NLS-1$
+            throw new IllegalArgumentException(errMsg);
+        } catch (SecurityException secEx) {
+            // A security manager has been installed that doesn't allow access to env vars.
+            // So an environment variable might have been set, but we can't tell.
+            // Let's log a warning and continue with ADB's default port.
+            // The issue is that adb would be started (by the forked process having access
+            // to the env vars) on the desired port, but within this process, we can't figure out
+            // what that port is. However, a security manager not granting access to env vars
+            // but allowing to fork is a rare and interesting configuration, so the right
+            // thing seems to be to continue using the default port, as forking is likely to
+            // fail later on in the scenario of the security manager.
+            Log.w(DDMS,
+                    "No access to env variables allowed by current security manager. " //$NON-NLS-1$
+                    + "If you've set ANDROID_ADB_SERVER_PORT: it's being ignored."); //$NON-NLS-1$
+        }
+        return result;
+    }
+
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/BadPacketException.java b/ddmlib/src/main/java/com/android/ddmlib/BadPacketException.java
new file mode 100644
index 0000000..129b312
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/BadPacketException.java
@@ -0,0 +1,35 @@
+/* //device/tools/ddms/libs/ddmlib/src/com/android/ddmlib/BadPacketException.java
+**
+** Copyright 2007, 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.ddmlib;
+
+/**
+ * Thrown if the contents of a packet are bad.
+ */
+ at SuppressWarnings("serial")
+class BadPacketException extends RuntimeException {
+    public BadPacketException()
+    {
+        super();
+    }
+
+    public BadPacketException(String msg)
+    {
+        super(msg);
+    }
+}
+
diff --git a/ddmlib/src/main/java/com/android/ddmlib/CanceledException.java b/ddmlib/src/main/java/com/android/ddmlib/CanceledException.java
new file mode 100644
index 0000000..84eda03
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/CanceledException.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2010 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.ddmlib;
+
+/**
+ * Abstract exception for exception that can be thrown when a user input cancels the action.
+ * <p/>
+ * {@link #wasCanceled()} returns whether the action was canceled because of user input.
+ *
+ */
+public abstract class CanceledException extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    CanceledException(String message) {
+        super(message);
+    }
+
+    CanceledException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    /**
+     * Returns true if the action was canceled by user input.
+     */
+    public abstract boolean wasCanceled();
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/ChunkHandler.java b/ddmlib/src/main/java/com/android/ddmlib/ChunkHandler.java
new file mode 100644
index 0000000..2cc6494
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/ChunkHandler.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import com.android.ddmlib.DebugPortManager.IDebugPortProvider;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Subclass this with a class that handles one or more chunk types.
+ */
+abstract class ChunkHandler {
+
+    public static final int CHUNK_HEADER_LEN = 8;   // 4-byte type, 4-byte len
+    public static final ByteOrder CHUNK_ORDER = ByteOrder.BIG_ENDIAN;
+
+    public static final int CHUNK_FAIL = type("FAIL");
+
+    ChunkHandler() {}
+
+    /**
+     * Client is ready.  The monitor thread calls this method on all
+     * handlers when the client is determined to be DDM-aware (usually
+     * after receiving a HELO response.)
+     *
+     * The handler can use this opportunity to initialize client-side
+     * activity.  Because there's a fair chance we'll want to send a
+     * message to the client, this method can throw an IOException.
+     */
+    abstract void clientReady(Client client) throws IOException;
+
+    /**
+     * Client has gone away.  Can be used to clean up any resources
+     * associated with this client connection.
+     */
+    abstract void clientDisconnected(Client client);
+
+    /**
+     * Handle an incoming chunk.  The data, of chunk type "type", begins
+     * at the start of "data" and continues to data.limit().
+     *
+     * If "isReply" is set, then "msgId" will be the ID of the request
+     * we sent to the client.  Otherwise, it's the ID generated by the
+     * client for this event.  Note that it's possible to receive chunks
+     * in reply packets for which we are not registered.
+     *
+     * The handler may not modify the contents of "data".
+     */
+    abstract void handleChunk(Client client, int type,
+        ByteBuffer data, boolean isReply, int msgId);
+
+    /**
+     * Handle chunks not recognized by handlers.  The handleChunk() method
+     * in sub-classes should call this if the chunk type isn't recognized.
+     */
+    protected void handleUnknownChunk(Client client, int type,
+        ByteBuffer data, boolean isReply, int msgId) {
+        if (type == CHUNK_FAIL) {
+            int errorCode, msgLen;
+            String msg;
+
+            errorCode = data.getInt();
+            msgLen = data.getInt();
+            msg = getString(data, msgLen);
+            Log.w("ddms", "WARNING: failure code=" + errorCode + " msg=" + msg);
+        } else {
+            Log.w("ddms", "WARNING: received unknown chunk " + name(type)
+                + ": len=" + data.limit() + ", reply=" + isReply
+                + ", msgId=0x" + Integer.toHexString(msgId));
+        }
+        Log.w("ddms", "         client " + client + ", handler " + this);
+    }
+
+
+    /**
+     * Utility function to copy a String out of a ByteBuffer.
+     *
+     * This is here because multiple chunk handlers can make use of it,
+     * and there's nowhere better to put it.
+     */
+    public static String getString(ByteBuffer buf, int len) {
+        char[] data = new char[len];
+        for (int i = 0; i < len; i++)
+            data[i] = buf.getChar();
+        return new String(data);
+    }
+
+    /**
+     * Utility function to copy a String into a ByteBuffer.
+     */
+    static void putString(ByteBuffer buf, String str) {
+        int len = str.length();
+        for (int i = 0; i < len; i++)
+            buf.putChar(str.charAt(i));
+    }
+
+    /**
+     * Convert a 4-character string to a 32-bit type.
+     */
+    static int type(String typeName) {
+        int val = 0;
+
+        if (typeName.length() != 4) {
+            Log.e("ddms", "Type name must be 4 letter long");
+            throw new RuntimeException("Type name must be 4 letter long");
+        }
+
+        for (int i = 0; i < 4; i++) {
+            val <<= 8;
+            val |= (byte) typeName.charAt(i);
+        }
+
+        return val;
+    }
+
+    /**
+     * Convert an integer type to a 4-character string.
+     */
+    static String name(int type) {
+        char[] ascii = new char[4];
+
+        ascii[0] = (char) ((type >> 24) & 0xff);
+        ascii[1] = (char) ((type >> 16) & 0xff);
+        ascii[2] = (char) ((type >> 8) & 0xff);
+        ascii[3] = (char) (type & 0xff);
+
+        return new String(ascii);
+    }
+
+    /**
+     * Allocate a ByteBuffer with enough space to hold the JDWP packet
+     * header and one chunk header in addition to the demands of the
+     * chunk being created.
+     *
+     * "maxChunkLen" indicates the size of the chunk contents only.
+     */
+    static ByteBuffer allocBuffer(int maxChunkLen) {
+        ByteBuffer buf =
+            ByteBuffer.allocate(JdwpPacket.JDWP_HEADER_LEN + 8 +maxChunkLen);
+        buf.order(CHUNK_ORDER);
+        return buf;
+    }
+
+    /**
+     * Return the slice of the JDWP packet buffer that holds just the
+     * chunk data.
+     */
+    static ByteBuffer getChunkDataBuf(ByteBuffer jdwpBuf) {
+        ByteBuffer slice;
+
+        assert jdwpBuf.position() == 0;
+
+        jdwpBuf.position(JdwpPacket.JDWP_HEADER_LEN + CHUNK_HEADER_LEN);
+        slice = jdwpBuf.slice();
+        slice.order(CHUNK_ORDER);
+        jdwpBuf.position(0);
+
+        return slice;
+    }
+
+    /**
+     * Write the chunk header at the start of the chunk.
+     *
+     * Pass in the byte buffer returned by JdwpPacket.getPayload().
+     */
+    static void finishChunkPacket(JdwpPacket packet, int type, int chunkLen) {
+        ByteBuffer buf = packet.getPayload();
+
+        buf.putInt(0x00, type);
+        buf.putInt(0x04, chunkLen);
+
+        packet.finishPacket(CHUNK_HEADER_LEN + chunkLen);
+    }
+
+    /**
+     * Check that the client is opened with the proper debugger port for the
+     * specified application name, and if not, reopen it.
+     * @param client
+     * @param uiThread
+     * @param appName
+     * @return
+     */
+    protected static Client checkDebuggerPortForAppName(Client client, String appName) {
+        IDebugPortProvider provider = DebugPortManager.getProvider();
+        if (provider != null) {
+            Device device = client.getDeviceImpl();
+            int newPort = provider.getPort(device, appName);
+
+            if (newPort != IDebugPortProvider.NO_STATIC_PORT &&
+                    newPort != client.getDebuggerListenPort()) {
+
+                AndroidDebugBridge bridge = AndroidDebugBridge.getBridge();
+                if (bridge != null) {
+                    DeviceMonitor deviceMonitor = bridge.getDeviceMonitor();
+                    if (deviceMonitor != null) {
+                        deviceMonitor.addClientToDropAndReopen(client, newPort);
+                        client = null;
+                    }
+                }
+            }
+        }
+
+        return client;
+    }
+}
+
diff --git a/ddmlib/src/main/java/com/android/ddmlib/Client.java b/ddmlib/src/main/java/com/android/ddmlib/Client.java
new file mode 100644
index 0000000..2aac328
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/Client.java
@@ -0,0 +1,871 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import com.android.annotations.NonNull;
+import com.android.ddmlib.ClientData.MethodProfilingStatus;
+import com.android.ddmlib.DebugPortManager.IDebugPortProvider;
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+
+import java.io.IOException;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.SocketChannel;
+import java.util.HashMap;
+
+/**
+ * This represents a single client, usually a Dalvik VM process.
+ * <p/>This class gives access to basic client information, as well as methods to perform actions
+ * on the client.
+ * <p/>More detailed information, usually updated in real time, can be access through the
+ * {@link ClientData} class. Each <code>Client</code> object has its own <code>ClientData</code>
+ * accessed through {@link #getClientData()}.
+ */
+public class Client {
+
+    private static final int SERVER_PROTOCOL_VERSION = 1;
+
+    /** Client change bit mask: application name change */
+    public static final int CHANGE_NAME                       = 0x0001;
+    /** Client change bit mask: debugger status change */
+    public static final int CHANGE_DEBUGGER_STATUS            = 0x0002;
+    /** Client change bit mask: debugger port change */
+    public static final int CHANGE_PORT                       = 0x0004;
+    /** Client change bit mask: thread update flag change */
+    public static final int CHANGE_THREAD_MODE                = 0x0008;
+    /** Client change bit mask: thread data updated */
+    public static final int CHANGE_THREAD_DATA                = 0x0010;
+    /** Client change bit mask: heap update flag change */
+    public static final int CHANGE_HEAP_MODE                  = 0x0020;
+    /** Client change bit mask: head data updated */
+    public static final int CHANGE_HEAP_DATA                  = 0x0040;
+    /** Client change bit mask: native heap data updated */
+    public static final int CHANGE_NATIVE_HEAP_DATA           = 0x0080;
+    /** Client change bit mask: thread stack trace updated */
+    public static final int CHANGE_THREAD_STACKTRACE          = 0x0100;
+    /** Client change bit mask: allocation information updated */
+    public static final int CHANGE_HEAP_ALLOCATIONS           = 0x0200;
+    /** Client change bit mask: allocation information updated */
+    public static final int CHANGE_HEAP_ALLOCATION_STATUS     = 0x0400;
+    /** Client change bit mask: allocation information updated */
+    public static final int CHANGE_METHOD_PROFILING_STATUS    = 0x0800;
+
+    /** Client change bit mask: combination of {@link Client#CHANGE_NAME},
+     * {@link Client#CHANGE_DEBUGGER_STATUS}, and {@link Client#CHANGE_PORT}.
+     */
+    public static final int CHANGE_INFO = CHANGE_NAME | CHANGE_DEBUGGER_STATUS | CHANGE_PORT;
+
+    private SocketChannel mChan;
+
+    // debugger we're associated with, if any
+    private Debugger mDebugger;
+    private int mDebuggerListenPort;
+
+    // list of IDs for requests we have sent to the client
+    private HashMap<Integer,ChunkHandler> mOutstandingReqs;
+
+    // chunk handlers stash state data in here
+    private ClientData mClientData;
+
+    // User interface state.  Changing the value causes a message to be
+    // sent to the client.
+    private boolean mThreadUpdateEnabled;
+    private boolean mHeapUpdateEnabled;
+
+    /*
+     * Read/write buffers.  We can get large quantities of data from the
+     * client, e.g. the response to a "give me the list of all known classes"
+     * request from the debugger.  Requests from the debugger, and from us,
+     * are much smaller.
+     *
+     * Pass-through debugger traffic is sent without copying.  "mWriteBuffer"
+     * is only used for data generated within Client.
+     */
+    private static final int INITIAL_BUF_SIZE = 2*1024;
+    private static final int MAX_BUF_SIZE = 200*1024*1024;
+    private ByteBuffer mReadBuffer;
+
+    private static final int WRITE_BUF_SIZE = 256;
+    private ByteBuffer mWriteBuffer;
+
+    private Device mDevice;
+
+    private int mConnState;
+
+    private static final int ST_INIT         = 1;
+    private static final int ST_NOT_JDWP     = 2;
+    private static final int ST_AWAIT_SHAKE  = 10;
+    private static final int ST_NEED_DDM_PKT = 11;
+    private static final int ST_NOT_DDM      = 12;
+    private static final int ST_READY        = 13;
+    private static final int ST_ERROR        = 20;
+    private static final int ST_DISCONNECTED = 21;
+
+
+    /**
+     * Create an object for a new client connection.
+     *
+     * @param device the device this client belongs to
+     * @param chan the connected {@link SocketChannel}.
+     * @param pid the client pid.
+     */
+    Client(Device device, SocketChannel chan, int pid) {
+        mDevice = device;
+        mChan = chan;
+
+        mReadBuffer = ByteBuffer.allocate(INITIAL_BUF_SIZE);
+        mWriteBuffer = ByteBuffer.allocate(WRITE_BUF_SIZE);
+
+        mOutstandingReqs = new HashMap<Integer,ChunkHandler>();
+
+        mConnState = ST_INIT;
+
+        mClientData = new ClientData(pid);
+
+        mThreadUpdateEnabled = DdmPreferences.getInitialThreadUpdate();
+        mHeapUpdateEnabled = DdmPreferences.getInitialHeapUpdate();
+    }
+
+    /**
+     * Returns a string representation of the {@link Client} object.
+     */
+    @Override
+    public String toString() {
+        return "[Client pid: " + mClientData.getPid() + "]";
+    }
+
+    /**
+     * Returns the {@link IDevice} on which this Client is running.
+     */
+    public IDevice getDevice() {
+        return mDevice;
+    }
+
+    /** Returns the {@link Device} on which this Client is running.
+     */
+    Device getDeviceImpl() {
+        return mDevice;
+    }
+
+    /**
+     * Returns the debugger port for this client.
+     */
+    public int getDebuggerListenPort() {
+        return mDebuggerListenPort;
+    }
+
+    /**
+     * Returns <code>true</code> if the client VM is DDM-aware.
+     *
+     * Calling here is only allowed after the connection has been
+     * established.
+     */
+    public boolean isDdmAware() {
+        switch (mConnState) {
+            case ST_INIT:
+            case ST_NOT_JDWP:
+            case ST_AWAIT_SHAKE:
+            case ST_NEED_DDM_PKT:
+            case ST_NOT_DDM:
+            case ST_ERROR:
+            case ST_DISCONNECTED:
+                return false;
+            case ST_READY:
+                return true;
+            default:
+                assert false;
+                return false;
+        }
+    }
+
+    /**
+     * Returns <code>true</code> if a debugger is currently attached to the client.
+     */
+    public boolean isDebuggerAttached() {
+        return mDebugger.isDebuggerAttached();
+    }
+
+    /**
+     * Return the Debugger object associated with this client.
+     */
+    Debugger getDebugger() {
+        return mDebugger;
+    }
+
+    /**
+     * Returns the {@link ClientData} object containing this client information.
+     */
+    @NonNull
+    public ClientData getClientData() {
+        return mClientData;
+    }
+
+    /**
+     * Forces the client to execute its garbage collector.
+     */
+    public void executeGarbageCollector() {
+        try {
+            HandleHeap.sendHPGC(this);
+        } catch (IOException ioe) {
+            Log.w("ddms", "Send of HPGC message failed");
+            // ignore
+        }
+    }
+
+    /**
+     * Makes the VM dump an HPROF file
+     */
+    public void dumpHprof() {
+        boolean canStream = mClientData.hasFeature(ClientData.FEATURE_HPROF_STREAMING);
+        try {
+            if (canStream) {
+                HandleHeap.sendHPDS(this);
+            } else {
+                String file = "/sdcard/" + mClientData.getClientDescription().replaceAll(
+                        "\\:.*", "") + ".hprof";
+                HandleHeap.sendHPDU(this, file);
+            }
+        } catch (IOException e) {
+            Log.w("ddms", "Send of HPDU message failed");
+            // ignore
+        }
+    }
+
+    public void toggleMethodProfiling() {
+        boolean canStream = mClientData.hasFeature(ClientData.FEATURE_PROFILING_STREAMING);
+        try {
+            if (mClientData.getMethodProfilingStatus() == MethodProfilingStatus.ON) {
+                if (canStream) {
+                    HandleProfiling.sendMPSE(this);
+                } else {
+                    HandleProfiling.sendMPRE(this);
+                }
+            } else {
+                int bufferSize = DdmPreferences.getProfilerBufferSizeMb() * 1024 * 1024;
+                if (canStream) {
+                    HandleProfiling.sendMPSS(this, bufferSize, 0 /*flags*/);
+                } else {
+                    String file = "/sdcard/" +
+                        mClientData.getClientDescription().replaceAll("\\:.*", "") +
+                        DdmConstants.DOT_TRACE;
+                    HandleProfiling.sendMPRS(this, file, bufferSize, 0 /*flags*/);
+                }
+            }
+        } catch (IOException e) {
+            Log.w("ddms", "Toggle method profiling failed");
+            // ignore
+        }
+    }
+
+    public boolean startOpenGlTracing() {
+        boolean canTraceOpenGl = mClientData.hasFeature(ClientData.FEATURE_OPENGL_TRACING);
+        if (!canTraceOpenGl) {
+            return false;
+        }
+
+        try {
+            HandleViewDebug.sendStartGlTracing(this);
+            return true;
+        } catch (IOException e) {
+            Log.w("ddms", "Start OpenGL Tracing failed");
+            return false;
+        }
+    }
+
+    public boolean stopOpenGlTracing() {
+        boolean canTraceOpenGl = mClientData.hasFeature(ClientData.FEATURE_OPENGL_TRACING);
+        if (!canTraceOpenGl) {
+            return false;
+        }
+
+        try {
+            HandleViewDebug.sendStopGlTracing(this);
+            return true;
+        } catch (IOException e) {
+            Log.w("ddms", "Stop OpenGL Tracing failed");
+            return false;
+        }
+    }
+
+    /**
+     * Sends a request to the VM to send the enable status of the method profiling.
+     * This is asynchronous.
+     * <p/>The allocation status can be accessed by {@link ClientData#getAllocationStatus()}.
+     * The notification that the new status is available will be received through
+     * {@link IClientChangeListener#clientChanged(Client, int)} with a <code>changeMask</code>
+     * containing the mask {@link #CHANGE_HEAP_ALLOCATION_STATUS}.
+     */
+    public void requestMethodProfilingStatus() {
+        try {
+            HandleHeap.sendREAQ(this);
+        } catch (IOException e) {
+            Log.e("ddmlib", e);
+        }
+    }
+
+
+    /**
+     * Enables or disables the thread update.
+     * <p/>If <code>true</code> the VM will be able to send thread information. Thread information
+     * must be requested with {@link #requestThreadUpdate()}.
+     * @param enabled the enable flag.
+     */
+    public void setThreadUpdateEnabled(boolean enabled) {
+        mThreadUpdateEnabled = enabled;
+        if (!enabled) {
+            mClientData.clearThreads();
+        }
+
+        try {
+            HandleThread.sendTHEN(this, enabled);
+        } catch (IOException ioe) {
+            // ignore it here; client will clean up shortly
+            ioe.printStackTrace();
+        }
+
+        update(CHANGE_THREAD_MODE);
+    }
+
+    /**
+     * Returns whether the thread update is enabled.
+     */
+    public boolean isThreadUpdateEnabled() {
+        return mThreadUpdateEnabled;
+    }
+
+    /**
+     * Sends a thread update request. This is asynchronous.
+     * <p/>The thread info can be accessed by {@link ClientData#getThreads()}. The notification
+     * that the new data is available will be received through
+     * {@link IClientChangeListener#clientChanged(Client, int)} with a <code>changeMask</code>
+     * containing the mask {@link #CHANGE_THREAD_DATA}.
+     */
+    public void requestThreadUpdate() {
+        HandleThread.requestThreadUpdate(this);
+    }
+
+    /**
+     * Sends a thread stack trace update request. This is asynchronous.
+     * <p/>The thread info can be accessed by {@link ClientData#getThreads()} and
+     * {@link ThreadInfo#getStackTrace()}.
+     * <p/>The notification that the new data is available
+     * will be received through {@link IClientChangeListener#clientChanged(Client, int)}
+     * with a <code>changeMask</code> containing the mask {@link #CHANGE_THREAD_STACKTRACE}.
+     */
+    public void requestThreadStackTrace(int threadId) {
+        HandleThread.requestThreadStackCallRefresh(this, threadId);
+    }
+
+    /**
+     * Enables or disables the heap update.
+     * <p/>If <code>true</code>, any GC will cause the client to send its heap information.
+     * <p/>The heap information can be accessed by {@link ClientData#getVmHeapData()}.
+     * <p/>The notification that the new data is available
+     * will be received through {@link IClientChangeListener#clientChanged(Client, int)}
+     * with a <code>changeMask</code> containing the value {@link #CHANGE_HEAP_DATA}.
+     * @param enabled the enable flag
+     */
+    public void setHeapUpdateEnabled(boolean enabled) {
+        mHeapUpdateEnabled = enabled;
+
+        try {
+            HandleHeap.sendHPIF(this,
+                    enabled ? HandleHeap.HPIF_WHEN_EVERY_GC : HandleHeap.HPIF_WHEN_NEVER);
+
+            HandleHeap.sendHPSG(this,
+                    enabled ? HandleHeap.WHEN_GC : HandleHeap.WHEN_DISABLE,
+                    HandleHeap.WHAT_MERGE);
+        } catch (IOException ioe) {
+            // ignore it here; client will clean up shortly
+        }
+
+        update(CHANGE_HEAP_MODE);
+    }
+
+    /**
+     * Returns whether the heap update is enabled.
+     * @see #setHeapUpdateEnabled(boolean)
+     */
+    public boolean isHeapUpdateEnabled() {
+        return mHeapUpdateEnabled;
+    }
+
+    /**
+     * Sends a native heap update request. this is asynchronous.
+     * <p/>The native heap info can be accessed by {@link ClientData#getNativeAllocationList()}.
+     * The notification that the new data is available will be received through
+     * {@link IClientChangeListener#clientChanged(Client, int)} with a <code>changeMask</code>
+     * containing the mask {@link #CHANGE_NATIVE_HEAP_DATA}.
+     */
+    public boolean requestNativeHeapInformation() {
+        try {
+            HandleNativeHeap.sendNHGT(this);
+            return true;
+        } catch (IOException e) {
+            Log.e("ddmlib", e);
+        }
+
+        return false;
+    }
+
+    /**
+     * Enables or disables the Allocation tracker for this client.
+     * <p/>If enabled, the VM will start tracking allocation information. A call to
+     * {@link #requestAllocationDetails()} will make the VM sends the information about all the
+     * allocations that happened between the enabling and the request.
+     * @param enable
+     * @see #requestAllocationDetails()
+     */
+    public void enableAllocationTracker(boolean enable) {
+        try {
+            HandleHeap.sendREAE(this, enable);
+        } catch (IOException e) {
+            Log.e("ddmlib", e);
+        }
+    }
+
+    /**
+     * Sends a request to the VM to send the enable status of the allocation tracking.
+     * This is asynchronous.
+     * <p/>The allocation status can be accessed by {@link ClientData#getAllocationStatus()}.
+     * The notification that the new status is available will be received through
+     * {@link IClientChangeListener#clientChanged(Client, int)} with a <code>changeMask</code>
+     * containing the mask {@link #CHANGE_HEAP_ALLOCATION_STATUS}.
+     */
+    public void requestAllocationStatus() {
+        try {
+            HandleHeap.sendREAQ(this);
+        } catch (IOException e) {
+            Log.e("ddmlib", e);
+        }
+    }
+
+    /**
+     * Sends a request to the VM to send the information about all the allocations that have
+     * happened since the call to {@link #enableAllocationTracker(boolean)} with <var>enable</var>
+     * set to <code>null</code>. This is asynchronous.
+     * <p/>The allocation information can be accessed by {@link ClientData#getAllocations()}.
+     * The notification that the new data is available will be received through
+     * {@link IClientChangeListener#clientChanged(Client, int)} with a <code>changeMask</code>
+     * containing the mask {@link #CHANGE_HEAP_ALLOCATIONS}.
+     */
+    public void requestAllocationDetails() {
+        try {
+            HandleHeap.sendREAL(this);
+        } catch (IOException e) {
+            Log.e("ddmlib", e);
+        }
+    }
+
+    /**
+     * Sends a kill message to the VM.
+     */
+    public void kill() {
+        try {
+            HandleExit.sendEXIT(this, 1);
+        } catch (IOException ioe) {
+            Log.w("ddms", "Send of EXIT message failed");
+            // ignore
+        }
+    }
+
+    /**
+     * Registers the client with a Selector.
+     */
+    void register(Selector sel) throws IOException {
+        if (mChan != null) {
+            mChan.register(sel, SelectionKey.OP_READ, this);
+        }
+    }
+
+    /**
+     * Sets the client to accept debugger connection on the "selected debugger port".
+     *
+     * @see AndroidDebugBridge#setSelectedClient(Client)
+     * @see DdmPreferences#setSelectedDebugPort(int)
+     */
+    public void setAsSelectedClient() {
+        MonitorThread monitorThread = MonitorThread.getInstance();
+        if (monitorThread != null) {
+            monitorThread.setSelectedClient(this);
+        }
+    }
+
+    /**
+     * Returns whether this client is the current selected client, accepting debugger connection
+     * on the "selected debugger port".
+     *
+     * @see #setAsSelectedClient()
+     * @see AndroidDebugBridge#setSelectedClient(Client)
+     * @see DdmPreferences#setSelectedDebugPort(int)
+     */
+    public boolean isSelectedClient() {
+        MonitorThread monitorThread = MonitorThread.getInstance();
+        if (monitorThread != null) {
+            return monitorThread.getSelectedClient() == this;
+        }
+
+        return false;
+    }
+
+    /**
+     * Tell the client to open a server socket channel and listen for
+     * connections on the specified port.
+     */
+    void listenForDebugger(int listenPort) throws IOException {
+        mDebuggerListenPort = listenPort;
+        mDebugger = new Debugger(this, listenPort);
+    }
+
+    /**
+     * Initiate the JDWP handshake.
+     *
+     * On failure, closes the socket and returns false.
+     */
+    boolean sendHandshake() {
+        assert mWriteBuffer.position() == 0;
+
+        try {
+            // assume write buffer can hold 14 bytes
+            JdwpPacket.putHandshake(mWriteBuffer);
+            int expectedLen = mWriteBuffer.position();
+            mWriteBuffer.flip();
+            if (mChan.write(mWriteBuffer) != expectedLen)
+                throw new IOException("partial handshake write");
+        }
+        catch (IOException ioe) {
+            Log.e("ddms-client", "IO error during handshake: " + ioe.getMessage());
+            mConnState = ST_ERROR;
+            close(true /* notify */);
+            return false;
+        }
+        finally {
+            mWriteBuffer.clear();
+        }
+
+        mConnState = ST_AWAIT_SHAKE;
+
+        return true;
+    }
+
+
+    /**
+     * Send a non-DDM packet to the client.
+     *
+     * Equivalent to sendAndConsume(packet, null).
+     */
+    void sendAndConsume(JdwpPacket packet) throws IOException {
+        sendAndConsume(packet, null);
+    }
+
+    /**
+     * Send a DDM packet to the client.
+     *
+     * Ideally, we can do this with a single channel write.  If that doesn't
+     * happen, we have to prevent anybody else from writing to the channel
+     * until this packet completes, so we synchronize on the channel.
+     *
+     * Another goal is to avoid unnecessary buffer copies, so we write
+     * directly out of the JdwpPacket's ByteBuffer.
+     */
+    void sendAndConsume(JdwpPacket packet, ChunkHandler replyHandler)
+        throws IOException {
+
+        if (mChan == null) {
+            // can happen for e.g. THST packets
+            Log.v("ddms", "Not sending packet -- client is closed");
+            return;
+        }
+
+        if (replyHandler != null) {
+            /*
+             * Add the ID to the list of outstanding requests.  We have to do
+             * this before sending the packet, in case the response comes back
+             * before our thread returns from the packet-send function.
+             */
+            addRequestId(packet.getId(), replyHandler);
+        }
+
+        synchronized (mChan) {
+            try {
+                packet.writeAndConsume(mChan);
+            }
+            catch (IOException ioe) {
+                removeRequestId(packet.getId());
+                throw ioe;
+            }
+        }
+    }
+
+    /**
+     * Forward the packet to the debugger (if still connected to one).
+     *
+     * Consumes the packet.
+     */
+    void forwardPacketToDebugger(JdwpPacket packet)
+        throws IOException {
+
+        Debugger dbg = mDebugger;
+
+        if (dbg == null) {
+            Log.d("ddms", "Discarding packet");
+            packet.consume();
+        } else {
+            dbg.sendAndConsume(packet);
+        }
+    }
+
+    /**
+     * Read data from our channel.
+     *
+     * This is called when data is known to be available, and we don't yet
+     * have a full packet in the buffer.  If the buffer is at capacity,
+     * expand it.
+     */
+    void read()
+        throws IOException, BufferOverflowException {
+
+        int count;
+
+        if (mReadBuffer.position() == mReadBuffer.capacity()) {
+            if (mReadBuffer.capacity() * 2 > MAX_BUF_SIZE) {
+                Log.e("ddms", "Exceeded MAX_BUF_SIZE!");
+                throw new BufferOverflowException();
+            }
+            Log.d("ddms", "Expanding read buffer to "
+                + mReadBuffer.capacity() * 2);
+
+            ByteBuffer newBuffer = ByteBuffer.allocate(mReadBuffer.capacity() * 2);
+
+            // copy entire buffer to new buffer
+            mReadBuffer.position(0);
+            newBuffer.put(mReadBuffer);  // leaves "position" at end of copied
+
+            mReadBuffer = newBuffer;
+        }
+
+        count = mChan.read(mReadBuffer);
+        if (count < 0)
+            throw new IOException("read failed");
+
+        if (Log.Config.LOGV) Log.v("ddms", "Read " + count + " bytes from " + this);
+        //Log.hexDump("ddms", Log.DEBUG, mReadBuffer.array(),
+        //    mReadBuffer.arrayOffset(), mReadBuffer.position());
+    }
+
+    /**
+     * Return information for the first full JDWP packet in the buffer.
+     *
+     * If we don't yet have a full packet, return null.
+     *
+     * If we haven't yet received the JDWP handshake, we watch for it here
+     * and consume it without admitting to have done so.  Upon receipt
+     * we send out the "HELO" message, which is why this can throw an
+     * IOException.
+     */
+    JdwpPacket getJdwpPacket() throws IOException {
+
+        /*
+         * On entry, the data starts at offset 0 and ends at "position".
+         * "limit" is set to the buffer capacity.
+         */
+        if (mConnState == ST_AWAIT_SHAKE) {
+            /*
+             * The first thing we get from the client is a response to our
+             * handshake.  It doesn't look like a packet, so we have to
+             * handle it specially.
+             */
+            int result;
+
+            result = JdwpPacket.findHandshake(mReadBuffer);
+            //Log.v("ddms", "findHand: " + result);
+            switch (result) {
+                case JdwpPacket.HANDSHAKE_GOOD:
+                    Log.d("ddms",
+                        "Good handshake from client, sending HELO to " + mClientData.getPid());
+                    JdwpPacket.consumeHandshake(mReadBuffer);
+                    mConnState = ST_NEED_DDM_PKT;
+                    HandleHello.sendHelloCommands(this, SERVER_PROTOCOL_VERSION);
+                    // see if we have another packet in the buffer
+                    return getJdwpPacket();
+                case JdwpPacket.HANDSHAKE_BAD:
+                    Log.d("ddms", "Bad handshake from client");
+                    if (MonitorThread.getInstance().getRetryOnBadHandshake()) {
+                        // we should drop the client, but also attempt to reopen it.
+                        // This is done by the DeviceMonitor.
+                        mDevice.getMonitor().addClientToDropAndReopen(this,
+                                IDebugPortProvider.NO_STATIC_PORT);
+                    } else {
+                        // mark it as bad, close the socket, and don't retry
+                        mConnState = ST_NOT_JDWP;
+                        close(true /* notify */);
+                    }
+                    break;
+                case JdwpPacket.HANDSHAKE_NOTYET:
+                    Log.d("ddms", "No handshake from client yet.");
+                    break;
+                default:
+                    Log.e("ddms", "Unknown packet while waiting for client handshake");
+            }
+            return null;
+        } else if (mConnState == ST_NEED_DDM_PKT ||
+            mConnState == ST_NOT_DDM ||
+            mConnState == ST_READY) {
+            /*
+             * Normal packet traffic.
+             */
+            if (mReadBuffer.position() != 0) {
+                if (Log.Config.LOGV) Log.v("ddms",
+                    "Checking " + mReadBuffer.position() + " bytes");
+            }
+            return JdwpPacket.findPacket(mReadBuffer);
+        } else {
+            /*
+             * Not expecting data when in this state.
+             */
+            Log.e("ddms", "Receiving data in state = " + mConnState);
+        }
+
+        return null;
+    }
+
+    /*
+     * Add the specified ID to the list of request IDs for which we await
+     * a response.
+     */
+    private void addRequestId(int id, ChunkHandler handler) {
+        synchronized (mOutstandingReqs) {
+            if (Log.Config.LOGV) Log.v("ddms",
+                "Adding req 0x" + Integer.toHexString(id) +" to set");
+            mOutstandingReqs.put(id, handler);
+        }
+    }
+
+    /*
+     * Remove the specified ID from the list, if present.
+     */
+    void removeRequestId(int id) {
+        synchronized (mOutstandingReqs) {
+            if (Log.Config.LOGV) Log.v("ddms",
+                "Removing req 0x" + Integer.toHexString(id) + " from set");
+            mOutstandingReqs.remove(id);
+        }
+
+        //Log.w("ddms", "Request " + Integer.toHexString(id)
+        //    + " could not be removed from " + this);
+    }
+
+    /**
+     * Determine whether this is a response to a request we sent earlier.
+     * If so, return the ChunkHandler responsible.
+     */
+    ChunkHandler isResponseToUs(int id) {
+
+        synchronized (mOutstandingReqs) {
+            ChunkHandler handler = mOutstandingReqs.get(id);
+            if (handler != null) {
+                if (Log.Config.LOGV) Log.v("ddms",
+                    "Found 0x" + Integer.toHexString(id)
+                    + " in request set - " + handler);
+                return handler;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * An earlier request resulted in a failure.  This is the expected
+     * response to a HELO message when talking to a non-DDM client.
+     */
+    void packetFailed(JdwpPacket reply) {
+        if (mConnState == ST_NEED_DDM_PKT) {
+            Log.d("ddms", "Marking " + this + " as non-DDM client");
+            mConnState = ST_NOT_DDM;
+        } else if (mConnState != ST_NOT_DDM) {
+            Log.w("ddms", "WEIRD: got JDWP failure packet on DDM req");
+        }
+    }
+
+    /**
+     * The MonitorThread calls this when it sees a DDM request or reply.
+     * If we haven't seen a DDM packet before, we advance the state to
+     * ST_READY and return "false".  Otherwise, just return true.
+     *
+     * The idea is to let the MonitorThread know when we first see a DDM
+     * packet, so we can send a broadcast to the handlers when a client
+     * connection is made.  This method is synchronized so that we only
+     * send the broadcast once.
+     */
+    synchronized boolean ddmSeen() {
+        if (mConnState == ST_NEED_DDM_PKT) {
+            mConnState = ST_READY;
+            return false;
+        } else if (mConnState != ST_READY) {
+            Log.w("ddms", "WEIRD: in ddmSeen with state=" + mConnState);
+        }
+        return true;
+    }
+
+    /**
+     * Close the client socket channel.  If there is a debugger associated
+     * with us, close that too.
+     *
+     * Closing a channel automatically unregisters it from the selector.
+     * However, we have to iterate through the selector loop before it
+     * actually lets them go and allows the file descriptors to close.
+     * The caller is expected to manage that.
+     * @param notify Whether or not to notify the listeners of a change.
+     */
+    void close(boolean notify) {
+        Log.d("ddms", "Closing " + this.toString());
+
+        mOutstandingReqs.clear();
+
+        try {
+            if (mChan != null) {
+                mChan.close();
+                mChan = null;
+            }
+
+            if (mDebugger != null) {
+                mDebugger.close();
+                mDebugger = null;
+            }
+        }
+        catch (IOException ioe) {
+            Log.w("ddms", "failed to close " + this);
+            // swallow it -- not much else to do
+        }
+
+        mDevice.removeClient(this, notify);
+    }
+
+    /**
+     * Returns whether this {@link Client} has a valid connection to the application VM.
+     */
+    public boolean isValid() {
+        return mChan != null;
+    }
+
+    void update(int changeMask) {
+        mDevice.update(this, changeMask);
+    }
+}
+
diff --git a/ddmlib/src/main/java/com/android/ddmlib/ClientData.java b/ddmlib/src/main/java/com/android/ddmlib/ClientData.java
new file mode 100644
index 0000000..1e72523
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/ClientData.java
@@ -0,0 +1,732 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import com.android.ddmlib.HeapSegment.HeapSegmentElement;
+
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+
+/**
+ * Contains the data of a {@link Client}.
+ */
+public class ClientData {
+    /* This is a place to stash data associated with a Client, such as thread
+    * states or heap data.  ClientData maps 1:1 to Client, but it's a little
+    * cleaner if we separate the data out.
+    *
+    * Message handlers are welcome to stash arbitrary data here.
+    *
+    * IMPORTANT: The data here is written by HandleFoo methods and read by
+    * FooPanel methods, which run in different threads.  All non-trivial
+    * access should be synchronized against the ClientData object.
+    */
+
+
+    /** Temporary name of VM to be ignored. */
+    private static final String PRE_INITIALIZED = "<pre-initialized>"; //$NON-NLS-1$
+
+    public static enum DebuggerStatus {
+        /** Debugger connection status: not waiting on one, not connected to one, but accepting
+         * new connections. This is the default value. */
+        DEFAULT,
+        /**
+         * Debugger connection status: the application's VM is paused, waiting for a debugger to
+         * connect to it before resuming. */
+        WAITING,
+        /** Debugger connection status : Debugger is connected */
+        ATTACHED,
+        /** Debugger connection status: The listening port for debugger connection failed to listen.
+         * No debugger will be able to connect. */
+        ERROR
+    }
+
+    public static enum AllocationTrackingStatus {
+        /**
+         * Allocation tracking status: unknown.
+         * <p/>This happens right after a {@link Client} is discovered
+         * by the {@link AndroidDebugBridge}, and before the {@link Client} answered the query
+         * regarding its allocation tracking status.
+         * @see Client#requestAllocationStatus()
+         */
+        UNKNOWN,
+        /** Allocation tracking status: the {@link Client} is not tracking allocations. */
+        OFF,
+        /** Allocation tracking status: the {@link Client} is tracking allocations. */
+        ON
+    }
+
+    public static enum MethodProfilingStatus {
+        /**
+         * Method profiling status: unknown.
+         * <p/>This happens right after a {@link Client} is discovered
+         * by the {@link AndroidDebugBridge}, and before the {@link Client} answered the query
+         * regarding its method profiling status.
+         * @see Client#requestMethodProfilingStatus()
+         */
+        UNKNOWN,
+        /** Method profiling status: the {@link Client} is not profiling method calls. */
+        OFF,
+        /** Method profiling status: the {@link Client} is profiling method calls. */
+        ON
+    }
+
+    /**
+     * Name of the value representing the max size of the heap, in the {@link Map} returned by
+     * {@link #getVmHeapInfo(int)}
+     */
+    public static final String HEAP_MAX_SIZE_BYTES = "maxSizeInBytes"; //$NON-NLS-1$
+    /**
+     * Name of the value representing the size of the heap, in the {@link Map} returned by
+     * {@link #getVmHeapInfo(int)}
+     */
+    public static final String HEAP_SIZE_BYTES = "sizeInBytes"; //$NON-NLS-1$
+    /**
+     * Name of the value representing the number of allocated bytes of the heap, in the
+     * {@link Map} returned by {@link #getVmHeapInfo(int)}
+     */
+    public static final String HEAP_BYTES_ALLOCATED = "bytesAllocated"; //$NON-NLS-1$
+    /**
+     * Name of the value representing the number of objects in the heap, in the {@link Map}
+     * returned by {@link #getVmHeapInfo(int)}
+     */
+    public static final String HEAP_OBJECTS_ALLOCATED = "objectsAllocated"; //$NON-NLS-1$
+
+    /**
+     * String for feature enabling starting/stopping method profiling
+     * @see #hasFeature(String)
+     */
+    public static final String FEATURE_PROFILING = "method-trace-profiling"; //$NON-NLS-1$
+
+    /**
+     * String for feature enabling direct streaming of method profiling data
+     * @see #hasFeature(String)
+     */
+    public static final String FEATURE_PROFILING_STREAMING = "method-trace-profiling-streaming"; //$NON-NLS-1$
+
+    /**
+     * String for feature indicating support for tracing OpenGL calls.
+     * @see #hasFeature(String)
+     */
+    public static final String FEATURE_OPENGL_TRACING = "opengl-tracing"; //$NON-NLS-1$
+
+    /**
+     * String for feature indicating support for providing view hierarchy.
+     * @see #hasFeature(String)
+     */
+    public static final String FEATURE_VIEW_HIERARCHY = "view-hierarchy"; //$NON-NLS-1$
+
+    /**
+     * String for feature allowing to dump hprof files
+     * @see #hasFeature(String)
+     */
+    public static final String FEATURE_HPROF = "hprof-heap-dump"; //$NON-NLS-1$
+
+    /**
+     * String for feature allowing direct streaming of hprof dumps
+     * @see #hasFeature(String)
+     */
+    public static final String FEATURE_HPROF_STREAMING = "hprof-heap-dump-streaming"; //$NON-NLS-1$
+
+    private static IHprofDumpHandler sHprofDumpHandler;
+    private static IMethodProfilingHandler sMethodProfilingHandler;
+
+    // is this a DDM-aware client?
+    private boolean mIsDdmAware;
+
+    // the client's process ID
+    private final int mPid;
+
+    // Java VM identification string
+    private String mVmIdentifier;
+
+    // client's self-description
+    private String mClientDescription;
+
+    // client's user id (on device in a multi user environment)
+    private int mUserId;
+
+    // client's user id is valid
+    private boolean mValidUserId;
+
+    // how interested are we in a debugger?
+    private DebuggerStatus mDebuggerInterest;
+
+    // List of supported features by the client.
+    private final HashSet<String> mFeatures = new HashSet<String>();
+
+    // Thread tracking (THCR, THDE).
+    private TreeMap<Integer,ThreadInfo> mThreadMap;
+
+    /** VM Heap data */
+    private final HeapData mHeapData = new HeapData();
+    /** Native Heap data */
+    private final HeapData mNativeHeapData = new HeapData();
+
+    private HashMap<Integer, HashMap<String, Long>> mHeapInfoMap =
+            new HashMap<Integer, HashMap<String, Long>>();
+
+
+    /** library map info. Stored here since the backtrace data
+     * is computed on a need to display basis.
+     */
+    private ArrayList<NativeLibraryMapInfo> mNativeLibMapInfo =
+        new ArrayList<NativeLibraryMapInfo>();
+
+    /** Native Alloc info list */
+    private ArrayList<NativeAllocationInfo> mNativeAllocationList =
+        new ArrayList<NativeAllocationInfo>();
+    private int mNativeTotalMemory;
+
+    private AllocationInfo[] mAllocations;
+    private AllocationTrackingStatus mAllocationStatus = AllocationTrackingStatus.UNKNOWN;
+
+    private String mPendingHprofDump;
+
+    private MethodProfilingStatus mProfilingStatus = MethodProfilingStatus.UNKNOWN;
+    private String mPendingMethodProfiling;
+
+    /**
+     * Heap Information.
+     * <p/>The heap is composed of several {@link HeapSegment} objects.
+     * <p/>A call to {@link #isHeapDataComplete()} will indicate if the segments (available through
+     * {@link #getHeapSegments()}) represent the full heap.
+     */
+    public static class HeapData {
+        private TreeSet<HeapSegment> mHeapSegments = new TreeSet<HeapSegment>();
+        private boolean mHeapDataComplete = false;
+        private byte[] mProcessedHeapData;
+        private Map<Integer, ArrayList<HeapSegmentElement>> mProcessedHeapMap;
+
+        /**
+         * Abandon the current list of heap segments.
+         */
+        public synchronized void clearHeapData() {
+            /* Abandon the old segments instead of just calling .clear().
+             * This lets the user hold onto the old set if it wants to.
+             */
+            mHeapSegments = new TreeSet<HeapSegment>();
+            mHeapDataComplete = false;
+        }
+
+        /**
+         * Add raw HPSG chunk data to the list of heap segments.
+         *
+         * @param data The raw data from an HPSG chunk.
+         */
+        synchronized void addHeapData(ByteBuffer data) {
+            HeapSegment hs;
+
+            if (mHeapDataComplete) {
+                clearHeapData();
+            }
+
+            try {
+                hs = new HeapSegment(data);
+            } catch (BufferUnderflowException e) {
+                System.err.println("Discarding short HPSG data (length " + data.limit() + ")");
+                return;
+            }
+
+            mHeapSegments.add(hs);
+        }
+
+        /**
+         * Called when all heap data has arrived.
+         */
+        synchronized void sealHeapData() {
+            mHeapDataComplete = true;
+        }
+
+        /**
+         * Returns whether the heap data has been sealed.
+         */
+        public boolean isHeapDataComplete() {
+            return mHeapDataComplete;
+        }
+
+        /**
+         * Get the collected heap data, if sealed.
+         *
+         * @return The list of heap segments if the heap data has been sealed, or null if it hasn't.
+         */
+        public Collection<HeapSegment> getHeapSegments() {
+            if (isHeapDataComplete()) {
+                return mHeapSegments;
+            }
+            return null;
+        }
+
+        /**
+         * Sets the processed heap data.
+         *
+         * @param heapData The new heap data (can be null)
+         */
+        public void setProcessedHeapData(byte[] heapData) {
+            mProcessedHeapData = heapData;
+        }
+
+        /**
+         * Get the processed heap data, if present.
+         *
+         * @return the processed heap data, or null.
+         */
+        public byte[] getProcessedHeapData() {
+            return mProcessedHeapData;
+        }
+
+        public void setProcessedHeapMap(Map<Integer, ArrayList<HeapSegmentElement>> heapMap) {
+            mProcessedHeapMap = heapMap;
+        }
+
+        public Map<Integer, ArrayList<HeapSegmentElement>> getProcessedHeapMap() {
+            return mProcessedHeapMap;
+        }
+    }
+
+    /**
+     * Handlers able to act on HPROF dumps.
+     */
+    public interface IHprofDumpHandler {
+        /**
+         * Called when a HPROF dump succeeded.
+         * @param remoteFilePath the device-side path of the HPROF file.
+         * @param client the client for which the HPROF file was.
+         */
+        void onSuccess(String remoteFilePath, Client client);
+
+        /**
+         * Called when a HPROF dump was successful.
+         * @param data the data containing the HPROF file, streamed from the VM
+         * @param client the client that was profiled.
+         */
+        void onSuccess(byte[] data, Client client);
+
+        /**
+         * Called when a hprof dump failed to end on the VM side
+         * @param client the client that was profiled.
+         * @param message an optional (<code>null<code> ok) error message to be displayed.
+         */
+        void onEndFailure(Client client, String message);
+    }
+
+    /**
+     * Handlers able to act on Method profiling info
+     */
+    public interface IMethodProfilingHandler {
+        /**
+         * Called when a method tracing was successful.
+         * @param remoteFilePath the device-side path of the trace file.
+         * @param client the client that was profiled.
+         */
+        void onSuccess(String remoteFilePath, Client client);
+
+        /**
+         * Called when a method tracing was successful.
+         * @param data the data containing the trace file, streamed from the VM
+         * @param client the client that was profiled.
+         */
+        void onSuccess(byte[] data, Client client);
+
+        /**
+         * Called when method tracing failed to start
+         * @param client the client that was profiled.
+         * @param message an optional (<code>null<code> ok) error message to be displayed.
+         */
+        void onStartFailure(Client client, String message);
+
+        /**
+         * Called when method tracing failed to end on the VM side
+         * @param client the client that was profiled.
+         * @param message an optional (<code>null<code> ok) error message to be displayed.
+         */
+        void onEndFailure(Client client, String message);
+    }
+
+    /**
+     * Sets the handler to receive notifications when an HPROF dump succeeded or failed.
+     */
+    public static void setHprofDumpHandler(IHprofDumpHandler handler) {
+        sHprofDumpHandler = handler;
+    }
+
+    static IHprofDumpHandler getHprofDumpHandler() {
+        return sHprofDumpHandler;
+    }
+
+    /**
+     * Sets the handler to receive notifications when an HPROF dump succeeded or failed.
+     */
+    public static void setMethodProfilingHandler(IMethodProfilingHandler handler) {
+        sMethodProfilingHandler = handler;
+    }
+
+    static IMethodProfilingHandler getMethodProfilingHandler() {
+        return sMethodProfilingHandler;
+    }
+
+    /**
+     * Generic constructor.
+     */
+    ClientData(int pid) {
+        mPid = pid;
+
+        mDebuggerInterest = DebuggerStatus.DEFAULT;
+        mThreadMap = new TreeMap<Integer,ThreadInfo>();
+    }
+
+    /**
+     * Returns whether the process is DDM-aware.
+     */
+    public boolean isDdmAware() {
+        return mIsDdmAware;
+    }
+
+    /**
+     * Sets DDM-aware status.
+     */
+    void isDdmAware(boolean aware) {
+        mIsDdmAware = aware;
+    }
+
+    /**
+     * Returns the process ID.
+     */
+    public int getPid() {
+        return mPid;
+    }
+
+    /**
+     * Returns the Client's VM identifier.
+     */
+    public String getVmIdentifier() {
+        return mVmIdentifier;
+    }
+
+    /**
+     * Sets VM identifier.
+     */
+    void setVmIdentifier(String ident) {
+        mVmIdentifier = ident;
+    }
+
+    /**
+     * Returns the client description.
+     * <p/>This is generally the name of the package defined in the
+     * <code>AndroidManifest.xml</code>.
+     *
+     * @return the client description or <code>null</code> if not the description was not yet
+     * sent by the client.
+     */
+    public String getClientDescription() {
+        return mClientDescription;
+    }
+
+    /**
+     * Returns the client's user id.
+     * @return user id if set, -1 otherwise
+     */
+    public int getUserId() {
+        return mUserId;
+    }
+
+    /**
+     * Returns true if the user id of this client was set. Only devices that support multiple
+     * users will actually return the user id to ddms. For other/older devices, this will not
+     * be set.
+     */
+    public boolean isValidUserId() {
+        return mValidUserId;
+    }
+
+    /**
+     * Sets client description.
+     *
+     * There may be a race between HELO and APNM.  Rather than try
+     * to enforce ordering on the device, we just don't allow an empty
+     * name to replace a specified one.
+     */
+    void setClientDescription(String description) {
+        if (mClientDescription == null && !description.isEmpty()) {
+            /*
+             * The application VM is first named <pre-initialized> before being assigned
+             * its real name.
+             * Depending on the timing, we can get an APNM chunk setting this name before
+             * another one setting the final actual name. So if we get a SetClientDescription
+             * with this value we ignore it.
+             */
+            if (!PRE_INITIALIZED.equals(description)) {
+                mClientDescription = description;
+            }
+        }
+    }
+
+    void setUserId(int id) {
+        mUserId = id;
+        mValidUserId = true;
+    }
+
+    /**
+     * Returns the debugger connection status.
+     */
+    public DebuggerStatus getDebuggerConnectionStatus() {
+        return mDebuggerInterest;
+    }
+
+    /**
+     * Sets debugger connection status.
+     */
+    void setDebuggerConnectionStatus(DebuggerStatus status) {
+        mDebuggerInterest = status;
+    }
+
+    /**
+     * Sets the current heap info values for the specified heap.
+     *
+     * @param heapId The heap whose info to update
+     * @param sizeInBytes The size of the heap, in bytes
+     * @param bytesAllocated The number of bytes currently allocated in the heap
+     * @param objectsAllocated The number of objects currently allocated in
+     *                         the heap
+     */
+    // TODO: keep track of timestamp, reason
+    synchronized void setHeapInfo(int heapId, long maxSizeInBytes,
+            long sizeInBytes, long bytesAllocated, long objectsAllocated) {
+        HashMap<String, Long> heapInfo = new HashMap<String, Long>();
+        heapInfo.put(HEAP_MAX_SIZE_BYTES, maxSizeInBytes);
+        heapInfo.put(HEAP_SIZE_BYTES, sizeInBytes);
+        heapInfo.put(HEAP_BYTES_ALLOCATED, bytesAllocated);
+        heapInfo.put(HEAP_OBJECTS_ALLOCATED, objectsAllocated);
+        mHeapInfoMap.put(heapId, heapInfo);
+    }
+
+    /**
+     * Returns the {@link HeapData} object for the VM.
+     */
+    public HeapData getVmHeapData() {
+        return mHeapData;
+    }
+
+    /**
+     * Returns the {@link HeapData} object for the native code.
+     */
+    HeapData getNativeHeapData() {
+        return mNativeHeapData;
+    }
+
+    /**
+     * Returns an iterator over the list of known VM heap ids.
+     * <p/>
+     * The caller must synchronize on the {@link ClientData} object while iterating.
+     *
+     * @return an iterator over the list of heap ids
+     */
+    public synchronized Iterator<Integer> getVmHeapIds() {
+        return mHeapInfoMap.keySet().iterator();
+    }
+
+    /**
+     * Returns the most-recent info values for the specified VM heap.
+     *
+     * @param heapId The heap whose info should be returned
+     * @return a map containing the info values for the specified heap.
+     *         Returns <code>null</code> if the heap ID is unknown.
+     */
+    public synchronized Map<String, Long> getVmHeapInfo(int heapId) {
+        return mHeapInfoMap.get(heapId);
+    }
+
+    /**
+     * Adds a new thread to the list.
+     */
+    synchronized void addThread(int threadId, String threadName) {
+        ThreadInfo attr = new ThreadInfo(threadId, threadName);
+        mThreadMap.put(threadId, attr);
+    }
+
+    /**
+     * Removes a thread from the list.
+     */
+    synchronized void removeThread(int threadId) {
+        mThreadMap.remove(threadId);
+    }
+
+    /**
+     * Returns the list of threads as {@link ThreadInfo} objects.
+     * <p/>The list is empty until a thread update was requested with
+     * {@link Client#requestThreadUpdate()}.
+     */
+    public synchronized ThreadInfo[] getThreads() {
+        Collection<ThreadInfo> threads = mThreadMap.values();
+        return threads.toArray(new ThreadInfo[threads.size()]);
+    }
+
+    /**
+     * Returns the {@link ThreadInfo} by thread id.
+     */
+    synchronized ThreadInfo getThread(int threadId) {
+        return mThreadMap.get(threadId);
+    }
+
+    synchronized void clearThreads() {
+        mThreadMap.clear();
+    }
+
+    /**
+     * Returns the list of {@link NativeAllocationInfo}.
+     * @see Client#requestNativeHeapInformation()
+     */
+    public synchronized List<NativeAllocationInfo> getNativeAllocationList() {
+        return Collections.unmodifiableList(mNativeAllocationList);
+    }
+
+    /**
+     * adds a new {@link NativeAllocationInfo} to the {@link Client}
+     * @param allocInfo The {@link NativeAllocationInfo} to add.
+     */
+    synchronized void addNativeAllocation(NativeAllocationInfo allocInfo) {
+        mNativeAllocationList.add(allocInfo);
+    }
+
+    /**
+     * Clear the current malloc info.
+     */
+    synchronized void clearNativeAllocationInfo() {
+        mNativeAllocationList.clear();
+    }
+
+    /**
+     * Returns the total native memory.
+     * @see Client#requestNativeHeapInformation()
+     */
+    public synchronized int getTotalNativeMemory() {
+        return mNativeTotalMemory;
+    }
+
+    synchronized void setTotalNativeMemory(int totalMemory) {
+        mNativeTotalMemory = totalMemory;
+    }
+
+    synchronized void addNativeLibraryMapInfo(long startAddr, long endAddr, String library) {
+        mNativeLibMapInfo.add(new NativeLibraryMapInfo(startAddr, endAddr, library));
+    }
+
+    /**
+     * Returns the list of native libraries mapped in memory for this client.
+     */
+    public synchronized List<NativeLibraryMapInfo> getMappedNativeLibraries() {
+        return Collections.unmodifiableList(mNativeLibMapInfo);
+    }
+
+    synchronized void setAllocationStatus(AllocationTrackingStatus status) {
+        mAllocationStatus = status;
+    }
+
+    /**
+     * Returns the allocation tracking status.
+     * @see Client#requestAllocationStatus()
+     */
+    public synchronized AllocationTrackingStatus getAllocationStatus() {
+        return mAllocationStatus;
+    }
+
+    synchronized void setAllocations(AllocationInfo[] allocs) {
+        mAllocations = allocs;
+    }
+
+    /**
+     * Returns the list of tracked allocations.
+     * @see Client#requestAllocationDetails()
+     */
+    public synchronized AllocationInfo[] getAllocations() {
+        return mAllocations;
+    }
+
+    void addFeature(String feature) {
+        mFeatures.add(feature);
+    }
+
+    /**
+     * Returns true if the {@link Client} supports the given <var>feature</var>
+     * @param feature The feature to test.
+     * @return true if the feature is supported
+     *
+     * @see ClientData#FEATURE_PROFILING
+     * @see ClientData#FEATURE_HPROF
+     */
+    public boolean hasFeature(String feature) {
+        return mFeatures.contains(feature);
+    }
+
+    /**
+     * Sets the device-side path to the hprof file being written
+     * @param pendingHprofDump the file to the hprof file
+     */
+    void setPendingHprofDump(String pendingHprofDump) {
+        mPendingHprofDump = pendingHprofDump;
+    }
+
+    /**
+     * Returns the path to the device-side hprof file being written.
+     */
+    String getPendingHprofDump() {
+        return mPendingHprofDump;
+    }
+
+    public boolean hasPendingHprofDump() {
+        return mPendingHprofDump != null;
+    }
+
+    synchronized void setMethodProfilingStatus(MethodProfilingStatus status) {
+        mProfilingStatus = status;
+    }
+
+    /**
+     * Returns the method profiling status.
+     * @see Client#requestMethodProfilingStatus()
+     */
+    public synchronized MethodProfilingStatus getMethodProfilingStatus() {
+        return mProfilingStatus;
+    }
+
+    /**
+     * Sets the device-side path to the method profile file being written
+     * @param pendingMethodProfiling the file being written
+     */
+    void setPendingMethodProfiling(String pendingMethodProfiling) {
+        mPendingMethodProfiling = pendingMethodProfiling;
+    }
+
+    /**
+     * Returns the path to the device-side method profiling file being written.
+     */
+    String getPendingMethodProfiling() {
+        return mPendingMethodProfiling;
+    }
+}
+
diff --git a/ddmlib/src/main/java/com/android/ddmlib/CollectingOutputReceiver.java b/ddmlib/src/main/java/com/android/ddmlib/CollectingOutputReceiver.java
new file mode 100644
index 0000000..e262cf3
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/CollectingOutputReceiver.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2010 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.ddmlib;
+
+
+import java.io.UnsupportedEncodingException;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * A {@link IShellOutputReceiver} which collects the whole shell output into one
+ * {@link String}.
+ */
+public class CollectingOutputReceiver implements IShellOutputReceiver {
+    private CountDownLatch mCompletionLatch;
+    private StringBuffer mOutputBuffer = new StringBuffer();
+    private boolean mIsCanceled = false;
+
+    public CollectingOutputReceiver() {
+    }
+
+    public CollectingOutputReceiver(CountDownLatch commandCompleteLatch) {
+        mCompletionLatch = commandCompleteLatch;
+    }
+
+    public String getOutput() {
+        return mOutputBuffer.toString();
+    }
+
+    @Override
+    public boolean isCancelled() {
+        return mIsCanceled;
+    }
+
+    /**
+     * Cancel the output collection
+     */
+    public void cancel() {
+        mIsCanceled = true;
+    }
+
+    @Override
+    public void addOutput(byte[] data, int offset, int length) {
+        if (!isCancelled()) {
+            String s = null;
+            try {
+                s = new String(data, offset, length, "UTF-8"); //$NON-NLS-1$
+            } catch (UnsupportedEncodingException e) {
+                // normal encoding didn't work, try the default one
+                s = new String(data, offset,length);
+            }
+            mOutputBuffer.append(s);
+        }
+    }
+
+    @Override
+    public void flush() {
+        if (mCompletionLatch != null) {
+            mCompletionLatch.countDown();
+        }
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/DdmConstants.java b/ddmlib/src/main/java/com/android/ddmlib/DdmConstants.java
new file mode 100644
index 0000000..6aec91e
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/DdmConstants.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2009 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.ddmlib;
+
+public final class DdmConstants {
+
+    public static final int PLATFORM_UNKNOWN = 0;
+    public static final int PLATFORM_LINUX = 1;
+    public static final int PLATFORM_WINDOWS = 2;
+    public static final int PLATFORM_DARWIN = 3;
+
+    /**
+     * Returns current platform, one of {@link #PLATFORM_WINDOWS}, {@link #PLATFORM_DARWIN},
+     * {@link #PLATFORM_LINUX} or {@link #PLATFORM_UNKNOWN}.
+     */
+    public static final int CURRENT_PLATFORM = currentPlatform();
+
+    /**
+     * Extension for Traceview files.
+     */
+    public static final String DOT_TRACE = ".trace";
+
+    /** hprof-conv executable (with extension for the current OS)  */
+    public static final String FN_HPROF_CONVERTER = (CURRENT_PLATFORM == PLATFORM_WINDOWS) ?
+            "hprof-conv.exe" : "hprof-conv"; //$NON-NLS-1$ //$NON-NLS-2$
+
+    /** traceview executable (with extension for the current OS)  */
+    public static final String FN_TRACEVIEW = (CURRENT_PLATFORM == PLATFORM_WINDOWS) ?
+            "traceview.bat" : "traceview"; //$NON-NLS-1$ //$NON-NLS-2$
+
+    /**
+     * Returns current platform
+     *
+     * @return one of {@link #PLATFORM_WINDOWS}, {@link #PLATFORM_DARWIN},
+     * {@link #PLATFORM_LINUX} or {@link #PLATFORM_UNKNOWN}.
+     */
+    public static int currentPlatform() {
+        String os = System.getProperty("os.name");          //$NON-NLS-1$
+        if (os.startsWith("Mac OS")) {                      //$NON-NLS-1$
+            return PLATFORM_DARWIN;
+        } else if (os.startsWith("Windows")) {              //$NON-NLS-1$
+            return PLATFORM_WINDOWS;
+        } else if (os.startsWith("Linux")) {                //$NON-NLS-1$
+            return PLATFORM_LINUX;
+        }
+
+        return PLATFORM_UNKNOWN;
+    }
+
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/DdmPreferences.java b/ddmlib/src/main/java/com/android/ddmlib/DdmPreferences.java
new file mode 100644
index 0000000..b0072ec
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/DdmPreferences.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import com.android.ddmlib.Log.LogLevel;
+
+/**
+ * Preferences for the ddm library.
+ * <p/>This class does not handle storing the preferences. It is merely a central point for
+ * applications using the ddmlib to override the default values.
+ * <p/>Various components of the ddmlib query this class to get their values.
+ * <p/>Calls to some <code>set##()</code> methods will update the components using the values
+ * right away, while other methods will have no effect once {@link AndroidDebugBridge#init(boolean)}
+ * has been called.
+ * <p/>Check the documentation of each method.
+ */
+public final class DdmPreferences {
+
+    /** Default value for thread update flag upon client connection. */
+    public static final boolean DEFAULT_INITIAL_THREAD_UPDATE = false;
+    /** Default value for heap update flag upon client connection. */
+    public static final boolean DEFAULT_INITIAL_HEAP_UPDATE = false;
+    /** Default value for the selected client debug port */
+    public static final int DEFAULT_SELECTED_DEBUG_PORT = 8700;
+    /** Default value for the debug port base */
+    public static final int DEFAULT_DEBUG_PORT_BASE = 8600;
+    /** Default value for the logcat {@link LogLevel} */
+    public static final LogLevel DEFAULT_LOG_LEVEL = LogLevel.ERROR;
+    /** Default timeout values for adb connection (milliseconds) */
+    public static final int DEFAULT_TIMEOUT = 5000; // standard delay, in ms
+    /** Default profiler buffer size (megabytes) */
+    public static final int DEFAULT_PROFILER_BUFFER_SIZE_MB = 8;
+    /** Default values for the use of the ADBHOST environment variable. */
+    public static final boolean DEFAULT_USE_ADBHOST = false;
+    public static final String DEFAULT_ADBHOST_VALUE = "127.0.0.1";
+
+    private static boolean sThreadUpdate = DEFAULT_INITIAL_THREAD_UPDATE;
+    private static boolean sInitialHeapUpdate = DEFAULT_INITIAL_HEAP_UPDATE;
+
+    private static int sSelectedDebugPort = DEFAULT_SELECTED_DEBUG_PORT;
+    private static int sDebugPortBase = DEFAULT_DEBUG_PORT_BASE;
+    private static LogLevel sLogLevel = DEFAULT_LOG_LEVEL;
+    private static int sTimeOut = DEFAULT_TIMEOUT;
+    private static int sProfilerBufferSizeMb = DEFAULT_PROFILER_BUFFER_SIZE_MB;
+
+    private static boolean sUseAdbHost = DEFAULT_USE_ADBHOST;
+    private static String sAdbHostValue = DEFAULT_ADBHOST_VALUE;
+
+    /**
+     * Returns the initial {@link Client} flag for thread updates.
+     * @see #setInitialThreadUpdate(boolean)
+     */
+    public static boolean getInitialThreadUpdate() {
+        return sThreadUpdate;
+    }
+
+    /**
+     * Sets the initial {@link Client} flag for thread updates.
+     * <p/>This change takes effect right away, for newly created {@link Client} objects.
+     */
+    public static void setInitialThreadUpdate(boolean state) {
+        sThreadUpdate = state;
+    }
+
+    /**
+     * Returns the initial {@link Client} flag for heap updates.
+     * @see #setInitialHeapUpdate(boolean)
+     */
+    public static boolean getInitialHeapUpdate() {
+        return sInitialHeapUpdate;
+    }
+
+    /**
+     * Sets the initial {@link Client} flag for heap updates.
+     * <p/>If <code>true</code>, the {@link ClientData} will automatically be updated with
+     * the VM heap information whenever a GC happens.
+     * <p/>This change takes effect right away, for newly created {@link Client} objects.
+     */
+    public static void setInitialHeapUpdate(boolean state) {
+        sInitialHeapUpdate = state;
+    }
+
+    /**
+     * Returns the debug port used by the selected {@link Client}.
+     */
+    public static int getSelectedDebugPort() {
+        return sSelectedDebugPort;
+    }
+
+    /**
+     * Sets the debug port used by the selected {@link Client}.
+     * <p/>This change takes effect right away.
+     * @param port the new port to use.
+     */
+    public static void setSelectedDebugPort(int port) {
+        sSelectedDebugPort = port;
+
+        MonitorThread monitorThread = MonitorThread.getInstance();
+        if (monitorThread != null) {
+            monitorThread.setDebugSelectedPort(port);
+        }
+    }
+
+    /**
+     * Returns the debug port used by the first {@link Client}. Following clients, will use the
+     * next port.
+     */
+    public static int getDebugPortBase() {
+        return sDebugPortBase;
+    }
+
+    /**
+     * Sets the debug port used by the first {@link Client}.
+     * <p/>Once a port is used, the next Client will use port + 1. Quitting applications will
+     * release their debug port, and new clients will be able to reuse them.
+     * <p/>This must be called before {@link AndroidDebugBridge#init(boolean)}.
+     */
+    public static void setDebugPortBase(int port) {
+        sDebugPortBase = port;
+    }
+
+    /**
+     * Returns the minimum {@link LogLevel} being displayed.
+     */
+    public static LogLevel getLogLevel() {
+        return sLogLevel;
+    }
+
+    /**
+     * Sets the minimum {@link LogLevel} to display.
+     * <p/>This change takes effect right away.
+     */
+    public static void setLogLevel(String value) {
+        sLogLevel = LogLevel.getByString(value);
+
+        Log.setLevel(sLogLevel);
+    }
+
+    /**
+     * Returns the timeout to be used in adb connections (milliseconds).
+     */
+    public static int getTimeOut() {
+        return sTimeOut;
+    }
+
+    /**
+     * Sets the timeout value for adb connection.
+     * <p/>This change takes effect for newly created connections only.
+     * @param timeOut the timeout value (milliseconds).
+     */
+    public static void setTimeOut(int timeOut) {
+        sTimeOut = timeOut;
+    }
+
+    /**
+     * Returns the profiler buffer size (megabytes).
+     */
+    public static int getProfilerBufferSizeMb() {
+        return sProfilerBufferSizeMb;
+    }
+
+    /**
+     * Sets the profiler buffer size value.
+     * @param bufferSizeMb the buffer size (megabytes).
+     */
+    public static void setProfilerBufferSizeMb(int bufferSizeMb) {
+        sProfilerBufferSizeMb = bufferSizeMb;
+    }
+
+    /**
+     * Returns a boolean indicating that the user uses or not the variable ADBHOST.
+     */
+    public static boolean getUseAdbHost() {
+        return sUseAdbHost;
+    }
+
+    /**
+     * Sets the value of the boolean indicating that the user uses or not the variable ADBHOST.
+     * @param useAdbHost true if the user uses ADBHOST
+     */
+    public static void setUseAdbHost(boolean useAdbHost) {
+        sUseAdbHost = useAdbHost;
+    }
+
+    /**
+     * Returns the value of the ADBHOST variable set by the user.
+     */
+    public static String getAdbHostValue() {
+        return sAdbHostValue;
+    }
+
+    /**
+     * Sets the value of the ADBHOST variable.
+     * @param adbHostValue
+     */
+    public static void setAdbHostValue(String adbHostValue) {
+        sAdbHostValue = adbHostValue;
+    }
+
+    /**
+     * Non accessible constructor.
+     */
+    private DdmPreferences() {
+        // pass, only static methods in the class.
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/DebugPortManager.java b/ddmlib/src/main/java/com/android/ddmlib/DebugPortManager.java
new file mode 100644
index 0000000..e1367fe
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/DebugPortManager.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+/**
+ * Centralized point to provide a {@link IDebugPortProvider} to ddmlib.
+ *
+ * <p/>When {@link Client} objects are created, they start listening for debuggers on a specific
+ * port. The default behavior is to start with {@link DdmPreferences#getDebugPortBase()} and
+ * increment this value for each new <code>Client</code>.
+ *
+ * <p/>This {@link DebugPortManager} allows applications using ddmlib to provide a custom
+ * port provider on a per-<code>Client</code> basis, depending on the device/emulator they are
+ * running on, and/or their names.
+ */
+public class DebugPortManager {
+
+    /**
+     * Classes which implement this interface provide a method that provides a non random
+     * debugger port for a newly created {@link Client}.
+     */
+    public interface IDebugPortProvider {
+
+        public static final int NO_STATIC_PORT = -1;
+
+        /**
+         * Returns a non-random debugger port for the specified application running on the
+         * specified {@link Device}.
+         * @param device The device the application is running on.
+         * @param appName The application name, as defined in the <code>AndroidManifest.xml</code>
+         * <var>package</var> attribute of the <var>manifest</var> node.
+         * @return The non-random debugger port or {@link #NO_STATIC_PORT} if the {@link Client}
+         * should use the automatic debugger port provider.
+         */
+        public int getPort(IDevice device, String appName);
+    }
+
+    private static IDebugPortProvider sProvider = null;
+
+    /**
+     * Sets the {@link IDebugPortProvider} that will be used when a new {@link Client} requests
+     * a debugger port.
+     * @param provider the <code>IDebugPortProvider</code> to use.
+     */
+    public static void setProvider(IDebugPortProvider provider) {
+        sProvider = provider;
+    }
+
+    /**
+     * Returns the
+     * @return
+     */
+    static IDebugPortProvider getProvider() {
+        return sProvider;
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/Debugger.java b/ddmlib/src/main/java/com/android/ddmlib/Debugger.java
new file mode 100644
index 0000000..9356c13
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/Debugger.java
@@ -0,0 +1,353 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import com.android.ddmlib.ClientData.DebuggerStatus;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+
+/**
+ * This represents a pending or established connection with a JDWP debugger.
+ */
+class Debugger {
+
+    /*
+     * Messages from the debugger should be pretty small; may not even
+     * need an expanding-buffer implementation for this.
+     */
+    private static final int INITIAL_BUF_SIZE = 1 * 1024;
+    private static final int MAX_BUF_SIZE = 32 * 1024;
+    private ByteBuffer mReadBuffer;
+
+    private static final int PRE_DATA_BUF_SIZE = 256;
+    private ByteBuffer mPreDataBuffer;
+
+    /* connection state */
+    private int mConnState;
+    private static final int ST_NOT_CONNECTED = 1;
+    private static final int ST_AWAIT_SHAKE   = 2;
+    private static final int ST_READY         = 3;
+
+    /* peer */
+    private Client mClient;         // client we're forwarding to/from
+    private int mListenPort;        // listen to me
+    private ServerSocketChannel mListenChannel;
+
+    /* this goes up and down; synchronize methods that access the field */
+    private SocketChannel mChannel;
+
+    /**
+     * Create a new Debugger object, configured to listen for connections
+     * on a specific port.
+     */
+    Debugger(Client client, int listenPort) throws IOException {
+
+        mClient = client;
+        mListenPort = listenPort;
+
+        mListenChannel = ServerSocketChannel.open();
+        mListenChannel.configureBlocking(false);        // required for Selector
+
+        InetSocketAddress addr = new InetSocketAddress(
+                InetAddress.getByName("localhost"), //$NON-NLS-1$
+                listenPort);
+        mListenChannel.socket().setReuseAddress(true);  // enable SO_REUSEADDR
+        mListenChannel.socket().bind(addr);
+
+        mReadBuffer = ByteBuffer.allocate(INITIAL_BUF_SIZE);
+        mPreDataBuffer = ByteBuffer.allocate(PRE_DATA_BUF_SIZE);
+        mConnState = ST_NOT_CONNECTED;
+
+        Log.d("ddms", "Created: " + this.toString());
+    }
+
+    /**
+     * Returns "true" if a debugger is currently attached to us.
+     */
+    boolean isDebuggerAttached() {
+        return mChannel != null;
+    }
+
+    /**
+     * Represent the Debugger as a string.
+     */
+    @Override
+    public String toString() {
+        // mChannel != null means we have connection, ST_READY means it's going
+        return "[Debugger " + mListenPort + "-->" + mClient.getClientData().getPid()
+                + ((mConnState != ST_READY) ? " inactive]" : " active]");
+    }
+
+    /**
+     * Register the debugger's listen socket with the Selector.
+     */
+    void registerListener(Selector sel) throws IOException {
+        mListenChannel.register(sel, SelectionKey.OP_ACCEPT, this);
+    }
+
+    /**
+     * Return the Client being debugged.
+     */
+    Client getClient() {
+        return mClient;
+    }
+
+    /**
+     * Accept a new connection, but only if we don't already have one.
+     *
+     * Must be synchronized with other uses of mChannel and mPreBuffer.
+     *
+     * Returns "null" if we're already talking to somebody.
+     */
+    synchronized SocketChannel accept() throws IOException {
+        return accept(mListenChannel);
+    }
+
+    /**
+     * Accept a new connection from the specified listen channel.  This
+     * is so we can listen on a dedicated port for the "current" client,
+     * where "current" is constantly in flux.
+     *
+     * Must be synchronized with other uses of mChannel and mPreBuffer.
+     *
+     * Returns "null" if we're already talking to somebody.
+     */
+    synchronized SocketChannel accept(ServerSocketChannel listenChan)
+        throws IOException {
+
+        if (listenChan != null) {
+            SocketChannel newChan;
+
+            newChan = listenChan.accept();
+            if (mChannel != null) {
+                Log.w("ddms", "debugger already talking to " + mClient
+                    + " on " + mListenPort);
+                newChan.close();
+                return null;
+            }
+            mChannel = newChan;
+            mChannel.configureBlocking(false);         // required for Selector
+            mConnState = ST_AWAIT_SHAKE;
+            return mChannel;
+        }
+
+        return null;
+    }
+
+    /**
+     * Close the data connection only.
+     */
+    synchronized void closeData() {
+        try {
+            if (mChannel != null) {
+                mChannel.close();
+                mChannel = null;
+                mConnState = ST_NOT_CONNECTED;
+
+                ClientData cd = mClient.getClientData();
+                cd.setDebuggerConnectionStatus(DebuggerStatus.DEFAULT);
+                mClient.update(Client.CHANGE_DEBUGGER_STATUS);
+            }
+        } catch (IOException ioe) {
+            Log.w("ddms", "Failed to close data " + this);
+        }
+    }
+
+    /**
+     * Close the socket that's listening for new connections and (if
+     * we're connected) the debugger data socket.
+     */
+    synchronized void close() {
+        try {
+            if (mListenChannel != null) {
+                mListenChannel.close();
+            }
+            mListenChannel = null;
+            closeData();
+        } catch (IOException ioe) {
+            Log.w("ddms", "Failed to close listener " + this);
+        }
+    }
+
+    // TODO: ?? add a finalizer that verifies the channel was closed
+
+    /**
+     * Read data from our channel.
+     *
+     * This is called when data is known to be available, and we don't yet
+     * have a full packet in the buffer.  If the buffer is at capacity,
+     * expand it.
+     */
+    void read() throws IOException {
+        int count;
+
+        if (mReadBuffer.position() == mReadBuffer.capacity()) {
+            if (mReadBuffer.capacity() * 2 > MAX_BUF_SIZE) {
+                throw new BufferOverflowException();
+            }
+            Log.d("ddms", "Expanding read buffer to "
+                + mReadBuffer.capacity() * 2);
+
+            ByteBuffer newBuffer =
+                    ByteBuffer.allocate(mReadBuffer.capacity() * 2);
+            mReadBuffer.position(0);
+            newBuffer.put(mReadBuffer);     // leaves "position" at end
+
+            mReadBuffer = newBuffer;
+        }
+
+        count = mChannel.read(mReadBuffer);
+        Log.v("ddms", "Read " + count + " bytes from " + this);
+        if (count < 0) throw new IOException("read failed");
+    }
+
+    /**
+     * Return information for the first full JDWP packet in the buffer.
+     *
+     * If we don't yet have a full packet, return null.
+     *
+     * If we haven't yet received the JDWP handshake, we watch for it here
+     * and consume it without admitting to have done so.  We also send
+     * the handshake response to the debugger, along with any pending
+     * pre-connection data, which is why this can throw an IOException.
+     */
+    JdwpPacket getJdwpPacket() throws IOException {
+        /*
+         * On entry, the data starts at offset 0 and ends at "position".
+         * "limit" is set to the buffer capacity.
+         */
+        if (mConnState == ST_AWAIT_SHAKE) {
+            int result;
+
+            result = JdwpPacket.findHandshake(mReadBuffer);
+            //Log.v("ddms", "findHand: " + result);
+            switch (result) {
+                case JdwpPacket.HANDSHAKE_GOOD:
+                    Log.d("ddms", "Good handshake from debugger");
+                    JdwpPacket.consumeHandshake(mReadBuffer);
+                    sendHandshake();
+                    mConnState = ST_READY;
+
+                    ClientData cd = mClient.getClientData();
+                    cd.setDebuggerConnectionStatus(DebuggerStatus.ATTACHED);
+                    mClient.update(Client.CHANGE_DEBUGGER_STATUS);
+
+                    // see if we have another packet in the buffer
+                    return getJdwpPacket();
+                case JdwpPacket.HANDSHAKE_BAD:
+                    // not a debugger, throw an exception so we drop the line
+                    Log.d("ddms", "Bad handshake from debugger");
+                    throw new IOException("bad handshake");
+                case JdwpPacket.HANDSHAKE_NOTYET:
+                    break;
+                default:
+                    Log.e("ddms", "Unknown packet while waiting for client handshake");
+            }
+            return null;
+        } else if (mConnState == ST_READY) {
+            if (mReadBuffer.position() != 0) {
+                Log.v("ddms", "Checking " + mReadBuffer.position() + " bytes");
+            }
+            return JdwpPacket.findPacket(mReadBuffer);
+        } else {
+            Log.e("ddms", "Receiving data in state = " + mConnState);
+        }
+
+        return null;
+    }
+
+    /**
+     * Forward a packet to the client.
+     *
+     * "mClient" will never be null, though it's possible that the channel
+     * in the client has closed and our send attempt will fail.
+     *
+     * Consumes the packet.
+     */
+    void forwardPacketToClient(JdwpPacket packet) throws IOException {
+        mClient.sendAndConsume(packet);
+    }
+
+    /**
+     * Send the handshake to the debugger.  We also send along any packets
+     * we already received from the client (usually just a VM_START event,
+     * if anything at all).
+     */
+    private synchronized void sendHandshake() throws IOException {
+        ByteBuffer tempBuffer = ByteBuffer.allocate(JdwpPacket.HANDSHAKE_LEN);
+        JdwpPacket.putHandshake(tempBuffer);
+        int expectedLength = tempBuffer.position();
+        tempBuffer.flip();
+        if (mChannel.write(tempBuffer) != expectedLength) {
+            throw new IOException("partial handshake write");
+        }
+
+        expectedLength = mPreDataBuffer.position();
+        if (expectedLength > 0) {
+            Log.d("ddms", "Sending " + mPreDataBuffer.position()
+                    + " bytes of saved data");
+            mPreDataBuffer.flip();
+            if (mChannel.write(mPreDataBuffer) != expectedLength) {
+                throw new IOException("partial pre-data write");
+            }
+            mPreDataBuffer.clear();
+        }
+    }
+
+    /**
+     * Send a packet to the debugger.
+     *
+     * Ideally, we can do this with a single channel write.  If that doesn't
+     * happen, we have to prevent anybody else from writing to the channel
+     * until this packet completes, so we synchronize on the channel.
+     *
+     * Another goal is to avoid unnecessary buffer copies, so we write
+     * directly out of the JdwpPacket's ByteBuffer.
+     *
+     * We must synchronize on "mChannel" before writing to it.  We want to
+     * coordinate the buffered data with mChannel creation, so this whole
+     * method is synchronized.
+     */
+    synchronized void sendAndConsume(JdwpPacket packet)
+        throws IOException {
+
+        if (mChannel == null) {
+            /*
+             * Buffer this up so we can send it to the debugger when it
+             * finally does connect.  This is essential because the VM_START
+             * message might be telling the debugger that the VM is
+             * suspended.  The alternative approach would be for us to
+             * capture and interpret VM_START and send it later if we
+             * didn't choose to un-suspend the VM for our own purposes.
+             */
+            Log.d("ddms", "Saving packet 0x"
+                    + Integer.toHexString(packet.getId()));
+            packet.movePacket(mPreDataBuffer);
+        } else {
+            packet.writeAndConsume(mChannel);
+        }
+    }
+}
+
diff --git a/ddmlib/src/main/java/com/android/ddmlib/Device.java b/ddmlib/src/main/java/com/android/ddmlib/Device.java
new file mode 100644
index 0000000..55d285d
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/Device.java
@@ -0,0 +1,872 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import com.android.annotations.concurrency.GuardedBy;
+import com.android.ddmlib.log.LogReceiver;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.channels.SocketChannel;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+/**
+ * A Device. It can be a physical device or an emulator.
+ */
+final class Device implements IDevice {
+
+    private static final int INSTALL_TIMEOUT = 2*60*1000; //2min
+    private static final int BATTERY_TIMEOUT = 2*1000; //2 seconds
+    private static final int GETPROP_TIMEOUT = 2*1000; //2 seconds
+
+    /** Emulator Serial Number regexp. */
+    static final String RE_EMULATOR_SN = "emulator-(\\d+)"; //$NON-NLS-1$
+
+    /** Serial number of the device */
+    private String mSerialNumber = null;
+
+    /** Name of the AVD */
+    private String mAvdName = null;
+
+    /** State of the device. */
+    private DeviceState mState = null;
+
+    /** Device properties. */
+    private final Map<String, String> mProperties = new HashMap<String, String>();
+    private final Map<String, String> mMountPoints = new HashMap<String, String>();
+
+    @GuardedBy("mClients")
+    private final List<Client> mClients = new ArrayList<Client>();
+
+    /** Maps pid's of clients in {@link #mClients} to their package name. */
+    private final Map<Integer, String> mClientInfo = new ConcurrentHashMap<Integer, String>();
+
+    private DeviceMonitor mMonitor;
+
+    private static final String LOG_TAG = "Device";
+    private static final char SEPARATOR = '-';
+    private static final String UNKNOWN_PACKAGE = "";   //$NON-NLS-1$
+
+    /**
+     * Socket for the connection monitoring client connection/disconnection.
+     */
+    private SocketChannel mSocketChannel;
+
+    private boolean mArePropertiesSet = false;
+
+    private Integer mLastBatteryLevel = null;
+    private long mLastBatteryCheckTime = 0;
+
+    private String mName;
+
+    /**
+     * Output receiver for "pm install package.apk" command line.
+     */
+    private static final class InstallReceiver extends MultiLineReceiver {
+
+        private static final String SUCCESS_OUTPUT = "Success"; //$NON-NLS-1$
+        private static final Pattern FAILURE_PATTERN = Pattern.compile("Failure\\s+\\[(.*)\\]"); //$NON-NLS-1$
+
+        private String mErrorMessage = null;
+
+        public InstallReceiver() {
+        }
+
+        @Override
+        public void processNewLines(String[] lines) {
+            for (String line : lines) {
+                if (!line.isEmpty()) {
+                    if (line.startsWith(SUCCESS_OUTPUT)) {
+                        mErrorMessage = null;
+                    } else {
+                        Matcher m = FAILURE_PATTERN.matcher(line);
+                        if (m.matches()) {
+                            mErrorMessage = m.group(1);
+                        }
+                    }
+                }
+            }
+        }
+
+        @Override
+        public boolean isCancelled() {
+            return false;
+        }
+
+        public String getErrorMessage() {
+            return mErrorMessage;
+        }
+    }
+
+    /**
+     * Output receiver for "dumpsys battery" command line.
+     */
+    private static final class BatteryReceiver extends MultiLineReceiver {
+        private static final Pattern BATTERY_LEVEL = Pattern.compile("\\s*level: (\\d+)");
+        private static final Pattern SCALE = Pattern.compile("\\s*scale: (\\d+)");
+
+        private Integer mBatteryLevel = null;
+        private Integer mBatteryScale = null;
+
+        /**
+         * Get the parsed percent battery level.
+         * @return
+         */
+        public Integer getBatteryLevel() {
+            if (mBatteryLevel != null && mBatteryScale != null) {
+                return (mBatteryLevel * 100) / mBatteryScale;
+            }
+            return null;
+        }
+
+        @Override
+        public void processNewLines(String[] lines) {
+            for (String line : lines) {
+                Matcher batteryMatch = BATTERY_LEVEL.matcher(line);
+                if (batteryMatch.matches()) {
+                    try {
+                        mBatteryLevel = Integer.parseInt(batteryMatch.group(1));
+                    } catch (NumberFormatException e) {
+                        Log.w(LOG_TAG, String.format("Failed to parse %s as an integer",
+                                batteryMatch.group(1)));
+                    }
+                }
+                Matcher scaleMatch = SCALE.matcher(line);
+                if (scaleMatch.matches()) {
+                    try {
+                        mBatteryScale = Integer.parseInt(scaleMatch.group(1));
+                    } catch (NumberFormatException e) {
+                        Log.w(LOG_TAG, String.format("Failed to parse %s as an integer",
+                                batteryMatch.group(1)));
+                    }
+                }
+            }
+        }
+
+        @Override
+        public boolean isCancelled() {
+            return false;
+        }
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.android.ddmlib.IDevice#getSerialNumber()
+     */
+    @Override
+    public String getSerialNumber() {
+        return mSerialNumber;
+    }
+
+    @Override
+    public String getAvdName() {
+        return mAvdName;
+    }
+
+    /**
+     * Sets the name of the AVD
+     */
+    void setAvdName(String avdName) {
+        if (!isEmulator()) {
+            throw new IllegalArgumentException(
+                    "Cannot set the AVD name of the device is not an emulator");
+        }
+
+        mAvdName = avdName;
+    }
+
+    @Override
+    public String getName() {
+        if (mName != null) {
+            return mName;
+        }
+
+        if (isOnline()) {
+            // cache name only if device is online
+            mName = constructName();
+            return mName;
+        } else {
+            return constructName();
+        }
+    }
+
+    private String constructName() {
+        if (isEmulator()) {
+            String avdName = getAvdName();
+            if (avdName != null) {
+                return String.format("%s [%s]", avdName, getSerialNumber());
+            } else {
+                return getSerialNumber();
+            }
+        } else {
+            String manufacturer = null;
+            String model = null;
+
+            try {
+                manufacturer = cleanupStringForDisplay(
+                    getPropertyCacheOrSync(PROP_DEVICE_MANUFACTURER));
+                model = cleanupStringForDisplay(
+                    getPropertyCacheOrSync(PROP_DEVICE_MODEL));
+            } catch (Exception e) {
+                // If there are exceptions thrown while attempting to get these properties,
+                // we can just use the serial number, so ignore these exceptions.
+            }
+
+            StringBuilder sb = new StringBuilder(20);
+
+            if (manufacturer != null) {
+                sb.append(manufacturer);
+                sb.append(SEPARATOR);
+            }
+
+            if (model != null) {
+                sb.append(model);
+                sb.append(SEPARATOR);
+            }
+
+            sb.append(getSerialNumber());
+            return sb.toString();
+        }
+    }
+
+    private String cleanupStringForDisplay(String s) {
+        if (s == null) {
+            return null;
+        }
+
+        StringBuilder sb = new StringBuilder(s.length());
+        for (int i = 0; i < s.length(); i++) {
+            char c = s.charAt(i);
+
+            if (Character.isLetterOrDigit(c)) {
+                sb.append(Character.toLowerCase(c));
+            } else {
+                sb.append('_');
+            }
+        }
+
+        return sb.toString();
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.android.ddmlib.IDevice#getState()
+     */
+    @Override
+    public DeviceState getState() {
+        return mState;
+    }
+
+    /**
+     * Changes the state of the device.
+     */
+    void setState(DeviceState state) {
+        mState = state;
+    }
+
+
+    /*
+     * (non-Javadoc)
+     * @see com.android.ddmlib.IDevice#getProperties()
+     */
+    @Override
+    public Map<String, String> getProperties() {
+        return Collections.unmodifiableMap(mProperties);
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.android.ddmlib.IDevice#getPropertyCount()
+     */
+    @Override
+    public int getPropertyCount() {
+        return mProperties.size();
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.android.ddmlib.IDevice#getProperty(java.lang.String)
+     */
+    @Override
+    public String getProperty(String name) {
+        return mProperties.get(name);
+    }
+
+    @Override
+    public boolean arePropertiesSet() {
+        return mArePropertiesSet;
+    }
+
+    @Override
+    public String getPropertyCacheOrSync(String name) throws TimeoutException,
+            AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException {
+        if (mArePropertiesSet) {
+            return getProperty(name);
+        } else {
+            return getPropertySync(name);
+        }
+    }
+
+    @Override
+    public String getPropertySync(String name) throws TimeoutException,
+            AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException {
+        CountDownLatch latch = new CountDownLatch(1);
+        CollectingOutputReceiver receiver = new CollectingOutputReceiver(latch);
+        executeShellCommand(String.format("getprop '%s'", name), receiver, GETPROP_TIMEOUT);
+        try {
+            latch.await(GETPROP_TIMEOUT, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+            return null;
+        }
+
+        String value = receiver.getOutput().trim();
+        if (value.isEmpty()) {
+            return null;
+        }
+
+        return value;
+    }
+
+    @Override
+    public String getMountPoint(String name) {
+        return mMountPoints.get(name);
+    }
+
+
+    @Override
+    public String toString() {
+        return mSerialNumber;
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.android.ddmlib.IDevice#isOnline()
+     */
+    @Override
+    public boolean isOnline() {
+        return mState == DeviceState.ONLINE;
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.android.ddmlib.IDevice#isEmulator()
+     */
+    @Override
+    public boolean isEmulator() {
+        return mSerialNumber.matches(RE_EMULATOR_SN);
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.android.ddmlib.IDevice#isOffline()
+     */
+    @Override
+    public boolean isOffline() {
+        return mState == DeviceState.OFFLINE;
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.android.ddmlib.IDevice#isBootLoader()
+     */
+    @Override
+    public boolean isBootLoader() {
+        return mState == DeviceState.BOOTLOADER;
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.android.ddmlib.IDevice#getSyncService()
+     */
+    @Override
+    public SyncService getSyncService()
+            throws TimeoutException, AdbCommandRejectedException, IOException {
+        SyncService syncService = new SyncService(AndroidDebugBridge.getSocketAddress(), this);
+        if (syncService.openSync()) {
+            return syncService;
+         }
+
+        return null;
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.android.ddmlib.IDevice#getFileListingService()
+     */
+    @Override
+    public FileListingService getFileListingService() {
+        return new FileListingService(this);
+    }
+
+    @Override
+    public RawImage getScreenshot()
+            throws TimeoutException, AdbCommandRejectedException, IOException {
+        return AdbHelper.getFrameBuffer(AndroidDebugBridge.getSocketAddress(), this);
+    }
+
+    @Override
+    public void executeShellCommand(String command, IShellOutputReceiver receiver)
+            throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException,
+            IOException {
+        AdbHelper.executeRemoteCommand(AndroidDebugBridge.getSocketAddress(), command, this,
+                receiver, DdmPreferences.getTimeOut());
+    }
+
+    @Override
+    public void executeShellCommand(String command, IShellOutputReceiver receiver,
+            int maxTimeToOutputResponse)
+            throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException,
+            IOException {
+        AdbHelper.executeRemoteCommand(AndroidDebugBridge.getSocketAddress(), command, this,
+                receiver, maxTimeToOutputResponse);
+    }
+
+    @Override
+    public void runEventLogService(LogReceiver receiver)
+            throws TimeoutException, AdbCommandRejectedException, IOException {
+        AdbHelper.runEventLogService(AndroidDebugBridge.getSocketAddress(), this, receiver);
+    }
+
+    @Override
+    public void runLogService(String logname, LogReceiver receiver)
+            throws TimeoutException, AdbCommandRejectedException, IOException {
+        AdbHelper.runLogService(AndroidDebugBridge.getSocketAddress(), this, logname, receiver);
+    }
+
+    @Override
+    public void createForward(int localPort, int remotePort)
+            throws TimeoutException, AdbCommandRejectedException, IOException {
+        AdbHelper.createForward(AndroidDebugBridge.getSocketAddress(), this,
+                String.format("tcp:%d", localPort),     //$NON-NLS-1$
+                String.format("tcp:%d", remotePort));   //$NON-NLS-1$
+    }
+
+    @Override
+    public void createForward(int localPort, String remoteSocketName,
+            DeviceUnixSocketNamespace namespace) throws TimeoutException,
+            AdbCommandRejectedException, IOException {
+        AdbHelper.createForward(AndroidDebugBridge.getSocketAddress(), this,
+                String.format("tcp:%d", localPort),     //$NON-NLS-1$
+                String.format("%s:%s", namespace.getType(), remoteSocketName));   //$NON-NLS-1$
+    }
+
+    @Override
+    public void removeForward(int localPort, int remotePort)
+            throws TimeoutException, AdbCommandRejectedException, IOException {
+        AdbHelper.removeForward(AndroidDebugBridge.getSocketAddress(), this,
+                String.format("tcp:%d", localPort),     //$NON-NLS-1$
+                String.format("tcp:%d", remotePort));   //$NON-NLS-1$
+    }
+
+    @Override
+    public void removeForward(int localPort, String remoteSocketName,
+            DeviceUnixSocketNamespace namespace) throws TimeoutException,
+            AdbCommandRejectedException, IOException {
+        AdbHelper.removeForward(AndroidDebugBridge.getSocketAddress(), this,
+                String.format("tcp:%d", localPort),     //$NON-NLS-1$
+                String.format("%s:%s", namespace.getType(), remoteSocketName));   //$NON-NLS-1$
+    }
+
+    Device(DeviceMonitor monitor, String serialNumber, DeviceState deviceState) {
+        mMonitor = monitor;
+        mSerialNumber = serialNumber;
+        mState = deviceState;
+    }
+
+    DeviceMonitor getMonitor() {
+        return mMonitor;
+    }
+
+    @Override
+    public boolean hasClients() {
+        synchronized (mClients) {
+            return !mClients.isEmpty();
+        }
+    }
+
+    @Override
+    public Client[] getClients() {
+        synchronized (mClients) {
+            return mClients.toArray(new Client[mClients.size()]);
+        }
+    }
+
+    @Override
+    public Client getClient(String applicationName) {
+        synchronized (mClients) {
+            for (Client c : mClients) {
+                if (applicationName.equals(c.getClientData().getClientDescription())) {
+                    return c;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    void addClient(Client client) {
+        synchronized (mClients) {
+            mClients.add(client);
+        }
+
+        addClientInfo(client);
+    }
+
+    List<Client> getClientList() {
+        return mClients;
+    }
+
+    void clearClientList() {
+        synchronized (mClients) {
+            mClients.clear();
+        }
+
+        clearClientInfo();
+    }
+
+    /**
+     * Removes a {@link Client} from the list.
+     * @param client the client to remove.
+     * @param notify Whether or not to notify the listeners of a change.
+     */
+    void removeClient(Client client, boolean notify) {
+        mMonitor.addPortToAvailableList(client.getDebuggerListenPort());
+        synchronized (mClients) {
+            mClients.remove(client);
+        }
+        if (notify) {
+            mMonitor.getServer().deviceChanged(this, CHANGE_CLIENT_LIST);
+        }
+
+        removeClientInfo(client);
+    }
+
+    /**
+     * Sets the client monitoring socket.
+     * @param socketChannel the sockets
+     */
+    void setClientMonitoringSocket(SocketChannel socketChannel) {
+        mSocketChannel = socketChannel;
+    }
+
+    /**
+     * Returns the client monitoring socket.
+     */
+    SocketChannel getClientMonitoringSocket() {
+        return mSocketChannel;
+    }
+
+    void update(int changeMask) {
+        if ((changeMask & CHANGE_BUILD_INFO) != 0) {
+            mArePropertiesSet = true;
+        }
+        mMonitor.getServer().deviceChanged(this, changeMask);
+    }
+
+    void update(Client client, int changeMask) {
+        mMonitor.getServer().clientChanged(client, changeMask);
+        updateClientInfo(client, changeMask);
+    }
+
+    void addProperty(String label, String value) {
+        mProperties.put(label, value);
+    }
+
+    void setMountingPoint(String name, String value) {
+        mMountPoints.put(name, value);
+    }
+
+    private void addClientInfo(Client client) {
+        ClientData cd = client.getClientData();
+        setClientInfo(cd.getPid(), cd.getClientDescription());
+    }
+
+    private void updateClientInfo(Client client, int changeMask) {
+        if ((changeMask & Client.CHANGE_NAME) == Client.CHANGE_NAME) {
+            addClientInfo(client);
+        }
+    }
+
+    private void removeClientInfo(Client client) {
+        int pid = client.getClientData().getPid();
+        mClientInfo.remove(pid);
+    }
+
+    private void clearClientInfo() {
+        mClientInfo.clear();
+    }
+
+    private void setClientInfo(int pid, String pkgName) {
+        if (pkgName == null) {
+            pkgName = UNKNOWN_PACKAGE;
+        }
+
+        mClientInfo.put(pid, pkgName);
+    }
+
+    @Override
+    public String getClientName(int pid) {
+        String pkgName = mClientInfo.get(pid);
+        return pkgName == null ? UNKNOWN_PACKAGE : pkgName;
+    }
+
+    @Override
+    public void pushFile(String local, String remote)
+            throws IOException, AdbCommandRejectedException, TimeoutException, SyncException {
+        SyncService sync = null;
+        try {
+            String targetFileName = getFileName(local);
+
+            Log.d(targetFileName, String.format("Uploading %1$s onto device '%2$s'",
+                    targetFileName, getSerialNumber()));
+
+            sync = getSyncService();
+            if (sync != null) {
+                String message = String.format("Uploading file onto device '%1$s'",
+                        getSerialNumber());
+                Log.d(LOG_TAG, message);
+                sync.pushFile(local, remote, SyncService.getNullProgressMonitor());
+            } else {
+                throw new IOException("Unable to open sync connection!");
+            }
+        } catch (TimeoutException e) {
+            Log.e(LOG_TAG, "Error during Sync: timeout.");
+            throw e;
+
+        } catch (SyncException e) {
+            Log.e(LOG_TAG, String.format("Error during Sync: %1$s", e.getMessage()));
+            throw e;
+
+        } catch (IOException e) {
+            Log.e(LOG_TAG, String.format("Error during Sync: %1$s", e.getMessage()));
+            throw e;
+
+        } finally {
+            if (sync != null) {
+                sync.close();
+            }
+        }
+    }
+
+    @Override
+    public void pullFile(String remote, String local)
+            throws IOException, AdbCommandRejectedException, TimeoutException, SyncException {
+        SyncService sync = null;
+        try {
+            String targetFileName = getFileName(remote);
+
+            Log.d(targetFileName, String.format("Downloading %1$s from device '%2$s'",
+                    targetFileName, getSerialNumber()));
+
+            sync = getSyncService();
+            if (sync != null) {
+                String message = String.format("Downloading file from device '%1$s'",
+                        getSerialNumber());
+                Log.d(LOG_TAG, message);
+                sync.pullFile(remote, local, SyncService.getNullProgressMonitor());
+            } else {
+                throw new IOException("Unable to open sync connection!");
+            }
+        } catch (TimeoutException e) {
+            Log.e(LOG_TAG, "Error during Sync: timeout.");
+            throw e;
+
+        } catch (SyncException e) {
+            Log.e(LOG_TAG, String.format("Error during Sync: %1$s", e.getMessage()));
+            throw e;
+
+        } catch (IOException e) {
+            Log.e(LOG_TAG, String.format("Error during Sync: %1$s", e.getMessage()));
+            throw e;
+
+        } finally {
+            if (sync != null) {
+                sync.close();
+            }
+        }
+    }
+
+    @Override
+    public String installPackage(String packageFilePath, boolean reinstall, String... extraArgs)
+            throws InstallException {
+        try {
+            String remoteFilePath = syncPackageToDevice(packageFilePath);
+            String result = installRemotePackage(remoteFilePath, reinstall, extraArgs);
+            removeRemotePackage(remoteFilePath);
+            return result;
+        } catch (IOException e) {
+            throw new InstallException(e);
+        } catch (AdbCommandRejectedException e) {
+            throw new InstallException(e);
+        } catch (TimeoutException e) {
+            throw new InstallException(e);
+        } catch (SyncException e) {
+            throw new InstallException(e);
+        }
+    }
+
+    @Override
+    public String syncPackageToDevice(String localFilePath)
+            throws IOException, AdbCommandRejectedException, TimeoutException, SyncException {
+        SyncService sync = null;
+        try {
+            String packageFileName = getFileName(localFilePath);
+            String remoteFilePath = String.format("/data/local/tmp/%1$s", packageFileName); //$NON-NLS-1$
+
+            Log.d(packageFileName, String.format("Uploading %1$s onto device '%2$s'",
+                    packageFileName, getSerialNumber()));
+
+            sync = getSyncService();
+            if (sync != null) {
+                String message = String.format("Uploading file onto device '%1$s'",
+                        getSerialNumber());
+                Log.d(LOG_TAG, message);
+                sync.pushFile(localFilePath, remoteFilePath, SyncService.getNullProgressMonitor());
+            } else {
+                throw new IOException("Unable to open sync connection!");
+            }
+            return remoteFilePath;
+        } catch (TimeoutException e) {
+            Log.e(LOG_TAG, "Error during Sync: timeout.");
+            throw e;
+
+        } catch (SyncException e) {
+            Log.e(LOG_TAG, String.format("Error during Sync: %1$s", e.getMessage()));
+            throw e;
+
+        } catch (IOException e) {
+            Log.e(LOG_TAG, String.format("Error during Sync: %1$s", e.getMessage()));
+            throw e;
+
+        } finally {
+            if (sync != null) {
+                sync.close();
+            }
+        }
+    }
+
+    /**
+     * Helper method to retrieve the file name given a local file path
+     * @param filePath full directory path to file
+     * @return {@link String} file name
+     */
+    private String getFileName(String filePath) {
+        return new File(filePath).getName();
+    }
+
+    @Override
+    public String installRemotePackage(String remoteFilePath, boolean reinstall,
+            String... extraArgs) throws InstallException {
+        try {
+            InstallReceiver receiver = new InstallReceiver();
+            StringBuilder optionString = new StringBuilder();
+            if (reinstall) {
+                optionString.append("-r ");
+            }
+            for (String arg : extraArgs) {
+                optionString.append(arg);
+                optionString.append(' ');
+            }
+            String cmd = String.format("pm install %1$s \"%2$s\"", optionString.toString(),
+                    remoteFilePath);
+            executeShellCommand(cmd, receiver, INSTALL_TIMEOUT);
+            return receiver.getErrorMessage();
+        } catch (TimeoutException e) {
+            throw new InstallException(e);
+        } catch (AdbCommandRejectedException e) {
+            throw new InstallException(e);
+        } catch (ShellCommandUnresponsiveException e) {
+            throw new InstallException(e);
+        } catch (IOException e) {
+            throw new InstallException(e);
+        }
+    }
+
+    @Override
+    public void removeRemotePackage(String remoteFilePath) throws InstallException {
+        try {
+            executeShellCommand(String.format("rm \"%1$s\"", remoteFilePath),
+                    new NullOutputReceiver(), INSTALL_TIMEOUT);
+        } catch (IOException e) {
+            throw new InstallException(e);
+        } catch (TimeoutException e) {
+            throw new InstallException(e);
+        } catch (AdbCommandRejectedException e) {
+            throw new InstallException(e);
+        } catch (ShellCommandUnresponsiveException e) {
+            throw new InstallException(e);
+        }
+    }
+
+    @Override
+    public String uninstallPackage(String packageName) throws InstallException {
+        try {
+            InstallReceiver receiver = new InstallReceiver();
+            executeShellCommand("pm uninstall " + packageName, receiver, INSTALL_TIMEOUT);
+            return receiver.getErrorMessage();
+        } catch (TimeoutException e) {
+            throw new InstallException(e);
+        } catch (AdbCommandRejectedException e) {
+            throw new InstallException(e);
+        } catch (ShellCommandUnresponsiveException e) {
+            throw new InstallException(e);
+        } catch (IOException e) {
+            throw new InstallException(e);
+        }
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.android.ddmlib.IDevice#reboot()
+     */
+    @Override
+    public void reboot(String into)
+            throws TimeoutException, AdbCommandRejectedException, IOException {
+        AdbHelper.reboot(into, AndroidDebugBridge.getSocketAddress(), this);
+    }
+
+    @Override
+    public Integer getBatteryLevel() throws TimeoutException, AdbCommandRejectedException,
+            IOException, ShellCommandUnresponsiveException {
+        // use default of 5 minutes
+        return getBatteryLevel(5 * 60 * 1000);
+    }
+
+    @Override
+    public Integer getBatteryLevel(long freshnessMs) throws TimeoutException,
+            AdbCommandRejectedException, IOException, ShellCommandUnresponsiveException {
+        if (mLastBatteryLevel != null
+                && mLastBatteryCheckTime > (System.currentTimeMillis() - freshnessMs)) {
+            return mLastBatteryLevel;
+        }
+        BatteryReceiver receiver = new BatteryReceiver();
+        executeShellCommand("dumpsys battery", receiver, BATTERY_TIMEOUT);
+        mLastBatteryLevel = receiver.getBatteryLevel();
+        mLastBatteryCheckTime = System.currentTimeMillis();
+        return mLastBatteryLevel;
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/DeviceMonitor.java b/ddmlib/src/main/java/com/android/ddmlib/DeviceMonitor.java
new file mode 100644
index 0000000..b177615
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/DeviceMonitor.java
@@ -0,0 +1,945 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import com.android.ddmlib.AdbHelper.AdbResponse;
+import com.android.ddmlib.ClientData.DebuggerStatus;
+import com.android.ddmlib.DebugPortManager.IDebugPortProvider;
+import com.android.ddmlib.IDevice.DeviceState;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.channels.AsynchronousCloseException;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.SocketChannel;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A Device monitor. This connects to the Android Debug Bridge and get device and
+ * debuggable process information from it.
+ */
+final class DeviceMonitor {
+    private byte[] mLengthBuffer = new byte[4];
+    private byte[] mLengthBuffer2 = new byte[4];
+
+    private boolean mQuit = false;
+
+    private AndroidDebugBridge mServer;
+
+    private SocketChannel mMainAdbConnection = null;
+    private boolean mMonitoring = false;
+    private int mConnectionAttempt = 0;
+    private int mRestartAttemptCount = 0;
+    private boolean mInitialDeviceListDone = false;
+
+    private Selector mSelector;
+
+    private final ArrayList<Device> mDevices = new ArrayList<Device>();
+
+    private final ArrayList<Integer> mDebuggerPorts = new ArrayList<Integer>();
+
+    private final HashMap<Client, Integer> mClientsToReopen = new HashMap<Client, Integer>();
+
+    /**
+     * Creates a new {@link DeviceMonitor} object and links it to the running
+     * {@link AndroidDebugBridge} object.
+     * @param server the running {@link AndroidDebugBridge}.
+     */
+    DeviceMonitor(AndroidDebugBridge server) {
+        mServer = server;
+
+        mDebuggerPorts.add(DdmPreferences.getDebugPortBase());
+    }
+
+    /**
+     * Starts the monitoring.
+     */
+    void start() {
+        new Thread("Device List Monitor") { //$NON-NLS-1$
+            @Override
+            public void run() {
+                deviceMonitorLoop();
+            }
+        }.start();
+    }
+
+    /**
+     * Stops the monitoring.
+     */
+    void stop() {
+        mQuit = true;
+
+        // wakeup the main loop thread by closing the main connection to adb.
+        try {
+            if (mMainAdbConnection != null) {
+                mMainAdbConnection.close();
+            }
+        } catch (IOException e1) {
+        }
+
+        // wake up the secondary loop by closing the selector.
+        if (mSelector != null) {
+            mSelector.wakeup();
+        }
+    }
+
+
+
+    /**
+     * Returns if the monitor is currently connected to the debug bridge server.
+     * @return
+     */
+    boolean isMonitoring() {
+        return mMonitoring;
+    }
+
+    int getConnectionAttemptCount() {
+        return mConnectionAttempt;
+    }
+
+    int getRestartAttemptCount() {
+        return mRestartAttemptCount;
+    }
+
+    /**
+     * Returns the devices.
+     */
+    Device[] getDevices() {
+        synchronized (mDevices) {
+            return mDevices.toArray(new Device[mDevices.size()]);
+        }
+    }
+
+    boolean hasInitialDeviceList() {
+        return mInitialDeviceListDone;
+    }
+
+    AndroidDebugBridge getServer() {
+        return mServer;
+    }
+
+    void addClientToDropAndReopen(Client client, int port) {
+        synchronized (mClientsToReopen) {
+            Log.d("DeviceMonitor",
+                    "Adding " + client + " to list of client to reopen (" + port +").");
+            if (mClientsToReopen.get(client) == null) {
+                mClientsToReopen.put(client, port);
+            }
+        }
+        mSelector.wakeup();
+    }
+
+    /**
+     * Monitors the devices. This connects to the Debug Bridge
+     */
+    private void deviceMonitorLoop() {
+        do {
+            try {
+                if (mMainAdbConnection == null) {
+                    Log.d("DeviceMonitor", "Opening adb connection");
+                    mMainAdbConnection = openAdbConnection();
+                    if (mMainAdbConnection == null) {
+                        mConnectionAttempt++;
+                        Log.e("DeviceMonitor", "Connection attempts: " + mConnectionAttempt);
+                        if (mConnectionAttempt > 10) {
+                            if (!mServer.startAdb()) {
+                                mRestartAttemptCount++;
+                                Log.e("DeviceMonitor",
+                                        "adb restart attempts: " + mRestartAttemptCount);
+                            } else {
+                                mRestartAttemptCount = 0;
+                            }
+                        }
+                        waitABit();
+                    } else {
+                        Log.d("DeviceMonitor", "Connected to adb for device monitoring");
+                        mConnectionAttempt = 0;
+                    }
+                }
+
+                if (mMainAdbConnection != null && !mMonitoring) {
+                    mMonitoring = sendDeviceListMonitoringRequest();
+                }
+
+                if (mMonitoring) {
+                    // read the length of the incoming message
+                    int length = readLength(mMainAdbConnection, mLengthBuffer);
+
+                    if (length >= 0) {
+                        // read the incoming message
+                        processIncomingDeviceData(length);
+
+                        // flag the fact that we have build the list at least once.
+                        mInitialDeviceListDone = true;
+                    }
+                }
+            } catch (AsynchronousCloseException ace) {
+                // this happens because of a call to Quit. We do nothing, and the loop will break.
+            } catch (TimeoutException ioe) {
+                handleExpectionInMonitorLoop(ioe);
+            } catch (IOException ioe) {
+                handleExpectionInMonitorLoop(ioe);
+            }
+        } while (!mQuit);
+    }
+
+    private void handleExpectionInMonitorLoop(Exception e) {
+        if (!mQuit) {
+            if (e instanceof TimeoutException) {
+                Log.e("DeviceMonitor", "Adb connection Error: timeout");
+            } else {
+                Log.e("DeviceMonitor", "Adb connection Error:" + e.getMessage());
+            }
+            mMonitoring = false;
+            if (mMainAdbConnection != null) {
+                try {
+                    mMainAdbConnection.close();
+                } catch (IOException ioe) {
+                    // we can safely ignore that one.
+                }
+                mMainAdbConnection = null;
+
+                // remove all devices from list
+                // because we are going to call mServer.deviceDisconnected which will acquire this
+                // lock we lock it first, so that the AndroidDebugBridge lock is always locked
+                // first.
+                synchronized (AndroidDebugBridge.getLock()) {
+                    synchronized (mDevices) {
+                        for (int n = mDevices.size() - 1; n >= 0; n--) {
+                            Device device = mDevices.get(0);
+                            removeDevice(device);
+                            mServer.deviceDisconnected(device);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Sleeps for a little bit.
+     */
+    private void waitABit() {
+        try {
+            Thread.sleep(1000);
+        } catch (InterruptedException e1) {
+        }
+    }
+
+    /**
+     * Attempts to connect to the debug bridge server.
+     * @return a connect socket if success, null otherwise
+     */
+    private SocketChannel openAdbConnection() {
+        Log.d("DeviceMonitor", "Connecting to adb for Device List Monitoring...");
+
+        SocketChannel adbChannel = null;
+        try {
+            adbChannel = SocketChannel.open(AndroidDebugBridge.getSocketAddress());
+            adbChannel.socket().setTcpNoDelay(true);
+        } catch (IOException e) {
+        }
+
+        return adbChannel;
+    }
+
+    /**
+     *
+     * @return
+     * @throws IOException
+     */
+    private boolean sendDeviceListMonitoringRequest() throws TimeoutException, IOException {
+        byte[] request = AdbHelper.formAdbRequest("host:track-devices"); //$NON-NLS-1$
+
+        try {
+            AdbHelper.write(mMainAdbConnection, request);
+
+            AdbResponse resp = AdbHelper.readAdbResponse(mMainAdbConnection,
+                    false /* readDiagString */);
+
+            if (!resp.okay) {
+                // request was refused by adb!
+                Log.e("DeviceMonitor", "adb refused request: " + resp.message);
+            }
+
+            return resp.okay;
+        } catch (IOException e) {
+            Log.e("DeviceMonitor", "Sending Tracking request failed!");
+            mMainAdbConnection.close();
+            throw e;
+        }
+    }
+
+    /**
+     * Processes an incoming device message from the socket
+     * @param socket
+     * @param length
+     * @throws IOException
+     */
+    private void processIncomingDeviceData(int length) throws IOException {
+        ArrayList<Device> list = new ArrayList<Device>();
+
+        if (length > 0) {
+            byte[] buffer = new byte[length];
+            String result = read(mMainAdbConnection, buffer);
+
+            String[] devices = result.split("\n"); //$NON-NLS-1$
+
+            for (String d : devices) {
+                String[] param = d.split("\t"); //$NON-NLS-1$
+                if (param.length == 2) {
+                    // new adb uses only serial numbers to identify devices
+                    Device device = new Device(this, param[0] /*serialnumber*/,
+                            DeviceState.getState(param[1]));
+
+                    //add the device to the list
+                    list.add(device);
+                }
+            }
+        }
+
+        // now merge the new devices with the old ones.
+        updateDevices(list);
+    }
+
+    /**
+     *  Updates the device list with the new items received from the monitoring service.
+     */
+    private void updateDevices(ArrayList<Device> newList) {
+        // because we are going to call mServer.deviceDisconnected which will acquire this lock
+        // we lock it first, so that the AndroidDebugBridge lock is always locked first.
+        synchronized (AndroidDebugBridge.getLock()) {
+            // array to store the devices that must be queried for information.
+            // it's important to not do it inside the synchronized loop as this could block
+            // the whole workspace (this lock is acquired during build too).
+            ArrayList<Device> devicesToQuery = new ArrayList<Device>();
+            synchronized (mDevices) {
+                // For each device in the current list, we look for a matching the new list.
+                // * if we find it, we update the current object with whatever new information
+                //   there is
+                //   (mostly state change, if the device becomes ready, we query for build info).
+                //   We also remove the device from the new list to mark it as "processed"
+                // * if we do not find it, we remove it from the current list.
+                // Once this is done, the new list contains device we aren't monitoring yet, so we
+                // add them to the list, and start monitoring them.
+
+                for (int d = 0 ; d < mDevices.size() ;) {
+                    Device device = mDevices.get(d);
+
+                    // look for a similar device in the new list.
+                    int count = newList.size();
+                    boolean foundMatch = false;
+                    for (int dd = 0 ; dd < count ; dd++) {
+                        Device newDevice = newList.get(dd);
+                        // see if it matches in id and serial number.
+                        if (newDevice.getSerialNumber().equals(device.getSerialNumber())) {
+                            foundMatch = true;
+
+                            // update the state if needed.
+                            if (device.getState() != newDevice.getState()) {
+                                device.setState(newDevice.getState());
+                                device.update(Device.CHANGE_STATE);
+
+                                // if the device just got ready/online, we need to start
+                                // monitoring it.
+                                if (device.isOnline()) {
+                                    if (AndroidDebugBridge.getClientSupport()) {
+                                        if (!startMonitoringDevice(device)) {
+                                            Log.e("DeviceMonitor",
+                                                    "Failed to start monitoring "
+                                                    + device.getSerialNumber());
+                                        }
+                                    }
+
+                                    if (device.getPropertyCount() == 0) {
+                                        devicesToQuery.add(device);
+                                    }
+                                }
+                            }
+
+                            // remove the new device from the list since it's been used
+                            newList.remove(dd);
+                            break;
+                        }
+                    }
+
+                    if (!foundMatch) {
+                        // the device is gone, we need to remove it, and keep current index
+                        // to process the next one.
+                        removeDevice(device);
+                        mServer.deviceDisconnected(device);
+                    } else {
+                        // process the next one
+                        d++;
+                    }
+                }
+
+                // at this point we should still have some new devices in newList, so we
+                // process them.
+                for (Device newDevice : newList) {
+                    // add them to the list
+                    mDevices.add(newDevice);
+                    mServer.deviceConnected(newDevice);
+
+                    // start monitoring them.
+                    if (AndroidDebugBridge.getClientSupport()) {
+                        if (newDevice.isOnline()) {
+                            startMonitoringDevice(newDevice);
+                        }
+                    }
+
+                    // look for their build info.
+                    if (newDevice.isOnline()) {
+                        devicesToQuery.add(newDevice);
+                    }
+                }
+            }
+
+            // query the new devices for info.
+            for (Device d : devicesToQuery) {
+                queryNewDeviceForInfo(d);
+            }
+        }
+        newList.clear();
+    }
+
+    private void removeDevice(Device device) {
+        device.clearClientList();
+        mDevices.remove(device);
+
+        SocketChannel channel = device.getClientMonitoringSocket();
+        if (channel != null) {
+            try {
+                channel.close();
+            } catch (IOException e) {
+                // doesn't really matter if the close fails.
+            }
+        }
+    }
+
+    /**
+     * Queries a device for its build info.
+     * @param device the device to query.
+     */
+    private void queryNewDeviceForInfo(Device device) {
+        // TODO: do this in a separate thread.
+        try {
+            // first get the list of properties.
+            device.executeShellCommand(GetPropReceiver.GETPROP_COMMAND,
+                    new GetPropReceiver(device));
+
+            queryNewDeviceForMountingPoint(device, IDevice.MNT_EXTERNAL_STORAGE);
+            queryNewDeviceForMountingPoint(device, IDevice.MNT_DATA);
+            queryNewDeviceForMountingPoint(device, IDevice.MNT_ROOT);
+
+            // now get the emulator Virtual Device name (if applicable).
+            if (device.isEmulator()) {
+                EmulatorConsole console = EmulatorConsole.getConsole(device);
+                if (console != null) {
+                    device.setAvdName(console.getAvdName());
+                }
+            }
+        } catch (TimeoutException e) {
+            Log.w("DeviceMonitor", String.format("Connection timeout getting info for device %s",
+                    device.getSerialNumber()));
+
+        } catch (AdbCommandRejectedException e) {
+            // This should never happen as we only do this once the device is online.
+            Log.w("DeviceMonitor", String.format(
+                    "Adb rejected command to get  device %1$s info: %2$s",
+                    device.getSerialNumber(), e.getMessage()));
+
+        } catch (ShellCommandUnresponsiveException e) {
+            Log.w("DeviceMonitor", String.format(
+                    "Adb shell command took too long returning info for device %s",
+                    device.getSerialNumber()));
+
+        } catch (IOException e) {
+            Log.w("DeviceMonitor", String.format(
+                    "IO Error getting info for device %s",
+                    device.getSerialNumber()));
+        }
+    }
+
+    private void queryNewDeviceForMountingPoint(final Device device, final String name)
+            throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException,
+            IOException {
+        device.executeShellCommand("echo $" + name, new MultiLineReceiver() { //$NON-NLS-1$
+            @Override
+            public boolean isCancelled() {
+                return false;
+            }
+
+            @Override
+            public void processNewLines(String[] lines) {
+                for (String line : lines) {
+                    if (!line.isEmpty()) {
+                        // this should be the only one.
+                        device.setMountingPoint(name, line);
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * Starts a monitoring service for a device.
+     * @param device the device to monitor.
+     * @return true if success.
+     */
+    private boolean startMonitoringDevice(Device device) {
+        SocketChannel socketChannel = openAdbConnection();
+
+        if (socketChannel != null) {
+            try {
+                boolean result = sendDeviceMonitoringRequest(socketChannel, device);
+                if (result) {
+
+                    if (mSelector == null) {
+                        startDeviceMonitorThread();
+                    }
+
+                    device.setClientMonitoringSocket(socketChannel);
+
+                    synchronized (mDevices) {
+                        // always wakeup before doing the register. The synchronized block
+                        // ensure that the selector won't select() before the end of this block.
+                        // @see deviceClientMonitorLoop
+                        mSelector.wakeup();
+
+                        socketChannel.configureBlocking(false);
+                        socketChannel.register(mSelector, SelectionKey.OP_READ, device);
+                    }
+
+                    return true;
+                }
+            } catch (TimeoutException e) {
+                try {
+                    // attempt to close the socket if needed.
+                    socketChannel.close();
+                } catch (IOException e1) {
+                    // we can ignore that one. It may already have been closed.
+                }
+                Log.d("DeviceMonitor",
+                        "Connection Failure when starting to monitor device '"
+                        + device + "' : timeout");
+            } catch (AdbCommandRejectedException e) {
+                try {
+                    // attempt to close the socket if needed.
+                    socketChannel.close();
+                } catch (IOException e1) {
+                    // we can ignore that one. It may already have been closed.
+                }
+                Log.d("DeviceMonitor",
+                        "Adb refused to start monitoring device '"
+                        + device + "' : " + e.getMessage());
+            } catch (IOException e) {
+                try {
+                    // attempt to close the socket if needed.
+                    socketChannel.close();
+                } catch (IOException e1) {
+                    // we can ignore that one. It may already have been closed.
+                }
+                Log.d("DeviceMonitor",
+                        "Connection Failure when starting to monitor device '"
+                        + device + "' : " + e.getMessage());
+            }
+        }
+
+        return false;
+    }
+
+    private void startDeviceMonitorThread() throws IOException {
+        mSelector = Selector.open();
+        new Thread("Device Client Monitor") { //$NON-NLS-1$
+            @Override
+            public void run() {
+                deviceClientMonitorLoop();
+            }
+        }.start();
+    }
+
+    private void deviceClientMonitorLoop() {
+        do {
+            try {
+                // This synchronized block stops us from doing the select() if a new
+                // Device is being added.
+                // @see startMonitoringDevice()
+                synchronized (mDevices) {
+                }
+
+                int count = mSelector.select();
+
+                if (mQuit) {
+                    return;
+                }
+
+                synchronized (mClientsToReopen) {
+                    if (!mClientsToReopen.isEmpty()) {
+                        Set<Client> clients = mClientsToReopen.keySet();
+                        MonitorThread monitorThread = MonitorThread.getInstance();
+
+                        for (Client client : clients) {
+                            Device device = client.getDeviceImpl();
+                            int pid = client.getClientData().getPid();
+
+                            monitorThread.dropClient(client, false /* notify */);
+
+                            // This is kinda bad, but if we don't wait a bit, the client
+                            // will never answer the second handshake!
+                            waitABit();
+
+                            int port = mClientsToReopen.get(client);
+
+                            if (port == IDebugPortProvider.NO_STATIC_PORT) {
+                                port = getNextDebuggerPort();
+                            }
+                            Log.d("DeviceMonitor", "Reopening " + client);
+                            openClient(device, pid, port, monitorThread);
+                            device.update(Device.CHANGE_CLIENT_LIST);
+                        }
+
+                        mClientsToReopen.clear();
+                    }
+                }
+
+                if (count == 0) {
+                    continue;
+                }
+
+                Set<SelectionKey> keys = mSelector.selectedKeys();
+                Iterator<SelectionKey> iter = keys.iterator();
+
+                while (iter.hasNext()) {
+                    SelectionKey key = iter.next();
+                    iter.remove();
+
+                    if (key.isValid() && key.isReadable()) {
+                        Object attachment = key.attachment();
+
+                        if (attachment instanceof Device) {
+                            Device device = (Device)attachment;
+
+                            SocketChannel socket = device.getClientMonitoringSocket();
+
+                            if (socket != null) {
+                                try {
+                                    int length = readLength(socket, mLengthBuffer2);
+
+                                    processIncomingJdwpData(device, socket, length);
+                                } catch (IOException ioe) {
+                                    Log.d("DeviceMonitor",
+                                            "Error reading jdwp list: " + ioe.getMessage());
+                                    socket.close();
+
+                                    // restart the monitoring of that device
+                                    synchronized (mDevices) {
+                                        if (mDevices.contains(device)) {
+                                            Log.d("DeviceMonitor",
+                                                    "Restarting monitoring service for " + device);
+                                            startMonitoringDevice(device);
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            } catch (IOException e) {
+                Log.e("DeviceMonitor", "Connection error while monitoring clients.");
+            }
+
+        } while (!mQuit);
+    }
+
+    private boolean sendDeviceMonitoringRequest(SocketChannel socket, Device device)
+            throws TimeoutException, AdbCommandRejectedException, IOException {
+
+        try {
+            AdbHelper.setDevice(socket, device);
+
+            byte[] request = AdbHelper.formAdbRequest("track-jdwp"); //$NON-NLS-1$
+
+            AdbHelper.write(socket, request);
+
+            AdbResponse resp = AdbHelper.readAdbResponse(socket, false /* readDiagString */);
+
+            if (!resp.okay) {
+                // request was refused by adb!
+                Log.e("DeviceMonitor", "adb refused request: " + resp.message);
+            }
+
+            return resp.okay;
+        } catch (TimeoutException e) {
+            Log.e("DeviceMonitor", "Sending jdwp tracking request timed out!");
+            throw e;
+        } catch (IOException e) {
+            Log.e("DeviceMonitor", "Sending jdwp tracking request failed!");
+            throw e;
+        }
+    }
+
+    private void processIncomingJdwpData(Device device, SocketChannel monitorSocket, int length)
+            throws IOException {
+
+        // This methods reads @length bytes from the @monitorSocket channel.
+        // These bytes correspond to the pids of the current set of processes on the device.
+        // It takes this set of pids and compares them with the existing set of clients
+        // for the device. Clients that correspond to pids that are not alive anymore are
+        // dropped, and new clients are created for pids that don't have a corresponding Client.
+
+        if (length >= 0) {
+            // array for the current pids.
+            Set<Integer> newPids = new HashSet<Integer>();
+
+            // get the string data if there are any
+            if (length > 0) {
+                byte[] buffer = new byte[length];
+                String result = read(monitorSocket, buffer);
+
+                // split each line in its own list and create an array of integer pid
+                String[] pids = result.split("\n"); //$NON-NLS-1$
+
+                for (String pid : pids) {
+                    try {
+                        newPids.add(Integer.valueOf(pid));
+                    } catch (NumberFormatException nfe) {
+                        // looks like this pid is not really a number. Lets ignore it.
+                        continue;
+                    }
+                }
+            }
+
+            MonitorThread monitorThread = MonitorThread.getInstance();
+
+            List<Client> clients = device.getClientList();
+            Map<Integer, Client> existingClients = new HashMap<Integer, Client>();
+
+            synchronized (clients) {
+                for (Client c : clients) {
+                    existingClients.put(
+                            c.getClientData().getPid(),
+                            c);
+                }
+            }
+
+            Set<Client> clientsToRemove = new HashSet<Client>();
+            for (Integer pid : existingClients.keySet()) {
+                if (!newPids.contains(pid)) {
+                    clientsToRemove.add(existingClients.get(pid));
+                }
+            }
+
+            Set<Integer> pidsToAdd = new HashSet<Integer>(newPids);
+            pidsToAdd.removeAll(existingClients.keySet());
+
+            monitorThread.dropClients(clientsToRemove, false);
+
+            // at this point whatever pid is left in the list needs to be converted into Clients.
+            for (int newPid : pidsToAdd) {
+                openClient(device, newPid, getNextDebuggerPort(), monitorThread);
+            }
+
+            if (!pidsToAdd.isEmpty() || !clientsToRemove.isEmpty()) {
+                mServer.deviceChanged(device, Device.CHANGE_CLIENT_LIST);
+            }
+        }
+    }
+
+    /**
+     * Opens and creates a new client.
+     * @return
+     */
+    private void openClient(Device device, int pid, int port, MonitorThread monitorThread) {
+
+        SocketChannel clientSocket;
+        try {
+            clientSocket = AdbHelper.createPassThroughConnection(
+                    AndroidDebugBridge.getSocketAddress(), device, pid);
+
+            // required for Selector
+            clientSocket.configureBlocking(false);
+        } catch (UnknownHostException uhe) {
+            Log.d("DeviceMonitor", "Unknown Jdwp pid: " + pid);
+            return;
+        } catch (TimeoutException e) {
+            Log.w("DeviceMonitor",
+                    "Failed to connect to client '" + pid + "': timeout");
+            return;
+        } catch (AdbCommandRejectedException e) {
+            Log.w("DeviceMonitor",
+                    "Adb rejected connection to client '" + pid + "': " + e.getMessage());
+            return;
+
+        } catch (IOException ioe) {
+            Log.w("DeviceMonitor",
+                    "Failed to connect to client '" + pid + "': " + ioe.getMessage());
+            return ;
+        }
+
+        createClient(device, pid, clientSocket, port, monitorThread);
+    }
+
+    /**
+     * Creates a client and register it to the monitor thread
+     * @param device
+     * @param pid
+     * @param socket
+     * @param debuggerPort the debugger port.
+     * @param monitorThread the {@link MonitorThread} object.
+     */
+    private void createClient(Device device, int pid, SocketChannel socket, int debuggerPort,
+            MonitorThread monitorThread) {
+
+        /*
+         * Successfully connected to something. Create a Client object, add
+         * it to the list, and initiate the JDWP handshake.
+         */
+
+        Client client = new Client(device, socket, pid);
+
+        if (client.sendHandshake()) {
+            try {
+                if (AndroidDebugBridge.getClientSupport()) {
+                    client.listenForDebugger(debuggerPort);
+                }
+            } catch (IOException ioe) {
+                client.getClientData().setDebuggerConnectionStatus(DebuggerStatus.ERROR);
+                Log.e("ddms", "Can't bind to local " + debuggerPort + " for debugger");
+                // oh well
+            }
+
+            client.requestAllocationStatus();
+        } else {
+            Log.e("ddms", "Handshake with " + client + " failed!");
+            /*
+             * The handshake send failed. We could remove it now, but if the
+             * failure is "permanent" we'll just keep banging on it and
+             * getting the same result. Keep it in the list with its "error"
+             * state so we don't try to reopen it.
+             */
+        }
+
+        if (client.isValid()) {
+            device.addClient(client);
+            monitorThread.addClient(client);
+        } else {
+            client = null;
+        }
+    }
+
+    private int getNextDebuggerPort() {
+        // get the first port and remove it
+        synchronized (mDebuggerPorts) {
+            if (!mDebuggerPorts.isEmpty()) {
+                int port = mDebuggerPorts.get(0);
+
+                // remove it.
+                mDebuggerPorts.remove(0);
+
+                // if there's nothing left, add the next port to the list
+                if (mDebuggerPorts.isEmpty()) {
+                    mDebuggerPorts.add(port+1);
+                }
+
+                return port;
+            }
+        }
+
+        return -1;
+    }
+
+    void addPortToAvailableList(int port) {
+        if (port > 0) {
+            synchronized (mDebuggerPorts) {
+                // because there could be case where clients are closed twice, we have to make
+                // sure the port number is not already in the list.
+                if (mDebuggerPorts.indexOf(port) == -1) {
+                    // add the port to the list while keeping it sorted. It's not like there's
+                    // going to be tons of objects so we do it linearly.
+                    int count = mDebuggerPorts.size();
+                    for (int i = 0 ; i < count ; i++) {
+                        if (port < mDebuggerPorts.get(i)) {
+                            mDebuggerPorts.add(i, port);
+                            break;
+                        }
+                    }
+                    // TODO: check if we can compact the end of the list.
+                }
+            }
+        }
+    }
+
+    /**
+     * Reads the length of the next message from a socket.
+     * @param socket The {@link SocketChannel} to read from.
+     * @return the length, or 0 (zero) if no data is available from the socket.
+     * @throws IOException if the connection failed.
+     */
+    private int readLength(SocketChannel socket, byte[] buffer) throws IOException {
+        String msg = read(socket, buffer);
+
+        if (msg != null) {
+            try {
+                return Integer.parseInt(msg, 16);
+            } catch (NumberFormatException nfe) {
+                // we'll throw an exception below.
+            }
+       }
+
+        // we receive something we can't read. It's better to reset the connection at this point.
+        throw new IOException("Unable to read length");
+    }
+
+    /**
+     * Fills a buffer from a socket.
+     * @param socket
+     * @param buffer
+     * @return the content of the buffer as a string, or null if it failed to convert the buffer.
+     * @throws IOException
+     */
+    private String read(SocketChannel socket, byte[] buffer) throws IOException {
+        ByteBuffer buf = ByteBuffer.wrap(buffer, 0, buffer.length);
+
+        while (buf.position() != buf.limit()) {
+            int count;
+
+            count = socket.read(buf);
+            if (count < 0) {
+                throw new IOException("EOF");
+            }
+        }
+
+        try {
+            return new String(buffer, 0, buf.position(), AdbHelper.DEFAULT_ENCODING);
+        } catch (UnsupportedEncodingException e) {
+            // we'll return null below.
+        }
+
+        return null;
+    }
+
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/EmulatorConsole.java b/ddmlib/src/main/java/com/android/ddmlib/EmulatorConsole.java
new file mode 100644
index 0000000..4a87625
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/EmulatorConsole.java
@@ -0,0 +1,740 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.security.InvalidParameterException;
+import java.util.Formatter;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Provides control over emulated hardware of the Android emulator.
+ * <p/>This is basically a wrapper around the command line console normally used with telnet.
+ *<p/>
+ * Regarding line termination handling:<br>
+ * One of the issues is that the telnet protocol <b>requires</b> usage of <code>\r\n</code>. Most
+ * implementations don't enforce it (the dos one does). In this particular case, this is mostly
+ * irrelevant since we don't use telnet in Java, but that means we want to make
+ * sure we use the same line termination than what the console expects. The console
+ * code removes <code>\r</code> and waits for <code>\n</code>.
+ * <p/>However this means you <i>may</i> receive <code>\r\n</code> when reading from the console.
+ * <p/>
+ * <b>This API will change in the near future.</b>
+ */
+public final class EmulatorConsole {
+
+    private static final String DEFAULT_ENCODING = "ISO-8859-1"; //$NON-NLS-1$
+
+    private static final int WAIT_TIME = 5; // spin-wait sleep, in ms
+
+    private static final int STD_TIMEOUT = 5000; // standard delay, in ms
+
+    private static final String HOST = "127.0.0.1";  //$NON-NLS-1$
+
+    private static final String COMMAND_PING = "help\r\n"; //$NON-NLS-1$
+    private static final String COMMAND_AVD_NAME = "avd name\r\n"; //$NON-NLS-1$
+    private static final String COMMAND_KILL = "kill\r\n"; //$NON-NLS-1$
+    private static final String COMMAND_GSM_STATUS = "gsm status\r\n"; //$NON-NLS-1$
+    private static final String COMMAND_GSM_CALL = "gsm call %1$s\r\n"; //$NON-NLS-1$
+    private static final String COMMAND_GSM_CANCEL_CALL = "gsm cancel %1$s\r\n"; //$NON-NLS-1$
+    private static final String COMMAND_GSM_DATA = "gsm data %1$s\r\n"; //$NON-NLS-1$
+    private static final String COMMAND_GSM_VOICE = "gsm voice %1$s\r\n"; //$NON-NLS-1$
+    private static final String COMMAND_SMS_SEND = "sms send %1$s %2$s\r\n"; //$NON-NLS-1$
+    private static final String COMMAND_NETWORK_STATUS = "network status\r\n"; //$NON-NLS-1$
+    private static final String COMMAND_NETWORK_SPEED = "network speed %1$s\r\n"; //$NON-NLS-1$
+    private static final String COMMAND_NETWORK_LATENCY = "network delay %1$s\r\n"; //$NON-NLS-1$
+    private static final String COMMAND_GPS = "geo fix %1$f %2$f %3$f\r\n"; //$NON-NLS-1$
+
+    private static final Pattern RE_KO = Pattern.compile("KO:\\s+(.*)"); //$NON-NLS-1$
+
+    /**
+     * Array of delay values: no delay, gprs, edge/egprs, umts/3d
+     */
+    public static final int[] MIN_LATENCIES = new int[] {
+        0,      // No delay
+        150,    // gprs
+        80,     // edge/egprs
+        35      // umts/3g
+    };
+
+    /**
+     * Array of download speeds: full speed, gsm, hscsd, gprs, edge/egprs, umts/3g, hsdpa.
+     */
+    public static final int[] DOWNLOAD_SPEEDS = new int[] {
+        0,          // full speed
+        14400,      // gsm
+        43200,      // hscsd
+        80000,      // gprs
+        236800,     // edge/egprs
+        1920000,    // umts/3g
+        14400000    // hsdpa
+    };
+
+    /** Arrays of valid network speeds */
+    public static final String[] NETWORK_SPEEDS = new String[] {
+        "full", //$NON-NLS-1$
+        "gsm", //$NON-NLS-1$
+        "hscsd", //$NON-NLS-1$
+        "gprs", //$NON-NLS-1$
+        "edge", //$NON-NLS-1$
+        "umts", //$NON-NLS-1$
+        "hsdpa", //$NON-NLS-1$
+    };
+
+    /** Arrays of valid network latencies */
+    public static final String[] NETWORK_LATENCIES = new String[] {
+        "none", //$NON-NLS-1$
+        "gprs", //$NON-NLS-1$
+        "edge", //$NON-NLS-1$
+        "umts", //$NON-NLS-1$
+    };
+
+    /** Gsm Mode enum. */
+    public static enum GsmMode {
+        UNKNOWN((String)null),
+        UNREGISTERED(new String[] { "unregistered", "off" }),
+        HOME(new String[] { "home", "on" }),
+        ROAMING("roaming"),
+        SEARCHING("searching"),
+        DENIED("denied");
+
+        private final String[] tags;
+
+        GsmMode(String tag) {
+            if (tag != null) {
+                this.tags = new String[] { tag };
+            } else {
+                this.tags = new String[0];
+            }
+        }
+
+        GsmMode(String[] tags) {
+            this.tags = tags;
+        }
+
+        public static GsmMode getEnum(String tag) {
+            for (GsmMode mode : values()) {
+                for (String t : mode.tags) {
+                    if (t.equals(tag)) {
+                        return mode;
+                    }
+                }
+            }
+            return UNKNOWN;
+        }
+
+        /**
+         * Returns the first tag of the enum.
+         */
+        public String getTag() {
+            if (tags.length > 0) {
+                return tags[0];
+            }
+            return null;
+        }
+    }
+
+    public static final String RESULT_OK = null;
+
+    private static final Pattern sEmulatorRegexp = Pattern.compile(Device.RE_EMULATOR_SN);
+    private static final Pattern sVoiceStatusRegexp = Pattern.compile(
+            "gsm\\s+voice\\s+state:\\s*([a-z]+)", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
+    private static final Pattern sDataStatusRegexp = Pattern.compile(
+            "gsm\\s+data\\s+state:\\s*([a-z]+)", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
+    private static final Pattern sDownloadSpeedRegexp = Pattern.compile(
+            "\\s+download\\s+speed:\\s+(\\d+)\\s+bits.*", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
+    private static final Pattern sMinLatencyRegexp = Pattern.compile(
+            "\\s+minimum\\s+latency:\\s+(\\d+)\\s+ms", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
+
+    private static final HashMap<Integer, EmulatorConsole> sEmulators =
+        new HashMap<Integer, EmulatorConsole>();
+
+    /** Gsm Status class */
+    public static class GsmStatus {
+        /** Voice status. */
+        public GsmMode voice = GsmMode.UNKNOWN;
+        /** Data status. */
+        public GsmMode data = GsmMode.UNKNOWN;
+    }
+
+    /** Network Status class */
+    public static class NetworkStatus {
+        /** network speed status. This is an index in the {@link #DOWNLOAD_SPEEDS} array. */
+        public int speed = -1;
+        /** network latency status.  This is an index in the {@link #MIN_LATENCIES} array. */
+        public int latency = -1;
+    }
+
+    private int mPort;
+
+    private SocketChannel mSocketChannel;
+
+    private byte[] mBuffer = new byte[1024];
+
+    /**
+     * Returns an {@link EmulatorConsole} object for the given {@link Device}. This can
+     * be an already existing console, or a new one if it hadn't been created yet.
+     * @param d The device that the console links to.
+     * @return an <code>EmulatorConsole</code> object or <code>null</code> if the connection failed.
+     */
+    public static synchronized EmulatorConsole getConsole(IDevice d) {
+        // we need to make sure that the device is an emulator
+        // get the port number. This is the console port.
+        Integer port = getEmulatorPort(d.getSerialNumber());
+        if (port == null) {
+            return null;
+        }
+
+        EmulatorConsole console = sEmulators.get(port);
+
+        if (console != null) {
+            // if the console exist, we ping the emulator to check the connection.
+            if (!console.ping()) {
+                RemoveConsole(console.mPort);
+                console = null;
+            }
+        }
+
+        if (console == null) {
+            // no console object exists for this port so we create one, and start
+            // the connection.
+            console = new EmulatorConsole(port);
+            if (console.start()) {
+                sEmulators.put(port, console);
+            } else {
+                console = null;
+            }
+        }
+
+        return console;
+    }
+
+    /**
+     * Return port of emulator given its serial number.
+     *
+     * @param serialNumber the emulator's serial number
+     * @return the integer port or <code>null</code> if it could not be determined
+     */
+    public static Integer getEmulatorPort(String serialNumber) {
+        Matcher m = sEmulatorRegexp.matcher(serialNumber);
+        if (m.matches()) {
+            // get the port number. This is the console port.
+            int port;
+            try {
+                port = Integer.parseInt(m.group(1));
+                if (port > 0) {
+                    return port;
+                }
+            } catch (NumberFormatException e) {
+                // looks like we failed to get the port number. This is a bit strange since
+                // it's coming from a regexp that only accept digit, but we handle the case
+                // and return null.
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Removes the console object associated with a port from the map.
+     * @param port The port of the console to remove.
+     */
+    private static synchronized void RemoveConsole(int port) {
+        sEmulators.remove(port);
+    }
+
+    private EmulatorConsole(int port) {
+        super();
+        mPort = port;
+    }
+
+    /**
+     * Starts the connection of the console.
+     * @return true if success.
+     */
+    private boolean start() {
+
+        InetSocketAddress socketAddr;
+        try {
+            InetAddress hostAddr = InetAddress.getByName(HOST);
+            socketAddr = new InetSocketAddress(hostAddr, mPort);
+        } catch (UnknownHostException e) {
+            return false;
+        }
+
+        try {
+            mSocketChannel = SocketChannel.open(socketAddr);
+        } catch (IOException e1) {
+            return false;
+        }
+
+        // read some stuff from it
+        readLines();
+
+        return true;
+    }
+
+    /**
+     * Ping the emulator to check if the connection is still alive.
+     * @return true if the connection is alive.
+     */
+    private synchronized boolean ping() {
+        // it looks like we can send stuff, even when the emulator quit, but we can't read
+        // from the socket. So we check the return of readLines()
+        if (sendCommand(COMMAND_PING)) {
+            return readLines() != null;
+        }
+
+        return false;
+    }
+
+    /**
+     * Sends a KILL command to the emulator.
+     */
+    public synchronized void kill() {
+        if (sendCommand(COMMAND_KILL)) {
+            RemoveConsole(mPort);
+        }
+    }
+
+    public synchronized String getAvdName() {
+        if (sendCommand(COMMAND_AVD_NAME)) {
+            String[] result = readLines();
+            if (result != null && result.length == 2) { // this should be the name on first line,
+                                                        // and ok on 2nd line
+                return result[0];
+            } else {
+                // try to see if there's a message after KO
+                Matcher m = RE_KO.matcher(result[result.length-1]);
+                if (m.matches()) {
+                    return m.group(1);
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Get the network status of the emulator.
+     * @return a {@link NetworkStatus} object containing the {@link GsmStatus}, or
+     * <code>null</code> if the query failed.
+     */
+    public synchronized NetworkStatus getNetworkStatus() {
+        if (sendCommand(COMMAND_NETWORK_STATUS)) {
+            /* Result is in the format
+                Current network status:
+                download speed:      14400 bits/s (1.8 KB/s)
+                upload speed:        14400 bits/s (1.8 KB/s)
+                minimum latency:  0 ms
+                maximum latency:  0 ms
+             */
+            String[] result = readLines();
+
+            if (isValid(result)) {
+                // we only compare against the min latency and the download speed
+                // let's not rely on the order of the output, and simply loop through
+                // the line testing the regexp.
+                NetworkStatus status = new NetworkStatus();
+                for (String line : result) {
+                    Matcher m = sDownloadSpeedRegexp.matcher(line);
+                    if (m.matches()) {
+                        // get the string value
+                        String value = m.group(1);
+
+                        // get the index from the list
+                        status.speed = getSpeedIndex(value);
+
+                        // move on to next line.
+                        continue;
+                    }
+
+                    m = sMinLatencyRegexp.matcher(line);
+                    if (m.matches()) {
+                        // get the string value
+                        String value = m.group(1);
+
+                        // get the index from the list
+                        status.latency = getLatencyIndex(value);
+
+                        // move on to next line.
+                        continue;
+                    }
+                }
+
+                return status;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns the current gsm status of the emulator
+     * @return a {@link GsmStatus} object containing the gms status, or <code>null</code>
+     * if the query failed.
+     */
+    public synchronized GsmStatus getGsmStatus() {
+        if (sendCommand(COMMAND_GSM_STATUS)) {
+            /*
+             * result is in the format:
+             * gsm status
+             * gsm voice state: home
+             * gsm data state:  home
+             */
+
+            String[] result = readLines();
+            if (isValid(result)) {
+
+                GsmStatus status = new GsmStatus();
+
+                // let's not rely on the order of the output, and simply loop through
+                // the line testing the regexp.
+                for (String line : result) {
+                    Matcher m = sVoiceStatusRegexp.matcher(line);
+                    if (m.matches()) {
+                        // get the string value
+                        String value = m.group(1);
+
+                        // get the index from the list
+                        status.voice = GsmMode.getEnum(value.toLowerCase(Locale.US));
+
+                        // move on to next line.
+                        continue;
+                    }
+
+                    m = sDataStatusRegexp.matcher(line);
+                    if (m.matches()) {
+                        // get the string value
+                        String value = m.group(1);
+
+                        // get the index from the list
+                        status.data = GsmMode.getEnum(value.toLowerCase(Locale.US));
+
+                        // move on to next line.
+                        continue;
+                    }
+                }
+
+                return status;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Sets the GSM voice mode.
+     * @param mode the {@link GsmMode} value.
+     * @return RESULT_OK if success, an error String otherwise.
+     * @throws InvalidParameterException if mode is an invalid value.
+     */
+    public synchronized String setGsmVoiceMode(GsmMode mode) throws InvalidParameterException {
+        if (mode == GsmMode.UNKNOWN) {
+            throw new InvalidParameterException();
+        }
+
+        String command = String.format(COMMAND_GSM_VOICE, mode.getTag());
+        return processCommand(command);
+    }
+
+    /**
+     * Sets the GSM data mode.
+     * @param mode the {@link GsmMode} value
+     * @return {@link #RESULT_OK} if success, an error String otherwise.
+     * @throws InvalidParameterException if mode is an invalid value.
+     */
+    public synchronized String setGsmDataMode(GsmMode mode) throws InvalidParameterException {
+        if (mode == GsmMode.UNKNOWN) {
+            throw new InvalidParameterException();
+        }
+
+        String command = String.format(COMMAND_GSM_DATA, mode.getTag());
+        return processCommand(command);
+    }
+
+    /**
+     * Initiate an incoming call on the emulator.
+     * @param number a string representing the calling number.
+     * @return {@link #RESULT_OK} if success, an error String otherwise.
+     */
+    public synchronized String call(String number) {
+        String command = String.format(COMMAND_GSM_CALL, number);
+        return processCommand(command);
+    }
+
+    /**
+     * Cancels a current call.
+     * @param number the number of the call to cancel
+     * @return {@link #RESULT_OK} if success, an error String otherwise.
+     */
+    public synchronized String cancelCall(String number) {
+        String command = String.format(COMMAND_GSM_CANCEL_CALL, number);
+        return processCommand(command);
+    }
+
+    /**
+     * Sends an SMS to the emulator
+     * @param number The sender phone number
+     * @param message The SMS message. \ characters must be escaped. The carriage return is
+     * the 2 character sequence  {'\', 'n' }
+     *
+     * @return {@link #RESULT_OK} if success, an error String otherwise.
+     */
+    public synchronized String sendSms(String number, String message) {
+        String command = String.format(COMMAND_SMS_SEND, number, message);
+        return processCommand(command);
+    }
+
+    /**
+     * Sets the network speed.
+     * @param selectionIndex The index in the {@link #NETWORK_SPEEDS} table.
+     * @return {@link #RESULT_OK} if success, an error String otherwise.
+     */
+    public synchronized String setNetworkSpeed(int selectionIndex) {
+        String command = String.format(COMMAND_NETWORK_SPEED, NETWORK_SPEEDS[selectionIndex]);
+        return processCommand(command);
+    }
+
+    /**
+     * Sets the network latency.
+     * @param selectionIndex The index in the {@link #NETWORK_LATENCIES} table.
+     * @return {@link #RESULT_OK} if success, an error String otherwise.
+     */
+    public synchronized String setNetworkLatency(int selectionIndex) {
+        String command = String.format(COMMAND_NETWORK_LATENCY, NETWORK_LATENCIES[selectionIndex]);
+        return processCommand(command);
+    }
+
+    public synchronized String sendLocation(double longitude, double latitude, double elevation) {
+
+        // need to make sure the string format uses dot and not comma
+        Formatter formatter = new Formatter(Locale.US);
+        try {
+            formatter.format(COMMAND_GPS, longitude, latitude, elevation);
+
+            return processCommand(formatter.toString());
+        } finally {
+            formatter.close();
+        }
+    }
+
+    /**
+     * Sends a command to the emulator console.
+     * @param command The command string. <b>MUST BE TERMINATED BY \n</b>.
+     * @return true if success
+     */
+    private boolean sendCommand(String command) {
+        boolean result = false;
+        try {
+            byte[] bCommand;
+            try {
+                bCommand = command.getBytes(DEFAULT_ENCODING);
+            } catch (UnsupportedEncodingException e) {
+                // wrong encoding...
+                return result;
+            }
+
+            // write the command
+            AdbHelper.write(mSocketChannel, bCommand, bCommand.length, DdmPreferences.getTimeOut());
+
+            result = true;
+        } catch (Exception e) {
+            return false;
+        } finally {
+            if (!result) {
+                // FIXME connection failed somehow, we need to disconnect the console.
+                RemoveConsole(mPort);
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Sends a command to the emulator and parses its answer.
+     * @param command the command to send.
+     * @return {@link #RESULT_OK} if success, an error message otherwise.
+     */
+    private String processCommand(String command) {
+        if (sendCommand(command)) {
+            String[] result = readLines();
+
+            if (result != null && result.length > 0) {
+                Matcher m = RE_KO.matcher(result[result.length-1]);
+                if (m.matches()) {
+                    return m.group(1);
+                }
+                return RESULT_OK;
+            }
+
+            return "Unable to communicate with the emulator";
+        }
+
+        return "Unable to send command to the emulator";
+    }
+
+    /**
+     * Reads line from the console socket. This call is blocking until we read the lines:
+     * <ul>
+     * <li>OK\r\n</li>
+     * <li>KO<msg>\r\n</li>
+     * </ul>
+     * @return the array of strings read from the emulator.
+     */
+    private String[] readLines() {
+        try {
+            ByteBuffer buf = ByteBuffer.wrap(mBuffer, 0, mBuffer.length);
+            int numWaits = 0;
+            boolean stop = false;
+
+            while (buf.position() != buf.limit() && !stop) {
+                int count;
+
+                count = mSocketChannel.read(buf);
+                if (count < 0) {
+                    return null;
+                } else if (count == 0) {
+                    if (numWaits * WAIT_TIME > STD_TIMEOUT) {
+                        return null;
+                    }
+                    // non-blocking spin
+                    try {
+                        Thread.sleep(WAIT_TIME);
+                    } catch (InterruptedException ie) {
+                    }
+                    numWaits++;
+                } else {
+                    numWaits = 0;
+                }
+
+                // check the last few char aren't OK. For a valid message to test
+                // we need at least 4 bytes (OK/KO + \r\n)
+                if (buf.position() >= 4) {
+                    int pos = buf.position();
+                    if (endsWithOK(pos) || lastLineIsKO(pos)) {
+                        stop = true;
+                    }
+                }
+            }
+
+            String msg = new String(mBuffer, 0, buf.position(), DEFAULT_ENCODING);
+            return msg.split("\r\n"); //$NON-NLS-1$
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Returns true if the 4 characters *before* the current position are "OK\r\n"
+     * @param currentPosition The current position
+     */
+    private boolean endsWithOK(int currentPosition) {
+        return mBuffer[currentPosition - 1] == '\n' &&
+                mBuffer[currentPosition - 2] == '\r' &&
+                mBuffer[currentPosition - 3] == 'K' &&
+                mBuffer[currentPosition - 4] == 'O';
+
+    }
+
+    /**
+     * Returns true if the last line starts with KO and is also terminated by \r\n
+     * @param currentPosition the current position
+     */
+    private boolean lastLineIsKO(int currentPosition) {
+        // first check that the last 2 characters are CRLF
+        if (mBuffer[currentPosition-1] != '\n' ||
+                mBuffer[currentPosition-2] != '\r') {
+            return false;
+        }
+
+        // now loop backward looking for the previous CRLF, or the beginning of the buffer
+        int i = 0;
+        for (i = currentPosition-3 ; i >= 0; i--) {
+            if (mBuffer[i] == '\n') {
+                // found \n!
+                if (i > 0 && mBuffer[i-1] == '\r') {
+                    // found \r!
+                    break;
+                }
+            }
+        }
+
+        // here it is either -1 if we reached the start of the buffer without finding
+        // a CRLF, or the position of \n. So in both case we look at the characters at i+1 and i+2
+        if (mBuffer[i+1] == 'K' && mBuffer[i+2] == 'O') {
+            // found error!
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns true if the last line of the result does not start with KO
+     */
+    private boolean isValid(String[] result) {
+        if (result != null && result.length > 0) {
+            return !(RE_KO.matcher(result[result.length-1]).matches());
+        }
+        return false;
+    }
+
+    private int getLatencyIndex(String value) {
+        try {
+            // get the int value
+            int latency = Integer.parseInt(value);
+
+            // check for the speed from the index
+            for (int i = 0 ; i < MIN_LATENCIES.length; i++) {
+                if (MIN_LATENCIES[i] == latency) {
+                    return i;
+                }
+            }
+        } catch (NumberFormatException e) {
+            // Do nothing, we'll just return -1.
+        }
+
+        return -1;
+    }
+
+    private int getSpeedIndex(String value) {
+        try {
+            // get the int value
+            int speed = Integer.parseInt(value);
+
+            // check for the speed from the index
+            for (int i = 0 ; i < DOWNLOAD_SPEEDS.length; i++) {
+                if (DOWNLOAD_SPEEDS[i] == speed) {
+                    return i;
+                }
+            }
+        } catch (NumberFormatException e) {
+            // Do nothing, we'll just return -1.
+        }
+
+        return -1;
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/FileListingService.java b/ddmlib/src/main/java/com/android/ddmlib/FileListingService.java
new file mode 100644
index 0000000..5485a32
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/FileListingService.java
@@ -0,0 +1,852 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Provides {@link Device} side file listing service.
+ * <p/>To get an instance for a known {@link Device}, call {@link Device#getFileListingService()}.
+ */
+public final class FileListingService {
+
+    /** Pattern to find filenames that match "*.apk" */
+    private static final Pattern sApkPattern =
+        Pattern.compile(".*\\.apk", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
+
+    private static final String PM_FULL_LISTING = "pm list packages -f"; //$NON-NLS-1$
+
+    /** Pattern to parse the output of the 'pm -lf' command.<br>
+     * The output format looks like:<br>
+     * /data/app/myapp.apk=com.mypackage.myapp */
+    private static final Pattern sPmPattern = Pattern.compile("^package:(.+?)=(.+)$"); //$NON-NLS-1$
+
+    /** Top level data folder. */
+    public static final String DIRECTORY_DATA = "data"; //$NON-NLS-1$
+    /** Top level sdcard folder. */
+    public static final String DIRECTORY_SDCARD = "sdcard"; //$NON-NLS-1$
+    /** Top level mount folder. */
+    public static final String DIRECTORY_MNT = "mnt"; //$NON-NLS-1$
+    /** Top level system folder. */
+    public static final String DIRECTORY_SYSTEM = "system"; //$NON-NLS-1$
+    /** Top level temp folder. */
+    public static final String DIRECTORY_TEMP = "tmp"; //$NON-NLS-1$
+    /** Application folder. */
+    public static final String DIRECTORY_APP = "app"; //$NON-NLS-1$
+
+    public static final long REFRESH_RATE = 5000L;
+    /**
+     * Refresh test has to be slightly lower for precision issue.
+     */
+    static final long REFRESH_TEST = (long)(REFRESH_RATE * .8);
+
+    /** Entry type: File */
+    public static final int TYPE_FILE = 0;
+    /** Entry type: Directory */
+    public static final int TYPE_DIRECTORY = 1;
+    /** Entry type: Directory Link */
+    public static final int TYPE_DIRECTORY_LINK = 2;
+    /** Entry type: Block */
+    public static final int TYPE_BLOCK = 3;
+    /** Entry type: Character */
+    public static final int TYPE_CHARACTER = 4;
+    /** Entry type: Link */
+    public static final int TYPE_LINK = 5;
+    /** Entry type: Socket */
+    public static final int TYPE_SOCKET = 6;
+    /** Entry type: FIFO */
+    public static final int TYPE_FIFO = 7;
+    /** Entry type: Other */
+    public static final int TYPE_OTHER = 8;
+
+    /** Device side file separator. */
+    public static final String FILE_SEPARATOR = "/"; //$NON-NLS-1$
+
+    private static final String FILE_ROOT = "/"; //$NON-NLS-1$
+
+
+    /**
+     * Regexp pattern to parse the result from ls.
+     */
+    private static final Pattern LS_L_PATTERN = Pattern.compile(
+            "^([bcdlsp-][-r][-w][-xsS][-r][-w][-xsS][-r][-w][-xstST])\\s+(\\S+)\\s+(\\S+)\\s+" +
+            "([\\d\\s,]*)\\s+(\\d{4}-\\d\\d-\\d\\d)\\s+(\\d\\d:\\d\\d)\\s+(.*)$"); //$NON-NLS-1$
+
+    private static final Pattern LS_LD_PATTERN = Pattern.compile(
+                    "d[rwx-]{9}\\s+\\S+\\s+\\S+\\s+[0-9-]{10}\\s+\\d{2}:\\d{2}$"); //$NON-NLS-1$
+
+
+    private Device mDevice;
+    private FileEntry mRoot;
+
+    private ArrayList<Thread> mThreadList = new ArrayList<Thread>();
+
+    /**
+     * Represents an entry in a directory. This can be a file or a directory.
+     */
+    public static final class FileEntry {
+        /** Pattern to escape filenames for shell command consumption.
+         *  This pattern identifies any special characters that need to be escaped with a
+         *  backslash. */
+        private static final Pattern sEscapePattern = Pattern.compile(
+                "([\\\\()*+?\"'&#/\\s])"); //$NON-NLS-1$
+
+        /**
+         * Comparator object for FileEntry
+         */
+        private static Comparator<FileEntry> sEntryComparator = new Comparator<FileEntry>() {
+            @Override
+            public int compare(FileEntry o1, FileEntry o2) {
+                if (o1 instanceof FileEntry && o2 instanceof FileEntry) {
+                    FileEntry fe1 = o1;
+                    FileEntry fe2 = o2;
+                    return fe1.name.compareTo(fe2.name);
+                }
+                return 0;
+            }
+        };
+
+        FileEntry parent;
+        String name;
+        String info;
+        String permissions;
+        String size;
+        String date;
+        String time;
+        String owner;
+        String group;
+        int type;
+        boolean isAppPackage;
+
+        boolean isRoot;
+
+        /**
+         * Indicates whether the entry content has been fetched yet, or not.
+         */
+        long fetchTime = 0;
+
+        final ArrayList<FileEntry> mChildren = new ArrayList<FileEntry>();
+
+        /**
+         * Creates a new file entry.
+         * @param parent parent entry or null if entry is root
+         * @param name name of the entry.
+         * @param type entry type. Can be one of the following: {@link FileListingService#TYPE_FILE},
+         * {@link FileListingService#TYPE_DIRECTORY}, {@link FileListingService#TYPE_OTHER}.
+         */
+        private FileEntry(FileEntry parent, String name, int type, boolean isRoot) {
+            this.parent = parent;
+            this.name = name;
+            this.type = type;
+            this.isRoot = isRoot;
+
+            checkAppPackageStatus();
+        }
+
+        /**
+         * Returns the name of the entry
+         */
+        public String getName() {
+            return name;
+        }
+
+        /**
+         * Returns the size string of the entry, as returned by <code>ls</code>.
+         */
+        public String getSize() {
+            return size;
+        }
+
+        /**
+         * Returns the size of the entry.
+         */
+        public int getSizeValue() {
+            return Integer.parseInt(size);
+        }
+
+        /**
+         * Returns the date string of the entry, as returned by <code>ls</code>.
+         */
+        public String getDate() {
+            return date;
+        }
+
+        /**
+         * Returns the time string of the entry, as returned by <code>ls</code>.
+         */
+        public String getTime() {
+            return time;
+        }
+
+        /**
+         * Returns the permission string of the entry, as returned by <code>ls</code>.
+         */
+        public String getPermissions() {
+            return permissions;
+        }
+
+        /**
+         * Returns the owner string of the entry, as returned by <code>ls</code>.
+         */
+        public String getOwner() {
+            return owner;
+        }
+
+        /**
+         * Returns the group owner of the entry, as returned by <code>ls</code>.
+         */
+        public String getGroup() {
+            return group;
+        }
+
+        /**
+         * Returns the extra info for the entry.
+         * <p/>For a link, it will be a description of the link.
+         * <p/>For an application apk file it will be the application package as returned
+         * by the Package Manager.
+         */
+        public String getInfo() {
+            return info;
+        }
+
+        /**
+         * Return the full path of the entry.
+         * @return a path string using {@link FileListingService#FILE_SEPARATOR} as separator.
+         */
+        public String getFullPath() {
+            if (isRoot) {
+                return FILE_ROOT;
+            }
+            StringBuilder pathBuilder = new StringBuilder();
+            fillPathBuilder(pathBuilder, false);
+
+            return pathBuilder.toString();
+        }
+
+        /**
+         * Return the fully escaped path of the entry. This path is safe to use in a
+         * shell command line.
+         * @return a path string using {@link FileListingService#FILE_SEPARATOR} as separator
+         */
+        public String getFullEscapedPath() {
+            StringBuilder pathBuilder = new StringBuilder();
+            fillPathBuilder(pathBuilder, true);
+
+            return pathBuilder.toString();
+        }
+
+        /**
+         * Returns the path as a list of segments.
+         */
+        public String[] getPathSegments() {
+            ArrayList<String> list = new ArrayList<String>();
+            fillPathSegments(list);
+
+            return list.toArray(new String[list.size()]);
+        }
+
+        /**
+         * Returns the Entry type as an int, which will match one of the TYPE_(...) constants
+         */
+        public int getType() {
+            return type;
+        }
+
+        /**
+         * Sets a new type.
+         */
+        public void setType(int type) {
+            this.type = type;
+        }
+
+        /**
+         * Returns if the entry is a folder or a link to a folder.
+         */
+        public boolean isDirectory() {
+            return type == TYPE_DIRECTORY || type == TYPE_DIRECTORY_LINK;
+        }
+
+        /**
+         * Returns the parent entry.
+         */
+        public FileEntry getParent() {
+            return parent;
+        }
+
+        /**
+         * Returns the cached children of the entry. This returns the cache created from calling
+         * <code>FileListingService.getChildren()</code>.
+         */
+        public FileEntry[] getCachedChildren() {
+            return mChildren.toArray(new FileEntry[mChildren.size()]);
+        }
+
+        /**
+         * Returns the child {@link FileEntry} matching the name.
+         * This uses the cached children list.
+         * @param name the name of the child to return.
+         * @return the FileEntry matching the name or null.
+         */
+        public FileEntry findChild(String name) {
+            for (FileEntry entry : mChildren) {
+                if (entry.name.equals(name)) {
+                    return entry;
+                }
+            }
+            return null;
+        }
+
+        /**
+         * Returns whether the entry is the root.
+         */
+        public boolean isRoot() {
+            return isRoot;
+        }
+
+        void addChild(FileEntry child) {
+            mChildren.add(child);
+        }
+
+        void setChildren(ArrayList<FileEntry> newChildren) {
+            mChildren.clear();
+            mChildren.addAll(newChildren);
+        }
+
+        boolean needFetch() {
+            if (fetchTime == 0) {
+                return true;
+            }
+            long current = System.currentTimeMillis();
+            return current - fetchTime > REFRESH_TEST;
+
+        }
+
+        /**
+         * Returns if the entry is a valid application package.
+         */
+        public boolean isApplicationPackage() {
+            return isAppPackage;
+        }
+
+        /**
+         * Returns if the file name is an application package name.
+         */
+        public boolean isAppFileName() {
+            Matcher m = sApkPattern.matcher(name);
+            return m.matches();
+        }
+
+        /**
+         * Recursively fills the pathBuilder with the full path
+         * @param pathBuilder a StringBuilder used to create the path.
+         * @param escapePath Whether the path need to be escaped for consumption by
+         * a shell command line.
+         */
+        protected void fillPathBuilder(StringBuilder pathBuilder, boolean escapePath) {
+            if (isRoot) {
+                return;
+            }
+
+            if (parent != null) {
+                parent.fillPathBuilder(pathBuilder, escapePath);
+            }
+            pathBuilder.append(FILE_SEPARATOR);
+            pathBuilder.append(escapePath ? escape(name) : name);
+        }
+
+        /**
+         * Recursively fills the segment list with the full path.
+         * @param list The list of segments to fill.
+         */
+        protected void fillPathSegments(ArrayList<String> list) {
+            if (isRoot) {
+                return;
+            }
+
+            if (parent != null) {
+                parent.fillPathSegments(list);
+            }
+
+            list.add(name);
+        }
+
+        /**
+         * Sets the internal app package status flag. This checks whether the entry is in an app
+         * directory like /data/app or /system/app
+         */
+        private void checkAppPackageStatus() {
+            isAppPackage = false;
+
+            String[] segments = getPathSegments();
+            if (type == TYPE_FILE && segments.length == 3 && isAppFileName()) {
+                isAppPackage = DIRECTORY_APP.equals(segments[1]) &&
+                    (DIRECTORY_SYSTEM.equals(segments[0]) || DIRECTORY_DATA.equals(segments[0]));
+            }
+        }
+
+        /**
+         * Returns an escaped version of the entry name.
+         * @param entryName
+         */
+        public static String escape(String entryName) {
+            return sEscapePattern.matcher(entryName).replaceAll("\\\\$1"); //$NON-NLS-1$
+        }
+    }
+
+    private static class LsReceiver extends MultiLineReceiver {
+
+        private ArrayList<FileEntry> mEntryList;
+        private ArrayList<String> mLinkList;
+        private FileEntry[] mCurrentChildren;
+        private FileEntry mParentEntry;
+
+        /**
+         * Create an ls receiver/parser.
+         * @param currentChildren The list of current children. To prevent
+         *      collapse during update, reusing the same FileEntry objects for
+         *      files that were already there is paramount.
+         * @param entryList the list of new children to be filled by the
+         *      receiver.
+         * @param linkList the list of link path to compute post ls, to figure
+         *      out if the link pointed to a file or to a directory.
+         */
+        public LsReceiver(FileEntry parentEntry, ArrayList<FileEntry> entryList,
+                ArrayList<String> linkList) {
+            mParentEntry = parentEntry;
+            mCurrentChildren = parentEntry.getCachedChildren();
+            mEntryList = entryList;
+            mLinkList = linkList;
+        }
+
+        @Override
+        public void processNewLines(String[] lines) {
+            for (String line : lines) {
+                // no need to handle empty lines.
+                if (line.isEmpty()) {
+                    continue;
+                }
+
+                // run the line through the regexp
+                Matcher m = LS_L_PATTERN.matcher(line);
+                if (!m.matches()) {
+                    continue;
+                }
+
+                // get the name
+                String name = m.group(7);
+
+                // get the rest of the groups
+                String permissions = m.group(1);
+                String owner = m.group(2);
+                String group = m.group(3);
+                String size = m.group(4);
+                String date = m.group(5);
+                String time = m.group(6);
+                String info = null;
+
+                // and the type
+                int objectType = TYPE_OTHER;
+                switch (permissions.charAt(0)) {
+                    case '-' :
+                        objectType = TYPE_FILE;
+                        break;
+                    case 'b' :
+                        objectType = TYPE_BLOCK;
+                        break;
+                    case 'c' :
+                        objectType = TYPE_CHARACTER;
+                        break;
+                    case 'd' :
+                        objectType = TYPE_DIRECTORY;
+                        break;
+                    case 'l' :
+                        objectType = TYPE_LINK;
+                        break;
+                    case 's' :
+                        objectType = TYPE_SOCKET;
+                        break;
+                    case 'p' :
+                        objectType = TYPE_FIFO;
+                        break;
+                }
+
+
+                // now check what we may be linking to
+                if (objectType == TYPE_LINK) {
+                    String[] segments = name.split("\\s->\\s"); //$NON-NLS-1$
+
+                    // we should have 2 segments
+                    if (segments.length == 2) {
+                        // update the entry name to not contain the link
+                        name = segments[0];
+
+                        // and the link name
+                        info = segments[1];
+
+                        // now get the path to the link
+                        String[] pathSegments = info.split(FILE_SEPARATOR);
+                        if (pathSegments.length == 1) {
+                            // the link is to something in the same directory,
+                            // unless the link is ..
+                            if ("..".equals(pathSegments[0])) { //$NON-NLS-1$
+                                // set the type and we're done.
+                                objectType = TYPE_DIRECTORY_LINK;
+                            } else {
+                                // either we found the object already
+                                // or we'll find it later.
+                            }
+                        }
+                    }
+
+                    // add an arrow in front to specify it's a link.
+                    info = "-> " + info; //$NON-NLS-1$;
+                }
+
+                // get the entry, either from an existing one, or a new one
+                FileEntry entry = getExistingEntry(name);
+                if (entry == null) {
+                    entry = new FileEntry(mParentEntry, name, objectType, false /* isRoot */);
+                }
+
+                // add some misc info
+                entry.permissions = permissions;
+                entry.size = size;
+                entry.date = date;
+                entry.time = time;
+                entry.owner = owner;
+                entry.group = group;
+                if (objectType == TYPE_LINK) {
+                    entry.info = info;
+                }
+
+                mEntryList.add(entry);
+            }
+        }
+
+        /**
+         * Queries for an already existing Entry per name
+         * @param name the name of the entry
+         * @return the existing FileEntry or null if no entry with a matching
+         * name exists.
+         */
+        private FileEntry getExistingEntry(String name) {
+            for (int i = 0 ; i < mCurrentChildren.length; i++) {
+                FileEntry e = mCurrentChildren[i];
+
+                // since we're going to "erase" the one we use, we need to
+                // check that the item is not null.
+                if (e != null) {
+                    // compare per name, case-sensitive.
+                    if (name.equals(e.name)) {
+                        // erase from the list
+                        mCurrentChildren[i] = null;
+
+                        // and return the object
+                        return e;
+                    }
+                }
+            }
+
+            // couldn't find any matching object, return null
+            return null;
+        }
+
+        @Override
+        public boolean isCancelled() {
+            return false;
+        }
+
+        /**
+         * Determine if any symlinks in the <code entries> list are links-to-directories, and if so
+         * mark them as such.  This allows us to traverse them properly later on.
+         */
+        public void finishLinks(IDevice device, ArrayList<FileEntry> entries)
+                throws TimeoutException, AdbCommandRejectedException,
+                ShellCommandUnresponsiveException, IOException {
+            final int[] nLines = {0};
+            MultiLineReceiver receiver = new MultiLineReceiver() {
+                @Override
+                public void processNewLines(String[] lines) {
+                    for (String line : lines) {
+                        Matcher m = LS_LD_PATTERN.matcher(line);
+                        if (m.matches()) {
+                            nLines[0]++;
+                        }
+                    }
+                }
+
+                @Override
+                public boolean isCancelled() {
+                    return false;
+                }
+            };
+
+            for (FileEntry entry : entries) {
+                if (entry.getType() != TYPE_LINK) continue;
+
+                // We simply need to determine whether the referent is a directory or not.
+                // We do this by running `ls -ld ${link}/`.  If the referent exists and is a
+                // directory, we'll see the normal directory listing.  Otherwise, we'll see an
+                // error of some sort.
+                nLines[0] = 0;
+
+                final String command = String.format("ls -l -d %s%s", entry.getFullEscapedPath(),
+                        FILE_SEPARATOR);
+
+                device.executeShellCommand(command, receiver);
+
+                if (nLines[0] > 0) {
+                    // We saw lines matching the directory pattern, so it's a directory!
+                    entry.setType(TYPE_DIRECTORY_LINK);
+                }
+            }
+        }
+    }
+
+    /**
+     * Classes which implement this interface provide a method that deals with asynchronous
+     * result from <code>ls</code> command on the device.
+     *
+     * @see FileListingService#getChildren(com.android.ddmlib.FileListingService.FileEntry, boolean, com.android.ddmlib.FileListingService.IListingReceiver)
+     */
+    public interface IListingReceiver {
+        public void setChildren(FileEntry entry, FileEntry[] children);
+
+        public void refreshEntry(FileEntry entry);
+    }
+
+    /**
+     * Creates a File Listing Service for a specified {@link Device}.
+     * @param device The Device the service is connected to.
+     */
+    FileListingService(Device device) {
+        mDevice = device;
+    }
+
+    /**
+     * Returns the root element.
+     * @return the {@link FileEntry} object representing the root element or
+     * <code>null</code> if the device is invalid.
+     */
+    public FileEntry getRoot() {
+        if (mDevice != null) {
+            if (mRoot == null) {
+                mRoot = new FileEntry(null /* parent */, "" /* name */, TYPE_DIRECTORY,
+                        true /* isRoot */);
+            }
+
+            return mRoot;
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns the children of a {@link FileEntry}.
+     * <p/>
+     * This method supports a cache mechanism and synchronous and asynchronous modes.
+     * <p/>
+     * If <var>receiver</var> is <code>null</code>, the device side <code>ls</code>
+     * command is done synchronously, and the method will return upon completion of the command.<br>
+     * If <var>receiver</var> is non <code>null</code>, the command is launched is a separate
+     * thread and upon completion, the receiver will be notified of the result.
+     * <p/>
+     * The result for each <code>ls</code> command is cached in the parent
+     * <code>FileEntry</code>. <var>useCache</var> allows usage of this cache, but only if the
+     * cache is valid. The cache is valid only for {@link FileListingService#REFRESH_RATE} ms.
+     * After that a new <code>ls</code> command is always executed.
+     * <p/>
+     * If the cache is valid and <code>useCache == true</code>, the method will always simply
+     * return the value of the cache, whether a {@link IListingReceiver} has been provided or not.
+     *
+     * @param entry The parent entry.
+     * @param useCache A flag to use the cache or to force a new ls command.
+     * @param receiver A receiver for asynchronous calls.
+     * @return The list of children or <code>null</code> for asynchronous calls.
+     *
+     * @see FileEntry#getCachedChildren()
+     */
+    public FileEntry[] getChildren(final FileEntry entry, boolean useCache,
+            final IListingReceiver receiver) {
+        // first thing we do is check the cache, and if we already have a recent
+        // enough children list, we just return that.
+        if (useCache && !entry.needFetch()) {
+            return entry.getCachedChildren();
+        }
+
+        // if there's no receiver, then this is a synchronous call, and we
+        // return the result of ls
+        if (receiver == null) {
+            doLs(entry);
+            return entry.getCachedChildren();
+        }
+
+        // this is a asynchronous call.
+        // we launch a thread that will do ls and give the listing
+        // to the receiver
+        Thread t = new Thread("ls " + entry.getFullPath()) { //$NON-NLS-1$
+            @Override
+            public void run() {
+                doLs(entry);
+
+                receiver.setChildren(entry, entry.getCachedChildren());
+
+                final FileEntry[] children = entry.getCachedChildren();
+                if (children.length > 0 && children[0].isApplicationPackage()) {
+                    final HashMap<String, FileEntry> map = new HashMap<String, FileEntry>();
+
+                    for (FileEntry child : children) {
+                        String path = child.getFullPath();
+                        map.put(path, child);
+                    }
+
+                    // call pm.
+                    String command = PM_FULL_LISTING;
+                    try {
+                        mDevice.executeShellCommand(command, new MultiLineReceiver() {
+                            @Override
+                            public void processNewLines(String[] lines) {
+                                for (String line : lines) {
+                                    if (!line.isEmpty()) {
+                                        // get the filepath and package from the line
+                                        Matcher m = sPmPattern.matcher(line);
+                                        if (m.matches()) {
+                                            // get the children with that path
+                                            FileEntry entry = map.get(m.group(1));
+                                            if (entry != null) {
+                                                entry.info = m.group(2);
+                                                receiver.refreshEntry(entry);
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                            @Override
+                            public boolean isCancelled() {
+                                return false;
+                            }
+                        });
+                    } catch (Exception e) {
+                        // adb failed somehow, we do nothing.
+                    }
+                }
+
+
+                // if another thread is pending, launch it
+                synchronized (mThreadList) {
+                    // first remove ourselves from the list
+                    mThreadList.remove(this);
+
+                    // then launch the next one if applicable.
+                    if (!mThreadList.isEmpty()) {
+                        Thread t = mThreadList.get(0);
+                        t.start();
+                    }
+                }
+            }
+        };
+
+        // we don't want to run multiple ls on the device at the same time, so we
+        // store the thread in a list and launch it only if there's no other thread running.
+        // the thread will launch the next one once it's done.
+        synchronized (mThreadList) {
+            // add to the list
+            mThreadList.add(t);
+
+            // if it's the only one, launch it.
+            if (mThreadList.size() == 1) {
+                t.start();
+            }
+        }
+
+        // and we return null.
+        return null;
+    }
+
+    /**
+     * Returns the children of a {@link FileEntry}.
+     * <p/>
+     * This method is the explicit synchronous version of
+     * {@link #getChildren(FileEntry, boolean, IListingReceiver)}. It is roughly equivalent to
+     * calling
+     * getChildren(FileEntry, false, null)
+     *
+     * @param entry The parent entry.
+     * @return The list of children
+     * @throws TimeoutException in case of timeout on the connection when sending the command.
+     * @throws AdbCommandRejectedException if adb rejects the command.
+     * @throws ShellCommandUnresponsiveException in case the shell command doesn't send any output
+     *            for a period longer than <var>maxTimeToOutputResponse</var>.
+     * @throws IOException in case of I/O error on the connection.
+     */
+    public FileEntry[] getChildrenSync(final FileEntry entry) throws TimeoutException,
+            AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException {
+        doLsAndThrow(entry);
+        return entry.getCachedChildren();
+    }
+
+    private void doLs(FileEntry entry) {
+        try {
+            doLsAndThrow(entry);
+        } catch (Exception e) {
+            // do nothing
+        }
+    }
+
+    private void doLsAndThrow(FileEntry entry) throws TimeoutException,
+            AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException {
+        // create a list that will receive the list of the entries
+        ArrayList<FileEntry> entryList = new ArrayList<FileEntry>();
+
+        // create a list that will receive the link to compute post ls;
+        ArrayList<String> linkList = new ArrayList<String>();
+
+        try {
+            // create the command
+            String command = "ls -l " + entry.getFullEscapedPath(); //$NON-NLS-1$
+            if (entry.isDirectory()) {
+                // If we expect a file to behave like a directory, we should stick a "/" at the end.
+                // This is a good habit, and is mandatory for symlinks-to-directories, which will
+                // otherwise behave like symlinks.
+                command += FILE_SEPARATOR;
+            }
+
+            // create the receiver object that will parse the result from ls
+            LsReceiver receiver = new LsReceiver(entry, entryList, linkList);
+
+            // call ls.
+            mDevice.executeShellCommand(command, receiver);
+
+            // finish the process of the receiver to handle links
+            receiver.finishLinks(mDevice, entryList);
+        } finally {
+            // at this point we need to refresh the viewer
+            entry.fetchTime = System.currentTimeMillis();
+
+            // sort the children and set them as the new children
+            Collections.sort(entryList, FileEntry.sEntryComparator);
+            entry.setChildren(entryList);
+        }
+    }
+
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/GetPropReceiver.java b/ddmlib/src/main/java/com/android/ddmlib/GetPropReceiver.java
new file mode 100644
index 0000000..d7368c8
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/GetPropReceiver.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A receiver able to parse the result of the execution of
+ * {@link #GETPROP_COMMAND} on a device.
+ */
+final class GetPropReceiver extends MultiLineReceiver {
+    static final String GETPROP_COMMAND = "getprop"; //$NON-NLS-1$
+
+    private static final Pattern GETPROP_PATTERN = Pattern.compile("^\\[([^]]+)\\]\\:\\s*\\[(.*)\\]$"); //$NON-NLS-1$
+
+    /** indicates if we need to read the first */
+    private Device mDevice = null;
+
+    /**
+     * Creates the receiver with the device the receiver will modify.
+     * @param device The device to modify
+     */
+    public GetPropReceiver(Device device) {
+        mDevice = device;
+    }
+
+    @Override
+    public void processNewLines(String[] lines) {
+        // We receive an array of lines. We're expecting
+        // to have the build info in the first line, and the build
+        // date in the 2nd line. There seems to be an empty line
+        // after all that.
+
+        for (String line : lines) {
+            if (line.isEmpty() || line.startsWith("#")) {
+                continue;
+            }
+
+            Matcher m = GETPROP_PATTERN.matcher(line);
+            if (m.matches()) {
+                String label = m.group(1);
+                String value = m.group(2);
+
+                if (!label.isEmpty()) {
+                    mDevice.addProperty(label, value);
+                }
+            }
+        }
+    }
+
+    @Override
+    public boolean isCancelled() {
+        return false;
+    }
+
+    @Override
+    public void done() {
+        mDevice.update(Device.CHANGE_BUILD_INFO);
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/HandleAppName.java b/ddmlib/src/main/java/com/android/ddmlib/HandleAppName.java
new file mode 100644
index 0000000..da4ade3
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/HandleAppName.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+
+/**
+ * Handle the "app name" chunk (APNM).
+ */
+final class HandleAppName extends ChunkHandler {
+
+    public static final int CHUNK_APNM = ChunkHandler.type("APNM");
+
+    private static final HandleAppName mInst = new HandleAppName();
+
+
+    private HandleAppName() {}
+
+    /**
+     * Register for the packets we expect to get from the client.
+     */
+    public static void register(MonitorThread mt) {
+        mt.registerChunkHandler(CHUNK_APNM, mInst);
+    }
+
+    /**
+     * Client is ready.
+     */
+    @Override
+    public void clientReady(Client client) throws IOException {}
+
+    /**
+     * Client went away.
+     */
+    @Override
+    public void clientDisconnected(Client client) {}
+
+    /**
+     * Chunk handler entry point.
+     */
+    @Override
+    public void handleChunk(Client client, int type, ByteBuffer data,
+            boolean isReply, int msgId) {
+
+        Log.d("ddm-appname", "handling " + ChunkHandler.name(type));
+
+        if (type == CHUNK_APNM) {
+            assert !isReply;
+            handleAPNM(client, data);
+        } else {
+            handleUnknownChunk(client, type, data, isReply, msgId);
+        }
+    }
+
+    /*
+     * Handle a reply to our APNM message.
+     */
+    private static void handleAPNM(Client client, ByteBuffer data) {
+        int appNameLen;
+        String appName;
+
+        appNameLen = data.getInt();
+        appName = getString(data, appNameLen);
+
+        // Newer devices send user id in the APNM packet.
+        int userId = -1;
+        boolean validUserId = false;
+        if (data.hasRemaining()) {
+            try {
+                userId = data.getInt();
+                validUserId = true;
+            } catch (BufferUnderflowException e) {
+                // two integers + utf-16 string
+                int expectedPacketLength = 8 + appNameLen * 2;
+
+                Log.e("ddm-appname", "Insufficient data in APNM chunk to retrieve user id.");
+                Log.e("ddm-appname", "Actual chunk length: " + data.capacity());
+                Log.e("ddm-appname", "Expected chunk length: " + expectedPacketLength);
+            }
+        }
+
+        Log.d("ddm-appname", "APNM: app='" + appName + "'");
+
+        ClientData cd = client.getClientData();
+        synchronized (cd) {
+            cd.setClientDescription(appName);
+
+            if (validUserId) {
+                cd.setUserId(userId);
+            }
+        }
+
+        client = checkDebuggerPortForAppName(client, appName);
+
+        if (client != null) {
+            client.update(Client.CHANGE_NAME);
+        }
+    }
+ }
+
diff --git a/ddmlib/src/main/java/com/android/ddmlib/HandleExit.java b/ddmlib/src/main/java/com/android/ddmlib/HandleExit.java
new file mode 100644
index 0000000..adeedbb
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/HandleExit.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * Submit an exit request.
+ */
+final class HandleExit extends ChunkHandler {
+
+    public static final int CHUNK_EXIT = type("EXIT");
+
+    private static final HandleExit mInst = new HandleExit();
+
+
+    private HandleExit() {}
+
+    /**
+     * Register for the packets we expect to get from the client.
+     */
+    public static void register(MonitorThread mt) {}
+
+    /**
+     * Client is ready.
+     */
+    @Override
+    public void clientReady(Client client) throws IOException {}
+
+    /**
+     * Client went away.
+     */
+    @Override
+    public void clientDisconnected(Client client) {}
+
+    /**
+     * Chunk handler entry point.
+     */
+    @Override
+    public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) {
+        handleUnknownChunk(client, type, data, isReply, msgId);
+    }
+
+    /**
+     * Send an EXIT request to the client.
+     */
+    public static void sendEXIT(Client client, int status)
+        throws IOException
+    {
+        ByteBuffer rawBuf = allocBuffer(4);
+        JdwpPacket packet = new JdwpPacket(rawBuf);
+        ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+        buf.putInt(status);
+
+        finishChunkPacket(packet, CHUNK_EXIT, buf.position());
+        Log.d("ddm-exit", "Sending " + name(CHUNK_EXIT) + ": " + status);
+        client.sendAndConsume(packet, mInst);
+    }
+}
+
diff --git a/ddmlib/src/main/java/com/android/ddmlib/HandleHeap.java b/ddmlib/src/main/java/com/android/ddmlib/HandleHeap.java
new file mode 100644
index 0000000..1761b79
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/HandleHeap.java
@@ -0,0 +1,594 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import com.android.ddmlib.ClientData.AllocationTrackingStatus;
+import com.android.ddmlib.ClientData.IHprofDumpHandler;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+
+/**
+ * Handle heap status updates.
+ */
+final class HandleHeap extends ChunkHandler {
+
+    public static final int CHUNK_HPIF = type("HPIF");
+    public static final int CHUNK_HPST = type("HPST");
+    public static final int CHUNK_HPEN = type("HPEN");
+    public static final int CHUNK_HPSG = type("HPSG");
+    public static final int CHUNK_HPGC = type("HPGC");
+    public static final int CHUNK_HPDU = type("HPDU");
+    public static final int CHUNK_HPDS = type("HPDS");
+    public static final int CHUNK_REAE = type("REAE");
+    public static final int CHUNK_REAQ = type("REAQ");
+    public static final int CHUNK_REAL = type("REAL");
+
+    // args to sendHPSG
+    public static final int WHEN_DISABLE = 0;
+    public static final int WHEN_GC = 1;
+    public static final int WHAT_MERGE = 0; // merge adjacent objects
+    public static final int WHAT_OBJ = 1;   // keep objects distinct
+
+    // args to sendHPIF
+    public static final int HPIF_WHEN_NEVER = 0;
+    public static final int HPIF_WHEN_NOW = 1;
+    public static final int HPIF_WHEN_NEXT_GC = 2;
+    public static final int HPIF_WHEN_EVERY_GC = 3;
+
+    private static final HandleHeap mInst = new HandleHeap();
+
+    private HandleHeap() {}
+
+    /**
+     * Register for the packets we expect to get from the client.
+     */
+    public static void register(MonitorThread mt) {
+        mt.registerChunkHandler(CHUNK_HPIF, mInst);
+        mt.registerChunkHandler(CHUNK_HPST, mInst);
+        mt.registerChunkHandler(CHUNK_HPEN, mInst);
+        mt.registerChunkHandler(CHUNK_HPSG, mInst);
+        mt.registerChunkHandler(CHUNK_HPDS, mInst);
+        mt.registerChunkHandler(CHUNK_REAQ, mInst);
+        mt.registerChunkHandler(CHUNK_REAL, mInst);
+    }
+
+    /**
+     * Client is ready.
+     */
+    @Override
+    public void clientReady(Client client) throws IOException {
+        if (client.isHeapUpdateEnabled()) {
+            //sendHPSG(client, WHEN_GC, WHAT_MERGE);
+            sendHPIF(client, HPIF_WHEN_EVERY_GC);
+        }
+    }
+
+    /**
+     * Client went away.
+     */
+    @Override
+    public void clientDisconnected(Client client) {}
+
+    /**
+     * Chunk handler entry point.
+     */
+    @Override
+    public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) {
+        Log.d("ddm-heap", "handling " + ChunkHandler.name(type));
+
+        if (type == CHUNK_HPIF) {
+            handleHPIF(client, data);
+        } else if (type == CHUNK_HPST) {
+            handleHPST(client, data);
+        } else if (type == CHUNK_HPEN) {
+            handleHPEN(client, data);
+        } else if (type == CHUNK_HPSG) {
+            handleHPSG(client, data);
+        } else if (type == CHUNK_HPDU) {
+            handleHPDU(client, data);
+        } else if (type == CHUNK_HPDS) {
+            handleHPDS(client, data);
+        } else if (type == CHUNK_REAQ) {
+            handleREAQ(client, data);
+        } else if (type == CHUNK_REAL) {
+            handleREAL(client, data);
+        } else {
+            handleUnknownChunk(client, type, data, isReply, msgId);
+        }
+    }
+
+    /*
+     * Handle a heap info message.
+     */
+    private void handleHPIF(Client client, ByteBuffer data) {
+        Log.d("ddm-heap", "HPIF!");
+        try {
+            int numHeaps = data.getInt();
+
+            for (int i = 0; i < numHeaps; i++) {
+                int heapId = data.getInt();
+                @SuppressWarnings("unused")
+                long timeStamp = data.getLong();
+                @SuppressWarnings("unused")
+                byte reason = data.get();
+                long maxHeapSize = (long)data.getInt() & 0x00ffffffff;
+                long heapSize = (long)data.getInt() & 0x00ffffffff;
+                long bytesAllocated = (long)data.getInt() & 0x00ffffffff;
+                long objectsAllocated = (long)data.getInt() & 0x00ffffffff;
+
+                client.getClientData().setHeapInfo(heapId, maxHeapSize,
+                        heapSize, bytesAllocated, objectsAllocated);
+                client.update(Client.CHANGE_HEAP_DATA);
+            }
+        } catch (BufferUnderflowException ex) {
+            Log.w("ddm-heap", "malformed HPIF chunk from client");
+        }
+    }
+
+    /**
+     * Send an HPIF (HeaP InFo) request to the client.
+     */
+    public static void sendHPIF(Client client, int when) throws IOException {
+        ByteBuffer rawBuf = allocBuffer(1);
+        JdwpPacket packet = new JdwpPacket(rawBuf);
+        ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+        buf.put((byte)when);
+
+        finishChunkPacket(packet, CHUNK_HPIF, buf.position());
+        Log.d("ddm-heap", "Sending " + name(CHUNK_HPIF) + ": when=" + when);
+        client.sendAndConsume(packet, mInst);
+    }
+
+    /*
+     * Handle a heap segment series start message.
+     */
+    private void handleHPST(Client client, ByteBuffer data) {
+        /* Clear out any data that's sitting around to
+         * get ready for the chunks that are about to come.
+         */
+//xxx todo: only clear data that belongs to the heap mentioned in <data>.
+        client.getClientData().getVmHeapData().clearHeapData();
+    }
+
+    /*
+     * Handle a heap segment series end message.
+     */
+    private void handleHPEN(Client client, ByteBuffer data) {
+        /* Let the UI know that we've received all of the
+         * data for this heap.
+         */
+//xxx todo: only seal data that belongs to the heap mentioned in <data>.
+        client.getClientData().getVmHeapData().sealHeapData();
+        client.update(Client.CHANGE_HEAP_DATA);
+    }
+
+    /*
+     * Handle a heap segment message.
+     */
+    private void handleHPSG(Client client, ByteBuffer data) {
+        byte dataCopy[] = new byte[data.limit()];
+        data.rewind();
+        data.get(dataCopy);
+        data = ByteBuffer.wrap(dataCopy);
+        client.getClientData().getVmHeapData().addHeapData(data);
+//xxx todo: add to the heap mentioned in <data>
+    }
+
+    /**
+     * Sends an HPSG (HeaP SeGment) request to the client.
+     */
+    public static void sendHPSG(Client client, int when, int what)
+        throws IOException {
+
+        ByteBuffer rawBuf = allocBuffer(2);
+        JdwpPacket packet = new JdwpPacket(rawBuf);
+        ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+        buf.put((byte)when);
+        buf.put((byte)what);
+
+        finishChunkPacket(packet, CHUNK_HPSG, buf.position());
+        Log.d("ddm-heap", "Sending " + name(CHUNK_HPSG) + ": when="
+            + when + ", what=" + what);
+        client.sendAndConsume(packet, mInst);
+    }
+
+    /**
+     * Sends an HPGC request to the client.
+     */
+    public static void sendHPGC(Client client)
+        throws IOException {
+        ByteBuffer rawBuf = allocBuffer(0);
+        JdwpPacket packet = new JdwpPacket(rawBuf);
+        ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+        // no data
+
+        finishChunkPacket(packet, CHUNK_HPGC, buf.position());
+        Log.d("ddm-heap", "Sending " + name(CHUNK_HPGC));
+        client.sendAndConsume(packet, mInst);
+    }
+
+    /**
+     * Sends an HPDU request to the client.
+     *
+     * We will get an HPDU response when the heap dump has completed.  On
+     * failure we get a generic failure response.
+     *
+     * @param fileName name of output file (on device)
+     */
+    public static void sendHPDU(Client client, String fileName)
+        throws IOException {
+        ByteBuffer rawBuf = allocBuffer(4 + fileName.length() * 2);
+        JdwpPacket packet = new JdwpPacket(rawBuf);
+        ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+        buf.putInt(fileName.length());
+        putString(buf, fileName);
+
+        finishChunkPacket(packet, CHUNK_HPDU, buf.position());
+        Log.d("ddm-heap", "Sending " + name(CHUNK_HPDU) + " '" + fileName +"'");
+        client.sendAndConsume(packet, mInst);
+        client.getClientData().setPendingHprofDump(fileName);
+    }
+
+    /**
+     * Sends an HPDS request to the client.
+     *
+     * We will get an HPDS response when the heap dump has completed.  On
+     * failure we get a generic failure response.
+     *
+     * This is more expensive for the device than HPDU, because the entire
+     * heap dump is held in RAM instead of spooled out to a temp file.  On
+     * the other hand, permission to write to /sdcard is not required.
+     *
+     * @param fileName name of output file (on device)
+     */
+    public static void sendHPDS(Client client)
+        throws IOException {
+        ByteBuffer rawBuf = allocBuffer(0);
+        JdwpPacket packet = new JdwpPacket(rawBuf);
+        ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+        finishChunkPacket(packet, CHUNK_HPDS, buf.position());
+        Log.d("ddm-heap", "Sending " + name(CHUNK_HPDS));
+        client.sendAndConsume(packet, mInst);
+    }
+
+    /*
+     * Handle notification of completion of a HeaP DUmp.
+     */
+    private void handleHPDU(Client client, ByteBuffer data) {
+        byte result;
+
+        // get the filename and make the client not have pending HPROF dump anymore.
+        String filename = client.getClientData().getPendingHprofDump();
+        client.getClientData().setPendingHprofDump(null);
+
+        // get the dump result
+        result = data.get();
+
+        // get the app-level handler for HPROF dump
+        IHprofDumpHandler handler = ClientData.getHprofDumpHandler();
+        if (handler != null) {
+            if (result == 0) {
+                handler.onSuccess(filename, client);
+
+                Log.d("ddm-heap", "Heap dump request has finished");
+            } else {
+                handler.onEndFailure(client, null);
+                Log.w("ddm-heap", "Heap dump request failed (check device log)");
+            }
+        }
+    }
+
+    /*
+     * Handle HeaP Dump Streaming response.  "data" contains the full
+     * hprof dump.
+     */
+    private void handleHPDS(Client client, ByteBuffer data) {
+        IHprofDumpHandler handler = ClientData.getHprofDumpHandler();
+        if (handler != null) {
+            byte[] stuff = new byte[data.capacity()];
+            data.get(stuff, 0, stuff.length);
+
+            Log.d("ddm-hprof", "got hprof file, size: " + data.capacity() + " bytes");
+
+            handler.onSuccess(stuff, client);
+        }
+    }
+
+    /**
+     * Sends a REAE (REcent Allocation Enable) request to the client.
+     */
+    public static void sendREAE(Client client, boolean enable)
+        throws IOException {
+        ByteBuffer rawBuf = allocBuffer(1);
+        JdwpPacket packet = new JdwpPacket(rawBuf);
+        ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+        buf.put((byte) (enable ? 1 : 0));
+
+        finishChunkPacket(packet, CHUNK_REAE, buf.position());
+        Log.d("ddm-heap", "Sending " + name(CHUNK_REAE) + ": " + enable);
+        client.sendAndConsume(packet, mInst);
+    }
+
+    /**
+     * Sends a REAQ (REcent Allocation Query) request to the client.
+     */
+    public static void sendREAQ(Client client)
+        throws IOException {
+        ByteBuffer rawBuf = allocBuffer(0);
+        JdwpPacket packet = new JdwpPacket(rawBuf);
+        ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+        // no data
+
+        finishChunkPacket(packet, CHUNK_REAQ, buf.position());
+        Log.d("ddm-heap", "Sending " + name(CHUNK_REAQ));
+        client.sendAndConsume(packet, mInst);
+    }
+
+    /**
+     * Sends a REAL (REcent ALlocation) request to the client.
+     */
+    public static void sendREAL(Client client)
+        throws IOException {
+        ByteBuffer rawBuf = allocBuffer(0);
+        JdwpPacket packet = new JdwpPacket(rawBuf);
+        ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+        // no data
+
+        finishChunkPacket(packet, CHUNK_REAL, buf.position());
+        Log.d("ddm-heap", "Sending " + name(CHUNK_REAL));
+        client.sendAndConsume(packet, mInst);
+    }
+
+    /*
+     * Handle the response from our REcent Allocation Query message.
+     */
+    private void handleREAQ(Client client, ByteBuffer data) {
+        boolean enabled;
+
+        enabled = (data.get() != 0);
+        Log.d("ddm-heap", "REAQ says: enabled=" + enabled);
+
+        client.getClientData().setAllocationStatus(enabled ?
+                AllocationTrackingStatus.ON : AllocationTrackingStatus.OFF);
+        client.update(Client.CHANGE_HEAP_ALLOCATION_STATUS);
+    }
+
+    /**
+     * Converts a VM class descriptor string ("Landroid/os/Debug;") to
+     * a dot-notation class name ("android.os.Debug").
+     */
+    private String descriptorToDot(String str) {
+        // count the number of arrays.
+        int array = 0;
+        while (str.startsWith("[")) {
+            str = str.substring(1);
+            array++;
+        }
+
+        int len = str.length();
+
+        /* strip off leading 'L' and trailing ';' if appropriate */
+        if (len >= 2 && str.charAt(0) == 'L' && str.charAt(len - 1) == ';') {
+            str = str.substring(1, len-1);
+            str = str.replace('/', '.');
+        } else {
+            // convert the basic types
+            if ("C".equals(str)) {
+                str = "char";
+            } else if ("B".equals(str)) {
+                str = "byte";
+            } else if ("Z".equals(str)) {
+                str = "boolean";
+            } else if ("S".equals(str)) {
+                str = "short";
+            } else if ("I".equals(str)) {
+                str = "int";
+            } else if ("J".equals(str)) {
+                str = "long";
+            } else if ("F".equals(str)) {
+                str = "float";
+            } else if ("D".equals(str)) {
+                str = "double";
+            }
+        }
+
+        // now add the array part
+        for (int a = 0 ; a < array; a++) {
+            str = str + "[]";
+        }
+
+        return str;
+    }
+
+    /**
+     * Reads a string table out of "data".
+     *
+     * This is just a serial collection of strings, each of which is a
+     * four-byte length followed by UTF-16 data.
+     */
+    private void readStringTable(ByteBuffer data, String[] strings) {
+        int count = strings.length;
+        int i;
+
+        for (i = 0; i < count; i++) {
+            int nameLen = data.getInt();
+            String descriptor = getString(data, nameLen);
+            strings[i] = descriptorToDot(descriptor);
+        }
+    }
+
+    /*
+     * Handle a REcent ALlocation response.
+     *
+     * Message header (all values big-endian):
+     *   (1b) message header len (to allow future expansion); includes itself
+     *   (1b) entry header len
+     *   (1b) stack frame len
+     *   (2b) number of entries
+     *   (4b) offset to string table from start of message
+     *   (2b) number of class name strings
+     *   (2b) number of method name strings
+     *   (2b) number of source file name strings
+     *   For each entry:
+     *     (4b) total allocation size
+     *     (2b) threadId
+     *     (2b) allocated object's class name index
+     *     (1b) stack depth
+     *     For each stack frame:
+     *       (2b) method's class name
+     *       (2b) method name
+     *       (2b) method source file
+     *       (2b) line number, clipped to 32767; -2 if native; -1 if no source
+     *   (xb) class name strings
+     *   (xb) method name strings
+     *   (xb) source file strings
+     *
+     *   As with other DDM traffic, strings are sent as a 4-byte length
+     *   followed by UTF-16 data.
+     */
+    private void handleREAL(Client client, ByteBuffer data) {
+        Log.e("ddm-heap", "*** Received " + name(CHUNK_REAL));
+        int messageHdrLen, entryHdrLen, stackFrameLen;
+        int numEntries, offsetToStrings;
+        int numClassNames, numMethodNames, numFileNames;
+
+        /*
+         * Read the header.
+         */
+        messageHdrLen = (data.get() & 0xff);
+        entryHdrLen = (data.get() & 0xff);
+        stackFrameLen = (data.get() & 0xff);
+        numEntries = (data.getShort() & 0xffff);
+        offsetToStrings = data.getInt();
+        numClassNames = (data.getShort() & 0xffff);
+        numMethodNames = (data.getShort() & 0xffff);
+        numFileNames = (data.getShort() & 0xffff);
+
+
+        /*
+         * Skip forward to the strings and read them.
+         */
+        data.position(offsetToStrings);
+
+        String[] classNames = new String[numClassNames];
+        String[] methodNames = new String[numMethodNames];
+        String[] fileNames = new String[numFileNames];
+
+        readStringTable(data, classNames);
+        readStringTable(data, methodNames);
+        //System.out.println("METHODS: "
+        //    + java.util.Arrays.deepToString(methodNames));
+        readStringTable(data, fileNames);
+
+        /*
+         * Skip back to a point just past the header and start reading
+         * entries.
+         */
+        data.position(messageHdrLen);
+
+        ArrayList<AllocationInfo> list = new ArrayList<AllocationInfo>(numEntries);
+        int allocNumber = numEntries; // order value for the entry. This is sent in reverse order.
+        for (int i = 0; i < numEntries; i++) {
+            int totalSize;
+            int threadId, classNameIndex, stackDepth;
+
+            totalSize = data.getInt();
+            threadId = (data.getShort() & 0xffff);
+            classNameIndex = (data.getShort() & 0xffff);
+            stackDepth = (data.get() & 0xff);
+            /* we've consumed 9 bytes; gobble up any extra */
+            for (int skip = 9; skip < entryHdrLen; skip++)
+                data.get();
+
+            StackTraceElement[] steArray = new StackTraceElement[stackDepth];
+
+            /*
+             * Pull out the stack trace.
+             */
+            for (int sti = 0; sti < stackDepth; sti++) {
+                int methodClassNameIndex, methodNameIndex;
+                int methodSourceFileIndex;
+                short lineNumber;
+                String methodClassName, methodName, methodSourceFile;
+
+                methodClassNameIndex = (data.getShort() & 0xffff);
+                methodNameIndex = (data.getShort() & 0xffff);
+                methodSourceFileIndex = (data.getShort() & 0xffff);
+                lineNumber = data.getShort();
+
+                methodClassName = classNames[methodClassNameIndex];
+                methodName = methodNames[methodNameIndex];
+                methodSourceFile = fileNames[methodSourceFileIndex];
+
+                steArray[sti] = new StackTraceElement(methodClassName,
+                    methodName, methodSourceFile, lineNumber);
+
+                /* we've consumed 8 bytes; gobble up any extra */
+                for (int skip = 9; skip < stackFrameLen; skip++)
+                    data.get();
+            }
+
+            list.add(new AllocationInfo(allocNumber--, classNames[classNameIndex],
+                totalSize, (short) threadId, steArray));
+        }
+
+        client.getClientData().setAllocations(list.toArray(new AllocationInfo[numEntries]));
+        client.update(Client.CHANGE_HEAP_ALLOCATIONS);
+    }
+
+    /*
+     * For debugging: dump the contents of an AllocRecord array.
+     *
+     * The array starts with the oldest known allocation and ends with
+     * the most recent allocation.
+     */
+    @SuppressWarnings("unused")
+    private static void dumpRecords(AllocationInfo[] records) {
+        System.out.println("Found " + records.length + " records:");
+
+        for (AllocationInfo rec: records) {
+            System.out.println("tid=" + rec.getThreadId() + " "
+                + rec.getAllocatedClass() + " (" + rec.getSize() + " bytes)");
+
+            for (StackTraceElement ste: rec.getStackTrace()) {
+                if (ste.isNativeMethod()) {
+                    System.out.println("    " + ste.getClassName()
+                        + "." + ste.getMethodName()
+                        + " (Native method)");
+                } else {
+                    System.out.println("    " + ste.getClassName()
+                        + "." + ste.getMethodName()
+                        + " (" + ste.getFileName()
+                        + ":" + ste.getLineNumber() + ")");
+                }
+            }
+        }
+    }
+
+}
+
diff --git a/ddmlib/src/main/java/com/android/ddmlib/HandleHello.java b/ddmlib/src/main/java/com/android/ddmlib/HandleHello.java
new file mode 100644
index 0000000..b5c2968
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/HandleHello.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+
+/**
+ * Handle the "hello" chunk (HELO) and feature discovery.
+ */
+final class HandleHello extends ChunkHandler {
+
+    public static final int CHUNK_HELO = ChunkHandler.type("HELO");
+    public static final int CHUNK_FEAT = ChunkHandler.type("FEAT");
+
+    private static final HandleHello mInst = new HandleHello();
+
+    private HandleHello() {}
+
+    /**
+     * Register for the packets we expect to get from the client.
+     */
+    public static void register(MonitorThread mt) {
+        mt.registerChunkHandler(CHUNK_HELO, mInst);
+    }
+
+    /**
+     * Client is ready.
+     */
+    @Override
+    public void clientReady(Client client) throws IOException {
+        Log.d("ddm-hello", "Now ready: " + client);
+    }
+
+    /**
+     * Client went away.
+     */
+    @Override
+    public void clientDisconnected(Client client) {
+        Log.d("ddm-hello", "Now disconnected: " + client);
+    }
+
+    /**
+     * Sends HELLO-type commands to the VM after a good handshake.
+     * @param client
+     * @param serverProtocolVersion
+     * @throws IOException
+     */
+    public static void sendHelloCommands(Client client, int serverProtocolVersion)
+            throws IOException {
+        sendHELO(client, serverProtocolVersion);
+        sendFEAT(client);
+        HandleProfiling.sendMPRQ(client);
+    }
+
+    /**
+     * Chunk handler entry point.
+     */
+    @Override
+    public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) {
+
+        Log.d("ddm-hello", "handling " + ChunkHandler.name(type));
+
+        if (type == CHUNK_HELO) {
+            assert isReply;
+            handleHELO(client, data);
+        } else if (type == CHUNK_FEAT) {
+            handleFEAT(client, data);
+        } else {
+            handleUnknownChunk(client, type, data, isReply, msgId);
+        }
+    }
+
+    /*
+     * Handle a reply to our HELO message.
+     */
+    private static void handleHELO(Client client, ByteBuffer data) {
+        int version, pid, vmIdentLen, appNameLen;
+        String vmIdent, appName;
+
+        version = data.getInt();
+        pid = data.getInt();
+        vmIdentLen = data.getInt();
+        appNameLen = data.getInt();
+
+        vmIdent = getString(data, vmIdentLen);
+        appName = getString(data, appNameLen);
+
+        // Newer devices send user id in the APNM packet.
+        int userId = -1;
+        boolean validUserId = false;
+        if (data.hasRemaining()) {
+            try {
+                userId = data.getInt();
+                validUserId = true;
+            } catch (BufferUnderflowException e) {
+                // five integers + two utf-16 strings
+                int expectedPacketLength = 20 + appNameLen * 2 + vmIdentLen * 2;
+
+                Log.e("ddm-hello", "Insufficient data in HELO chunk to retrieve user id.");
+                Log.e("ddm-hello", "Actual chunk length: " + data.capacity());
+                Log.e("ddm-hello", "Expected chunk length: " + expectedPacketLength);
+            }
+        }
+
+        Log.d("ddm-hello", "HELO: v=" + version + ", pid=" + pid
+            + ", vm='" + vmIdent + "', app='" + appName + "'");
+
+        ClientData cd = client.getClientData();
+
+        synchronized (cd) {
+            if (cd.getPid() == pid) {
+                cd.setVmIdentifier(vmIdent);
+                cd.setClientDescription(appName);
+                cd.isDdmAware(true);
+
+                if (validUserId) {
+                    cd.setUserId(userId);
+                }
+            } else {
+                Log.e("ddm-hello", "Received pid (" + pid + ") does not match client pid ("
+                        + cd.getPid() + ")");
+            }
+        }
+
+        client = checkDebuggerPortForAppName(client, appName);
+
+        if (client != null) {
+            client.update(Client.CHANGE_NAME);
+        }
+    }
+
+
+    /**
+     * Send a HELO request to the client.
+     */
+    public static void sendHELO(Client client, int serverProtocolVersion)
+        throws IOException
+    {
+        ByteBuffer rawBuf = allocBuffer(4);
+        JdwpPacket packet = new JdwpPacket(rawBuf);
+        ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+        buf.putInt(serverProtocolVersion);
+
+        finishChunkPacket(packet, CHUNK_HELO, buf.position());
+        Log.d("ddm-hello", "Sending " + name(CHUNK_HELO)
+            + " ID=0x" + Integer.toHexString(packet.getId()));
+        client.sendAndConsume(packet, mInst);
+    }
+
+    /**
+     * Handle a reply to our FEAT request.
+     */
+    private static void handleFEAT(Client client, ByteBuffer data) {
+        int featureCount;
+        int i;
+
+        featureCount = data.getInt();
+        for (i = 0; i < featureCount; i++) {
+            int len = data.getInt();
+            String feature = getString(data, len);
+            client.getClientData().addFeature(feature);
+
+            Log.d("ddm-hello", "Feature: " + feature);
+        }
+    }
+
+    /**
+     * Send a FEAT request to the client.
+     */
+    public static void sendFEAT(Client client) throws IOException {
+        ByteBuffer rawBuf = allocBuffer(0);
+        JdwpPacket packet = new JdwpPacket(rawBuf);
+        ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+        // no data
+
+        finishChunkPacket(packet, CHUNK_FEAT, buf.position());
+        Log.d("ddm-heap", "Sending " + name(CHUNK_FEAT));
+        client.sendAndConsume(packet, mInst);
+    }
+}
+
diff --git a/ddmlib/src/main/java/com/android/ddmlib/HandleNativeHeap.java b/ddmlib/src/main/java/com/android/ddmlib/HandleNativeHeap.java
new file mode 100644
index 0000000..c3e6211
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/HandleNativeHeap.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Handle thread status updates.
+ */
+final class HandleNativeHeap extends ChunkHandler {
+
+    public static final int CHUNK_NHGT = type("NHGT"); //$NON-NLS-1$
+    public static final int CHUNK_NHSG = type("NHSG"); //$NON-NLS-1$
+    public static final int CHUNK_NHST = type("NHST"); //$NON-NLS-1$
+    public static final int CHUNK_NHEN = type("NHEN"); //$NON-NLS-1$
+
+    private static final HandleNativeHeap mInst = new HandleNativeHeap();
+
+    private HandleNativeHeap() {
+    }
+
+
+    /**
+     * Register for the packets we expect to get from the client.
+     */
+    public static void register(MonitorThread mt) {
+        mt.registerChunkHandler(CHUNK_NHGT, mInst);
+        mt.registerChunkHandler(CHUNK_NHSG, mInst);
+        mt.registerChunkHandler(CHUNK_NHST, mInst);
+        mt.registerChunkHandler(CHUNK_NHEN, mInst);
+    }
+
+    /**
+     * Client is ready.
+     */
+    @Override
+    public void clientReady(Client client) throws IOException {}
+
+    /**
+     * Client went away.
+     */
+    @Override
+    public void clientDisconnected(Client client) {}
+
+    /**
+     * Chunk handler entry point.
+     */
+    @Override
+    public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) {
+
+        Log.d("ddm-nativeheap", "handling " + ChunkHandler.name(type));
+
+        if (type == CHUNK_NHGT) {
+            handleNHGT(client, data);
+        } else if (type == CHUNK_NHST) {
+            // start chunk before any NHSG chunk(s)
+            client.getClientData().getNativeHeapData().clearHeapData();
+        } else if (type == CHUNK_NHEN) {
+            // end chunk after NHSG chunk(s)
+            client.getClientData().getNativeHeapData().sealHeapData();
+        } else if (type == CHUNK_NHSG) {
+            handleNHSG(client, data);
+        } else {
+            handleUnknownChunk(client, type, data, isReply, msgId);
+        }
+
+        client.update(Client.CHANGE_NATIVE_HEAP_DATA);
+    }
+
+    /**
+     * Send an NHGT (Native Thread GeT) request to the client.
+     */
+    public static void sendNHGT(Client client) throws IOException {
+
+        ByteBuffer rawBuf = allocBuffer(0);
+        JdwpPacket packet = new JdwpPacket(rawBuf);
+        ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+        // no data in request message
+
+        finishChunkPacket(packet, CHUNK_NHGT, buf.position());
+        Log.d("ddm-nativeheap", "Sending " + name(CHUNK_NHGT));
+        client.sendAndConsume(packet, mInst);
+
+        rawBuf = allocBuffer(2);
+        packet = new JdwpPacket(rawBuf);
+        buf = getChunkDataBuf(rawBuf);
+
+        buf.put((byte)HandleHeap.WHEN_DISABLE);
+        buf.put((byte)HandleHeap.WHAT_OBJ);
+
+        finishChunkPacket(packet, CHUNK_NHSG, buf.position());
+        Log.d("ddm-nativeheap", "Sending " + name(CHUNK_NHSG));
+        client.sendAndConsume(packet, mInst);
+    }
+
+    /*
+     * Handle our native heap data.
+     */
+    private void handleNHGT(Client client, ByteBuffer data) {
+        ClientData cd = client.getClientData();
+
+        Log.d("ddm-nativeheap", "NHGT: " + data.limit() + " bytes");
+
+        // TODO - process incoming data and save in "cd"
+        byte[] copy = new byte[data.limit()];
+        data.get(copy);
+
+        // clear the previous run
+        cd.clearNativeAllocationInfo();
+
+        ByteBuffer buffer = ByteBuffer.wrap(copy);
+        buffer.order(ByteOrder.LITTLE_ENDIAN);
+
+//        read the header
+//        typedef struct Header {
+//            uint32_t mapSize;
+//            uint32_t allocSize;
+//            uint32_t allocInfoSize;
+//            uint32_t totalMemory;
+//              uint32_t backtraceSize;
+//        };
+
+        int mapSize = buffer.getInt();
+        int allocSize = buffer.getInt();
+        int allocInfoSize = buffer.getInt();
+        int totalMemory = buffer.getInt();
+        int backtraceSize = buffer.getInt();
+
+        Log.d("ddms", "mapSize: " + mapSize);
+        Log.d("ddms", "allocSize: " + allocSize);
+        Log.d("ddms", "allocInfoSize: " + allocInfoSize);
+        Log.d("ddms", "totalMemory: " + totalMemory);
+
+        cd.setTotalNativeMemory(totalMemory);
+
+        // this means that updates aren't turned on.
+        if (allocInfoSize == 0)
+          return;
+
+        if (mapSize > 0) {
+            byte[] maps = new byte[mapSize];
+            buffer.get(maps, 0, mapSize);
+            parseMaps(cd, maps);
+        }
+
+        int iterations = allocSize / allocInfoSize;
+
+        for (int i = 0 ; i < iterations ; i++) {
+            NativeAllocationInfo info = new NativeAllocationInfo(
+                    buffer.getInt() /* size */,
+                    buffer.getInt() /* allocations */);
+
+            for (int j = 0 ; j < backtraceSize ; j++) {
+                long addr = (buffer.getInt()) & 0x00000000ffffffffL;
+
+                if (addr == 0x0) {
+                    // skip past null addresses
+                    continue;
+                }
+
+                info.addStackCallAddress(addr);
+            }
+
+            cd.addNativeAllocation(info);
+        }
+    }
+
+    private void handleNHSG(Client client, ByteBuffer data) {
+        byte dataCopy[] = new byte[data.limit()];
+        data.rewind();
+        data.get(dataCopy);
+        data = ByteBuffer.wrap(dataCopy);
+        client.getClientData().getNativeHeapData().addHeapData(data);
+
+        if (true) {
+            return;
+        }
+
+        // WORK IN PROGRESS
+
+//        Log.e("ddm-nativeheap", "NHSG: ----------------------------------");
+//        Log.e("ddm-nativeheap", "NHSG: " + data.limit() + " bytes");
+
+        byte[] copy = new byte[data.limit()];
+        data.get(copy);
+
+        ByteBuffer buffer = ByteBuffer.wrap(copy);
+        buffer.order(ByteOrder.BIG_ENDIAN);
+
+        int id = buffer.getInt();
+        int unitsize = buffer.get();
+        long startAddress = buffer.getInt() & 0x00000000ffffffffL;
+        int offset = buffer.getInt();
+        int allocationUnitCount = buffer.getInt();
+
+//        Log.e("ddm-nativeheap", "id: " + id);
+//        Log.e("ddm-nativeheap", "unitsize: " + unitsize);
+//        Log.e("ddm-nativeheap", "startAddress: 0x" + Long.toHexString(startAddress));
+//        Log.e("ddm-nativeheap", "offset: " + offset);
+//        Log.e("ddm-nativeheap", "allocationUnitCount: " + allocationUnitCount);
+//        Log.e("ddm-nativeheap", "end: 0x" +
+//                Long.toHexString(startAddress + unitsize * allocationUnitCount));
+
+        // read the usage
+        while (buffer.position() < buffer.limit()) {
+            int eState = buffer.get() & 0x000000ff;
+            int eLen = (buffer.get() & 0x000000ff) + 1;
+            //Log.e("ddm-nativeheap", "solidity: " + (eState & 0x7) + " - kind: "
+            //        + ((eState >> 3) & 0x7) + " - len: " + eLen);
+        }
+
+
+//        count += unitsize * allocationUnitCount;
+//        Log.e("ddm-nativeheap", "count = " + count);
+
+    }
+
+    private void parseMaps(ClientData cd, byte[] maps) {
+        InputStreamReader input = new InputStreamReader(new ByteArrayInputStream(maps));
+        BufferedReader reader = new BufferedReader(input);
+
+        String line;
+
+        try {
+
+            // most libraries are defined on several lines, so we need to make sure we parse
+            // all the library lines and only add the library at the end
+            long startAddr = 0;
+            long endAddr = 0;
+            String library = null;
+
+            while ((line = reader.readLine()) != null) {
+                Log.d("ddms", "line: " + line);
+                if (line.length() < 16) {
+                    continue;
+                }
+
+                try {
+                    long tmpStart = Long.parseLong(line.substring(0, 8), 16);
+                    long tmpEnd = Long.parseLong(line.substring(9, 17), 16);
+
+                    int index = line.indexOf('/');
+
+                    if (index == -1)
+                        continue;
+
+                    String tmpLib = line.substring(index);
+
+                    if (library == null ||
+                            (library != null && !tmpLib.equals(library))) {
+
+                        if (library != null) {
+                            cd.addNativeLibraryMapInfo(startAddr, endAddr, library);
+                            Log.d("ddms", library + "(" + Long.toHexString(startAddr) +
+                                    " - " + Long.toHexString(endAddr) + ")");
+                        }
+
+                        // now init the new library
+                        library = tmpLib;
+                        startAddr = tmpStart;
+                        endAddr = tmpEnd;
+                    } else {
+                        // add the new end
+                        endAddr = tmpEnd;
+                    }
+                } catch (NumberFormatException e) {
+                    e.printStackTrace();
+                }
+            }
+
+            if (library != null) {
+                cd.addNativeLibraryMapInfo(startAddr, endAddr, library);
+                Log.d("ddms", library + "(" + Long.toHexString(startAddr) +
+                        " - " + Long.toHexString(endAddr) + ")");
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+
+}
+
diff --git a/ddmlib/src/main/java/com/android/ddmlib/HandleProfiling.java b/ddmlib/src/main/java/com/android/ddmlib/HandleProfiling.java
new file mode 100644
index 0000000..9d01fdf
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/HandleProfiling.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2009 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.ddmlib;
+
+import com.android.ddmlib.ClientData.IMethodProfilingHandler;
+import com.android.ddmlib.ClientData.MethodProfilingStatus;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * Handle heap status updates.
+ */
+final class HandleProfiling extends ChunkHandler {
+
+    public static final int CHUNK_MPRS = type("MPRS");
+    public static final int CHUNK_MPRE = type("MPRE");
+    public static final int CHUNK_MPSS = type("MPSS");
+    public static final int CHUNK_MPSE = type("MPSE");
+    public static final int CHUNK_MPRQ = type("MPRQ");
+    public static final int CHUNK_FAIL = type("FAIL");
+
+    private static final HandleProfiling mInst = new HandleProfiling();
+
+    private HandleProfiling() {}
+
+    /**
+     * Register for the packets we expect to get from the client.
+     */
+    public static void register(MonitorThread mt) {
+        mt.registerChunkHandler(CHUNK_MPRE, mInst);
+        mt.registerChunkHandler(CHUNK_MPSE, mInst);
+        mt.registerChunkHandler(CHUNK_MPRQ, mInst);
+    }
+
+    /**
+     * Client is ready.
+     */
+    @Override
+    public void clientReady(Client client) throws IOException {}
+
+    /**
+     * Client went away.
+     */
+    @Override
+    public void clientDisconnected(Client client) {}
+
+    /**
+     * Chunk handler entry point.
+     */
+    @Override
+    public void handleChunk(Client client, int type, ByteBuffer data,
+        boolean isReply, int msgId) {
+
+        Log.d("ddm-prof", "handling " + ChunkHandler.name(type));
+
+        if (type == CHUNK_MPRE) {
+            handleMPRE(client, data);
+        } else if (type == CHUNK_MPSE) {
+            handleMPSE(client, data);
+        } else if (type == CHUNK_MPRQ) {
+            handleMPRQ(client, data);
+        } else if (type == CHUNK_FAIL) {
+            handleFAIL(client, data);
+        } else {
+            handleUnknownChunk(client, type, data, isReply, msgId);
+        }
+    }
+
+    /**
+     * Send a MPRS (Method PRofiling Start) request to the client.
+     *
+     * The arguments to this method will eventually be passed to
+     * android.os.Debug.startMethodTracing() on the device.
+     *
+     * @param fileName is the name of the file to which profiling data
+     *          will be written (on the device); it will have {@link DdmConstants#DOT_TRACE}
+     *          appended if necessary
+     * @param bufferSize is the desired buffer size in bytes (8MB is good)
+     * @param flags see startMethodTracing() docs; use 0 for default behavior
+     */
+    public static void sendMPRS(Client client, String fileName, int bufferSize,
+        int flags) throws IOException {
+
+        ByteBuffer rawBuf = allocBuffer(3*4 + fileName.length() * 2);
+        JdwpPacket packet = new JdwpPacket(rawBuf);
+        ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+        buf.putInt(bufferSize);
+        buf.putInt(flags);
+        buf.putInt(fileName.length());
+        putString(buf, fileName);
+
+        finishChunkPacket(packet, CHUNK_MPRS, buf.position());
+        Log.d("ddm-prof", "Sending " + name(CHUNK_MPRS) + " '" + fileName
+            + "', size=" + bufferSize + ", flags=" + flags);
+        client.sendAndConsume(packet, mInst);
+
+        // record the filename we asked for.
+        client.getClientData().setPendingMethodProfiling(fileName);
+
+        // send a status query. this ensure that the status is properly updated if for some
+        // reason starting the tracing failed.
+        sendMPRQ(client);
+    }
+
+    /**
+     * Send a MPRE (Method PRofiling End) request to the client.
+     */
+    public static void sendMPRE(Client client) throws IOException {
+        ByteBuffer rawBuf = allocBuffer(0);
+        JdwpPacket packet = new JdwpPacket(rawBuf);
+        ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+        // no data
+
+        finishChunkPacket(packet, CHUNK_MPRE, buf.position());
+        Log.d("ddm-prof", "Sending " + name(CHUNK_MPRE));
+        client.sendAndConsume(packet, mInst);
+    }
+
+    /**
+     * Handle notification that method profiling has finished writing
+     * data to disk.
+     */
+    private void handleMPRE(Client client, ByteBuffer data) {
+        byte result;
+
+        // get the filename and make the client not have pending HPROF dump anymore.
+        String filename = client.getClientData().getPendingMethodProfiling();
+        client.getClientData().setPendingMethodProfiling(null);
+
+        result = data.get();
+
+        // get the app-level handler for method tracing dump
+        IMethodProfilingHandler handler = ClientData.getMethodProfilingHandler();
+        if (handler != null) {
+            if (result == 0) {
+                handler.onSuccess(filename, client);
+
+                Log.d("ddm-prof", "Method profiling has finished");
+            } else {
+                handler.onEndFailure(client, null /*message*/);
+
+                Log.w("ddm-prof", "Method profiling has failed (check device log)");
+            }
+        }
+
+        client.getClientData().setMethodProfilingStatus(MethodProfilingStatus.OFF);
+        client.update(Client.CHANGE_METHOD_PROFILING_STATUS);
+    }
+
+    /**
+     * Send a MPSS (Method Profiling Streaming Start) request to the client.
+     *
+     * The arguments to this method will eventually be passed to
+     * android.os.Debug.startMethodTracing() on the device.
+     *
+     * @param bufferSize is the desired buffer size in bytes (8MB is good)
+     * @param flags see startMethodTracing() docs; use 0 for default behavior
+     */
+    public static void sendMPSS(Client client, int bufferSize,
+        int flags) throws IOException {
+
+        ByteBuffer rawBuf = allocBuffer(2*4);
+        JdwpPacket packet = new JdwpPacket(rawBuf);
+        ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+        buf.putInt(bufferSize);
+        buf.putInt(flags);
+
+        finishChunkPacket(packet, CHUNK_MPSS, buf.position());
+        Log.d("ddm-prof", "Sending " + name(CHUNK_MPSS)
+            + "', size=" + bufferSize + ", flags=" + flags);
+        client.sendAndConsume(packet, mInst);
+
+        // send a status query. this ensure that the status is properly updated if for some
+        // reason starting the tracing failed.
+        sendMPRQ(client);
+    }
+
+    /**
+     * Send a MPSE (Method Profiling Streaming End) request to the client.
+     */
+    public static void sendMPSE(Client client) throws IOException {
+        ByteBuffer rawBuf = allocBuffer(0);
+        JdwpPacket packet = new JdwpPacket(rawBuf);
+        ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+        // no data
+
+        finishChunkPacket(packet, CHUNK_MPSE, buf.position());
+        Log.d("ddm-prof", "Sending " + name(CHUNK_MPSE));
+        client.sendAndConsume(packet, mInst);
+    }
+
+    /**
+     * Handle incoming profiling data.  The MPSE packet includes the
+     * complete .trace file.
+     */
+    private void handleMPSE(Client client, ByteBuffer data) {
+        IMethodProfilingHandler handler = ClientData.getMethodProfilingHandler();
+        if (handler != null) {
+            byte[] stuff = new byte[data.capacity()];
+            data.get(stuff, 0, stuff.length);
+
+            Log.d("ddm-prof", "got trace file, size: " + stuff.length + " bytes");
+
+            handler.onSuccess(stuff, client);
+        }
+
+        client.getClientData().setMethodProfilingStatus(MethodProfilingStatus.OFF);
+        client.update(Client.CHANGE_METHOD_PROFILING_STATUS);
+    }
+
+    /**
+     * Send a MPRQ (Method PRofiling Query) request to the client.
+     */
+    public static void sendMPRQ(Client client) throws IOException {
+        ByteBuffer rawBuf = allocBuffer(0);
+        JdwpPacket packet = new JdwpPacket(rawBuf);
+        ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+        // no data
+
+        finishChunkPacket(packet, CHUNK_MPRQ, buf.position());
+        Log.d("ddm-prof", "Sending " + name(CHUNK_MPRQ));
+        client.sendAndConsume(packet, mInst);
+    }
+
+    /**
+     * Receive response to query.
+     */
+    private void handleMPRQ(Client client, ByteBuffer data) {
+        byte result;
+
+        result = data.get();
+
+        if (result == 0) {
+            client.getClientData().setMethodProfilingStatus(MethodProfilingStatus.OFF);
+            Log.d("ddm-prof", "Method profiling is not running");
+        } else {
+            client.getClientData().setMethodProfilingStatus(MethodProfilingStatus.ON);
+            Log.d("ddm-prof", "Method profiling is running");
+        }
+        client.update(Client.CHANGE_METHOD_PROFILING_STATUS);
+    }
+
+    private void handleFAIL(Client client, ByteBuffer data) {
+        /*int errorCode =*/ data.getInt();
+        int length = data.getInt() * 2;
+        String message = null;
+        if (length > 0) {
+            byte[] messageBuffer = new byte[length];
+            data.get(messageBuffer, 0, length);
+            message = new String(messageBuffer);
+        }
+
+        // this can be sent if
+        // - MPRS failed (like wrong permission)
+        // - MPSE failed for whatever reason
+
+        String filename = client.getClientData().getPendingMethodProfiling();
+        if (filename != null) {
+            // reset the pending file.
+            client.getClientData().setPendingMethodProfiling(null);
+
+            // and notify of failure
+            IMethodProfilingHandler handler = ClientData.getMethodProfilingHandler();
+            if (handler != null) {
+                handler.onStartFailure(client, message);
+            }
+        } else {
+            // this is MPRE
+            // notify of failure
+            IMethodProfilingHandler handler = ClientData.getMethodProfilingHandler();
+            if (handler != null) {
+                handler.onEndFailure(client, message);
+            }
+        }
+
+        // send a query to know the current status
+        try {
+            sendMPRQ(client);
+        } catch (IOException e) {
+            Log.e("HandleProfiling", e);
+        }
+    }
+}
+
diff --git a/ddmlib/src/main/java/com/android/ddmlib/HandleTest.java b/ddmlib/src/main/java/com/android/ddmlib/HandleTest.java
new file mode 100644
index 0000000..b9f3a74
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/HandleTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import com.android.ddmlib.Log.LogLevel;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * Handle thread status updates.
+ */
+final class HandleTest extends ChunkHandler {
+
+    public static final int CHUNK_TEST = type("TEST");
+
+    private static final HandleTest mInst = new HandleTest();
+
+
+    private HandleTest() {}
+
+    /**
+     * Register for the packets we expect to get from the client.
+     */
+    public static void register(MonitorThread mt) {
+        mt.registerChunkHandler(CHUNK_TEST, mInst);
+    }
+
+    /**
+     * Client is ready.
+     */
+    @Override
+    public void clientReady(Client client) throws IOException {}
+
+    /**
+     * Client went away.
+     */
+    @Override
+    public void clientDisconnected(Client client) {}
+
+    /**
+     * Chunk handler entry point.
+     */
+    @Override
+    public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) {
+
+        Log.d("ddm-test", "handling " + ChunkHandler.name(type));
+
+        if (type == CHUNK_TEST) {
+            handleTEST(client, data);
+        } else {
+            handleUnknownChunk(client, type, data, isReply, msgId);
+        }
+    }
+
+    /*
+     * Handle a thread creation message.
+     */
+    private void handleTEST(Client client, ByteBuffer data)
+    {
+        /*
+         * Can't call data.array() on a read-only ByteBuffer, so we make
+         * a copy.
+         */
+        byte[] copy = new byte[data.limit()];
+        data.get(copy);
+
+        Log.d("ddm-test", "Received:");
+        Log.hexDump("ddm-test", LogLevel.DEBUG, copy, 0, copy.length);
+    }
+}
+
diff --git a/ddmlib/src/main/java/com/android/ddmlib/HandleThread.java b/ddmlib/src/main/java/com/android/ddmlib/HandleThread.java
new file mode 100644
index 0000000..95b9a8e
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/HandleThread.java
@@ -0,0 +1,379 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * Handle thread status updates.
+ */
+final class HandleThread extends ChunkHandler {
+
+    public static final int CHUNK_THEN = type("THEN");
+    public static final int CHUNK_THCR = type("THCR");
+    public static final int CHUNK_THDE = type("THDE");
+    public static final int CHUNK_THST = type("THST");
+    public static final int CHUNK_THNM = type("THNM");
+    public static final int CHUNK_STKL = type("STKL");
+
+    private static final HandleThread mInst = new HandleThread();
+
+    // only read/written by requestThreadUpdates()
+    private static volatile boolean sThreadStatusReqRunning = false;
+    private static volatile boolean sThreadStackTraceReqRunning = false;
+
+    private HandleThread() {}
+
+
+    /**
+     * Register for the packets we expect to get from the client.
+     */
+    public static void register(MonitorThread mt) {
+        mt.registerChunkHandler(CHUNK_THCR, mInst);
+        mt.registerChunkHandler(CHUNK_THDE, mInst);
+        mt.registerChunkHandler(CHUNK_THST, mInst);
+        mt.registerChunkHandler(CHUNK_THNM, mInst);
+        mt.registerChunkHandler(CHUNK_STKL, mInst);
+    }
+
+    /**
+     * Client is ready.
+     */
+    @Override
+    public void clientReady(Client client) throws IOException {
+        Log.d("ddm-thread", "Now ready: " + client);
+        if (client.isThreadUpdateEnabled())
+            sendTHEN(client, true);
+    }
+
+    /**
+     * Client went away.
+     */
+    @Override
+    public void clientDisconnected(Client client) {}
+
+    /**
+     * Chunk handler entry point.
+     */
+    @Override
+    public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) {
+
+        Log.d("ddm-thread", "handling " + ChunkHandler.name(type));
+
+        if (type == CHUNK_THCR) {
+            handleTHCR(client, data);
+        } else if (type == CHUNK_THDE) {
+            handleTHDE(client, data);
+        } else if (type == CHUNK_THST) {
+            handleTHST(client, data);
+        } else if (type == CHUNK_THNM) {
+            handleTHNM(client, data);
+        } else if (type == CHUNK_STKL) {
+            handleSTKL(client, data);
+        } else {
+            handleUnknownChunk(client, type, data, isReply, msgId);
+        }
+    }
+
+    /*
+     * Handle a thread creation message.
+     *
+     * We should be tolerant of receiving a duplicate create message.  (It
+     * shouldn't happen with the current implementation.)
+     */
+    private void handleTHCR(Client client, ByteBuffer data) {
+        int threadId, nameLen;
+        String name;
+
+        threadId = data.getInt();
+        nameLen = data.getInt();
+        name = getString(data, nameLen);
+
+        Log.v("ddm-thread", "THCR: " + threadId + " '" + name + "'");
+
+        client.getClientData().addThread(threadId, name);
+        client.update(Client.CHANGE_THREAD_DATA);
+    }
+
+    /*
+     * Handle a thread death message.
+     */
+    private void handleTHDE(Client client, ByteBuffer data) {
+        int threadId;
+
+        threadId = data.getInt();
+        Log.v("ddm-thread", "THDE: " + threadId);
+
+        client.getClientData().removeThread(threadId);
+        client.update(Client.CHANGE_THREAD_DATA);
+    }
+
+    /*
+     * Handle a thread status update message.
+     *
+     * Response has:
+     *  (1b) header len
+     *  (1b) bytes per entry
+     *  (2b) thread count
+     * Then, for each thread:
+     *  (4b) threadId (matches value from THCR)
+     *  (1b) thread status
+     *  (4b) tid
+     *  (4b) utime
+     *  (4b) stime
+     */
+    private void handleTHST(Client client, ByteBuffer data) {
+        int headerLen, bytesPerEntry, extraPerEntry;
+        int threadCount;
+
+        headerLen = (data.get() & 0xff);
+        bytesPerEntry = (data.get() & 0xff);
+        threadCount = data.getShort();
+
+        headerLen -= 4;     // we've read 4 bytes
+        while (headerLen-- > 0)
+            data.get();
+
+        extraPerEntry = bytesPerEntry - 18;     // we want 18 bytes
+
+        Log.v("ddm-thread", "THST: threadCount=" + threadCount);
+
+        /*
+         * For each thread, extract the data, find the appropriate
+         * client, and add it to the ClientData.
+         */
+        for (int i = 0; i < threadCount; i++) {
+            int threadId, status, tid, utime, stime;
+            boolean isDaemon = false;
+
+            threadId = data.getInt();
+            status = data.get();
+            tid = data.getInt();
+            utime = data.getInt();
+            stime = data.getInt();
+            if (bytesPerEntry >= 18)
+                isDaemon = (data.get() != 0);
+
+            Log.v("ddm-thread", "  id=" + threadId
+                + ", status=" + status + ", tid=" + tid
+                + ", utime=" + utime + ", stime=" + stime);
+
+            ClientData cd = client.getClientData();
+            ThreadInfo threadInfo = cd.getThread(threadId);
+            if (threadInfo != null)
+                threadInfo.updateThread(status, tid, utime, stime, isDaemon);
+            else
+                Log.d("ddms", "Thread with id=" + threadId + " not found");
+
+            // slurp up any extra
+            for (int slurp = extraPerEntry; slurp > 0; slurp--)
+                data.get();
+        }
+
+        client.update(Client.CHANGE_THREAD_DATA);
+    }
+
+    /*
+     * Handle a THNM (THread NaMe) message.  We get one of these after
+     * somebody calls Thread.setName() on a running thread.
+     */
+    private void handleTHNM(Client client, ByteBuffer data) {
+        int threadId, nameLen;
+        String name;
+
+        threadId = data.getInt();
+        nameLen = data.getInt();
+        name = getString(data, nameLen);
+
+        Log.v("ddm-thread", "THNM: " + threadId + " '" + name + "'");
+
+        ThreadInfo threadInfo = client.getClientData().getThread(threadId);
+        if (threadInfo != null) {
+            threadInfo.setThreadName(name);
+            client.update(Client.CHANGE_THREAD_DATA);
+        } else {
+            Log.d("ddms", "Thread with id=" + threadId + " not found");
+        }
+    }
+
+
+    /**
+     * Parse an incoming STKL.
+     */
+    private void handleSTKL(Client client, ByteBuffer data) {
+        StackTraceElement[] trace;
+        int i, threadId, stackDepth;
+        @SuppressWarnings("unused")
+        int future;
+
+        future = data.getInt();
+        threadId = data.getInt();
+
+        Log.v("ddms", "STKL: " + threadId);
+
+        /* un-serialize the StackTraceElement[] */
+        stackDepth = data.getInt();
+        trace = new StackTraceElement[stackDepth];
+        for (i = 0; i < stackDepth; i++) {
+            String className, methodName, fileName;
+            int len, lineNumber;
+
+            len = data.getInt();
+            className = getString(data, len);
+            len = data.getInt();
+            methodName = getString(data, len);
+            len = data.getInt();
+            if (len == 0) {
+                fileName = null;
+            } else {
+                fileName = getString(data, len);
+            }
+            lineNumber = data.getInt();
+
+            trace[i] = new StackTraceElement(className, methodName, fileName,
+                        lineNumber);
+        }
+
+        ThreadInfo threadInfo = client.getClientData().getThread(threadId);
+        if (threadInfo != null) {
+            threadInfo.setStackCall(trace);
+            client.update(Client.CHANGE_THREAD_STACKTRACE);
+        } else {
+            Log.d("STKL", String.format(
+                    "Got stackcall for thread %1$d, which does not exists (anymore?).", //$NON-NLS-1$
+                    threadId));
+        }
+    }
+
+
+    /**
+     * Send a THEN (THread notification ENable) request to the client.
+     */
+    public static void sendTHEN(Client client, boolean enable)
+        throws IOException {
+
+        ByteBuffer rawBuf = allocBuffer(1);
+        JdwpPacket packet = new JdwpPacket(rawBuf);
+        ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+        if (enable)
+            buf.put((byte)1);
+        else
+            buf.put((byte)0);
+
+        finishChunkPacket(packet, CHUNK_THEN, buf.position());
+        Log.d("ddm-thread", "Sending " + name(CHUNK_THEN) + ": " + enable);
+        client.sendAndConsume(packet, mInst);
+    }
+
+
+    /**
+     * Send a STKL (STacK List) request to the client.  The VM will suspend
+     * the target thread, obtain its stack, and return it.  If the thread
+     * is no longer running, a failure result will be returned.
+     */
+    public static void sendSTKL(Client client, int threadId)
+        throws IOException {
+
+        if (false) {
+            Log.d("ddm-thread", "would send STKL " + threadId);
+            return;
+        }
+
+        ByteBuffer rawBuf = allocBuffer(4);
+        JdwpPacket packet = new JdwpPacket(rawBuf);
+        ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+        buf.putInt(threadId);
+
+        finishChunkPacket(packet, CHUNK_STKL, buf.position());
+        Log.d("ddm-thread", "Sending " + name(CHUNK_STKL) + ": " + threadId);
+        client.sendAndConsume(packet, mInst);
+    }
+
+
+    /**
+     * This is called periodically from the UI thread.  To avoid locking
+     * the UI while we request the updates, we create a new thread.
+     *
+     */
+    static void requestThreadUpdate(final Client client) {
+        if (client.isDdmAware() && client.isThreadUpdateEnabled()) {
+            if (sThreadStatusReqRunning) {
+                Log.w("ddms", "Waiting for previous thread update req to finish");
+                return;
+            }
+
+            new Thread("Thread Status Req") {
+                @Override
+                public void run() {
+                    sThreadStatusReqRunning = true;
+                    try {
+                        sendTHST(client);
+                    } catch (IOException ioe) {
+                        Log.d("ddms", "Unable to request thread updates from "
+                                + client + ": " + ioe.getMessage());
+                    } finally {
+                        sThreadStatusReqRunning = false;
+                    }
+                }
+            }.start();
+        }
+    }
+
+    static void requestThreadStackCallRefresh(final Client client, final int threadId) {
+        if (client.isDdmAware() && client.isThreadUpdateEnabled()) {
+            if (sThreadStackTraceReqRunning) {
+                Log.w("ddms", "Waiting for previous thread stack call req to finish");
+                return;
+            }
+
+            new Thread("Thread Status Req") {
+                @Override
+                public void run() {
+                    sThreadStackTraceReqRunning = true;
+                    try {
+                        sendSTKL(client, threadId);
+                    } catch (IOException ioe) {
+                        Log.d("ddms", "Unable to request thread stack call updates from "
+                                + client + ": " + ioe.getMessage());
+                    } finally {
+                        sThreadStackTraceReqRunning = false;
+                    }
+                }
+            }.start();
+        }
+
+    }
+
+    /*
+     * Send a THST request to the specified client.
+     */
+    private static void sendTHST(Client client) throws IOException {
+        ByteBuffer rawBuf = allocBuffer(0);
+        JdwpPacket packet = new JdwpPacket(rawBuf);
+        ByteBuffer buf = getChunkDataBuf(rawBuf);
+
+        // nothing much to say
+
+        finishChunkPacket(packet, CHUNK_THST, buf.position());
+        Log.d("ddm-thread", "Sending " + name(CHUNK_THST));
+        client.sendAndConsume(packet, mInst);
+    }
+}
+
diff --git a/ddmlib/src/main/java/com/android/ddmlib/HandleViewDebug.java b/ddmlib/src/main/java/com/android/ddmlib/HandleViewDebug.java
new file mode 100644
index 0000000..1a279bd
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/HandleViewDebug.java
@@ -0,0 +1,343 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public final class HandleViewDebug extends ChunkHandler {
+    /** Enable/Disable tracing of OpenGL calls. */
+    public static final int CHUNK_VUGL = type("VUGL");
+
+    /** List {@link ViewRootImpl}'s of this process. */
+    public static final int CHUNK_VULW = type("VULW");
+
+    /** Operation on view root, first parameter in packet should be one of VURT_* constants */
+    public static final int CHUNK_VURT = type("VURT");
+
+    /** Dump view hierarchy. */
+    private static final int VURT_DUMP_HIERARCHY = 1;
+
+    /** Capture View Layers. */
+    private static final int VURT_CAPTURE_LAYERS = 2;
+
+    /**
+     * Generic View Operation, first parameter in the packet should be one of the
+     * VUOP_* constants below.
+     */
+    public static final int CHUNK_VUOP = type("VUOP");
+
+    /** Capture View. */
+    private static final int VUOP_CAPTURE_VIEW = 1;
+
+    /** Obtain the Display List corresponding to the view. */
+    private static final int VUOP_DUMP_DISPLAYLIST = 2;
+
+    /** Profile a view. */
+    private static final int VUOP_PROFILE_VIEW = 3;
+
+    /** Invoke a method on the view. */
+    private static final int VUOP_INVOKE_VIEW_METHOD = 4;
+
+    /** Set layout parameter. */
+    private static final int VUOP_SET_LAYOUT_PARAMETER = 5;
+
+    private static final String TAG = "ddmlib"; //$NON-NLS-1$
+
+    private static final HandleViewDebug sInstance = new HandleViewDebug();
+
+    private static final ViewDumpHandler sViewOpNullChunkHandler =
+            new NullChunkHandler(CHUNK_VUOP);
+
+    private HandleViewDebug() {}
+
+    public static void register(MonitorThread mt) {
+        // TODO: add chunk type for auto window updates
+        // and register here
+        mt.registerChunkHandler(CHUNK_VUGL, sInstance);
+        mt.registerChunkHandler(CHUNK_VULW, sInstance);
+        mt.registerChunkHandler(CHUNK_VUOP, sInstance);
+        mt.registerChunkHandler(CHUNK_VURT, sInstance);
+    }
+
+    @Override
+    public void clientReady(Client client) throws IOException {}
+
+    @Override
+    public void clientDisconnected(Client client) {}
+
+    public abstract static class ViewDumpHandler extends ChunkHandler {
+        private final CountDownLatch mLatch = new CountDownLatch(1);
+        private final int mChunkType;
+
+        public ViewDumpHandler(int chunkType) {
+            mChunkType = chunkType;
+        }
+
+        @Override
+        void clientReady(Client client) throws IOException {
+        }
+
+        @Override
+        void clientDisconnected(Client client) {
+        }
+
+        @Override
+        void handleChunk(Client client, int type, ByteBuffer data,
+                boolean isReply, int msgId) {
+            if (type != mChunkType) {
+                handleUnknownChunk(client, type, data, isReply, msgId);
+                return;
+            }
+
+            handleViewDebugResult(data);
+            mLatch.countDown();
+        }
+
+        protected abstract void handleViewDebugResult(ByteBuffer data);
+
+        protected void waitForResult(long timeout, TimeUnit unit) {
+            try {
+                mLatch.await(timeout, unit);
+            } catch (InterruptedException e) {
+                // pass
+            }
+        }
+    }
+
+    public static void listViewRoots(Client client, ViewDumpHandler replyHandler)
+            throws IOException {
+        ByteBuffer buf = allocBuffer(8);
+        JdwpPacket packet = new JdwpPacket(buf);
+        ByteBuffer chunkBuf = getChunkDataBuf(buf);
+        chunkBuf.putInt(1);
+        finishChunkPacket(packet, CHUNK_VULW, chunkBuf.position());
+        client.sendAndConsume(packet, replyHandler);
+    }
+
+    public static void dumpViewHierarchy(@NonNull Client client, @NonNull String viewRoot,
+            boolean skipChildren, boolean includeProperties, @NonNull ViewDumpHandler handler)
+                    throws IOException {
+        ByteBuffer buf = allocBuffer(4      // opcode
+                + 4                         // view root length
+                + viewRoot.length() * 2     // view root
+                + 4                         // skip children
+                + 4);                       // include view properties
+        JdwpPacket packet = new JdwpPacket(buf);
+        ByteBuffer chunkBuf = getChunkDataBuf(buf);
+
+        chunkBuf.putInt(VURT_DUMP_HIERARCHY);
+        chunkBuf.putInt(viewRoot.length());
+        putString(chunkBuf, viewRoot);
+        chunkBuf.putInt(skipChildren ? 1 : 0);
+        chunkBuf.putInt(includeProperties ? 1 : 0);
+
+        finishChunkPacket(packet, CHUNK_VURT, chunkBuf.position());
+        client.sendAndConsume(packet, handler);
+    }
+
+    public static void captureLayers(@NonNull Client client, @NonNull String viewRoot,
+            @NonNull ViewDumpHandler handler) throws IOException {
+        int bufLen = 8 + viewRoot.length() * 2;
+
+        ByteBuffer buf = allocBuffer(bufLen);
+        JdwpPacket packet = new JdwpPacket(buf);
+        ByteBuffer chunkBuf = getChunkDataBuf(buf);
+
+        chunkBuf.putInt(VURT_CAPTURE_LAYERS);
+        chunkBuf.putInt(viewRoot.length());
+        putString(chunkBuf, viewRoot);
+
+        finishChunkPacket(packet, CHUNK_VURT, chunkBuf.position());
+        client.sendAndConsume(packet, handler);
+    }
+
+    private static void sendViewOpPacket(@NonNull Client client, int op, @NonNull String viewRoot,
+            @NonNull String view, @Nullable byte[] extra, @Nullable ViewDumpHandler handler)
+                    throws IOException {
+        int bufLen = 4 +                        // opcode
+                4 + viewRoot.length() * 2 +     // view root strlen + view root
+                4 + view.length() * 2;          // view strlen + view
+
+        if (extra != null) {
+            bufLen += extra.length;
+        }
+
+        ByteBuffer buf = allocBuffer(bufLen);
+        JdwpPacket packet = new JdwpPacket(buf);
+        ByteBuffer chunkBuf = getChunkDataBuf(buf);
+
+        chunkBuf.putInt(op);
+        chunkBuf.putInt(viewRoot.length());
+        putString(chunkBuf, viewRoot);
+
+        chunkBuf.putInt(view.length());
+        putString(chunkBuf, view);
+
+        if (extra != null) {
+            chunkBuf.put(extra);
+        }
+
+        finishChunkPacket(packet, CHUNK_VUOP, chunkBuf.position());
+        if (handler != null) {
+            client.sendAndConsume(packet, handler);
+        } else {
+            client.sendAndConsume(packet);
+        }
+    }
+
+    public static void profileView(@NonNull Client client, @NonNull String viewRoot,
+            @NonNull String view, @NonNull ViewDumpHandler handler) throws IOException {
+        sendViewOpPacket(client, VUOP_PROFILE_VIEW, viewRoot, view, null, handler);
+    }
+
+    public static void captureView(@NonNull Client client, @NonNull String viewRoot,
+            @NonNull String view, @NonNull ViewDumpHandler handler) throws IOException {
+        sendViewOpPacket(client, VUOP_CAPTURE_VIEW, viewRoot, view, null, handler);
+    }
+
+    public static void invalidateView(@NonNull Client client, @NonNull String viewRoot,
+            @NonNull String view) throws IOException {
+        invokeMethod(client, viewRoot, view, "invalidate");
+    }
+
+    public static void requestLayout(@NonNull Client client, @NonNull String viewRoot,
+            @NonNull String view) throws IOException {
+        invokeMethod(client, viewRoot, view, "requestLayout");
+    }
+
+    public static void dumpDisplayList(@NonNull Client client, @NonNull String viewRoot,
+            @NonNull String view) throws IOException {
+        sendViewOpPacket(client, VUOP_DUMP_DISPLAYLIST, viewRoot, view, null,
+                sViewOpNullChunkHandler);
+    }
+
+    /** A {@link ViewDumpHandler} to use when no response is expected. */
+    private static class NullChunkHandler extends ViewDumpHandler {
+        public NullChunkHandler(int chunkType) {
+            super(chunkType);
+        }
+
+        @Override
+        protected void handleViewDebugResult(ByteBuffer data) {
+        }
+    }
+
+    public static void invokeMethod(@NonNull Client client, @NonNull String viewRoot,
+            @NonNull String view, @NonNull String method, Object... args) throws IOException {
+        int len = 4 + method.length() * 2;
+        if (args != null) {
+            // # of args
+            len += 4;
+
+            // for each argument, we send a char type specifier (2 bytes) and
+            // the arg value (max primitive size = sizeof(double) = 8
+            len += 10 * args.length;
+        }
+
+        byte[] extra = new byte[len];
+        ByteBuffer b = ByteBuffer.wrap(extra);
+
+        b.putInt(method.length());
+        putString(b, method);
+
+        if (args != null) {
+            b.putInt(args.length);
+
+            for (int i = 0; i < args.length; i++) {
+                Object arg = args[i];
+                if (arg instanceof Boolean) {
+                    b.putChar('Z');
+                    b.put((byte) ((Boolean) arg ? 1 : 0));
+                } else if (arg instanceof Byte) {
+                    b.putChar('B');
+                    b.put((Byte) arg);
+                } else if (arg instanceof Character) {
+                    b.putChar('C');
+                    b.putChar((Character) arg);
+                } else if (arg instanceof Short) {
+                    b.putChar('S');
+                    b.putShort((Short) arg);
+                } else if (arg instanceof Integer) {
+                    b.putChar('I');
+                    b.putInt((Integer) arg);
+                } else if (arg instanceof Long) {
+                    b.putChar('J');
+                    b.putLong((Long) arg);
+                } else if (arg instanceof Float) {
+                    b.putChar('F');
+                    b.putFloat((Float) arg);
+                } else if (arg instanceof Double) {
+                    b.putChar('D');
+                    b.putDouble((Double) arg);
+                } else {
+                    Log.e(TAG, "View method invocation only supports primitive arguments, supplied: " + arg);
+                    return;
+                }
+            }
+        }
+
+        sendViewOpPacket(client, VUOP_INVOKE_VIEW_METHOD, viewRoot, view, extra,
+                sViewOpNullChunkHandler );
+    }
+
+    public static void setLayoutParameter(@NonNull Client client, @NonNull String viewRoot,
+            @NonNull String view, @NonNull String parameter, int value) throws IOException {
+        int len = 4 + parameter.length() * 2 + 4;
+        byte[] extra = new byte[len];
+        ByteBuffer b = ByteBuffer.wrap(extra);
+
+        b.putInt(parameter.length());
+        putString(b, parameter);
+        b.putInt(value);
+        sendViewOpPacket(client, VUOP_SET_LAYOUT_PARAMETER, viewRoot, view, extra,
+                sViewOpNullChunkHandler);
+    }
+
+    @Override
+    public void handleChunk(Client client, int type, ByteBuffer data,
+            boolean isReply, int msgId) {
+    }
+
+    public static void sendStartGlTracing(Client client) throws IOException {
+        ByteBuffer buf = allocBuffer(4);
+        JdwpPacket packet = new JdwpPacket(buf);
+
+        ByteBuffer chunkBuf = getChunkDataBuf(buf);
+        chunkBuf.putInt(1);
+        finishChunkPacket(packet, CHUNK_VUGL, chunkBuf.position());
+
+        client.sendAndConsume(packet);
+    }
+
+    public static void sendStopGlTracing(Client client) throws IOException {
+        ByteBuffer buf = allocBuffer(4);
+        JdwpPacket packet = new JdwpPacket(buf);
+
+        ByteBuffer chunkBuf = getChunkDataBuf(buf);
+        chunkBuf.putInt(0);
+        finishChunkPacket(packet, CHUNK_VUGL, chunkBuf.position());
+
+        client.sendAndConsume(packet);
+    }
+}
+
diff --git a/ddmlib/src/main/java/com/android/ddmlib/HandleWait.java b/ddmlib/src/main/java/com/android/ddmlib/HandleWait.java
new file mode 100644
index 0000000..934cbea
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/HandleWait.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import com.android.ddmlib.ClientData.DebuggerStatus;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * Handle the "wait" chunk (WAIT).  These are sent up when the client is
+ * waiting for something, e.g. for a debugger to attach.
+ */
+final class HandleWait extends ChunkHandler {
+
+    public static final int CHUNK_WAIT = ChunkHandler.type("WAIT");
+
+    private static final HandleWait mInst = new HandleWait();
+
+
+    private HandleWait() {}
+
+    /**
+     * Register for the packets we expect to get from the client.
+     */
+    public static void register(MonitorThread mt) {
+        mt.registerChunkHandler(CHUNK_WAIT, mInst);
+    }
+
+    /**
+     * Client is ready.
+     */
+    @Override
+    public void clientReady(Client client) throws IOException {}
+
+    /**
+     * Client went away.
+     */
+    @Override
+    public void clientDisconnected(Client client) {}
+
+    /**
+     * Chunk handler entry point.
+     */
+    @Override
+    public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) {
+
+        Log.d("ddm-wait", "handling " + ChunkHandler.name(type));
+
+        if (type == CHUNK_WAIT) {
+            assert !isReply;
+            handleWAIT(client, data);
+        } else {
+            handleUnknownChunk(client, type, data, isReply, msgId);
+        }
+    }
+
+    /*
+     * Handle a reply to our WAIT message.
+     */
+    private static void handleWAIT(Client client, ByteBuffer data) {
+        byte reason;
+
+        reason = data.get();
+
+        Log.d("ddm-wait", "WAIT: reason=" + reason);
+
+
+        ClientData cd = client.getClientData();
+        synchronized (cd) {
+            cd.setDebuggerConnectionStatus(DebuggerStatus.WAITING);
+        }
+
+        client.update(Client.CHANGE_DEBUGGER_STATUS);
+    }
+}
+
diff --git a/ddmlib/src/main/java/com/android/ddmlib/HeapSegment.java b/ddmlib/src/main/java/com/android/ddmlib/HeapSegment.java
new file mode 100644
index 0000000..b6acd65
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/HeapSegment.java
@@ -0,0 +1,448 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.text.ParseException;
+
+/**
+ * Describes the types and locations of objects in a segment of a heap.
+ */
+public final class HeapSegment implements Comparable<HeapSegment> {
+
+    /**
+     * Describes an object/region encoded in the HPSG data.
+     */
+    public static class HeapSegmentElement implements Comparable<HeapSegmentElement> {
+
+        /*
+         * Solidity values, which must match the values in
+         * the HPSG data.
+         */
+
+        /** The element describes a free block. */
+        public static final int SOLIDITY_FREE = 0;
+
+        /** The element is strongly-reachable. */
+        public static final int SOLIDITY_HARD = 1;
+
+        /** The element is softly-reachable. */
+        public static final int SOLIDITY_SOFT = 2;
+
+        /** The element is weakly-reachable. */
+        public static final int SOLIDITY_WEAK = 3;
+
+        /** The element is phantom-reachable. */
+        public static final int SOLIDITY_PHANTOM = 4;
+
+        /** The element is pending finalization. */
+        public static final int SOLIDITY_FINALIZABLE = 5;
+
+        /** The element is not reachable, and is about to be swept/freed. */
+        public static final int SOLIDITY_SWEEP = 6;
+
+        /** The reachability of the object is unknown. */
+        public static final int SOLIDITY_INVALID = -1;
+
+
+        /*
+         * Kind values, which must match the values in
+         * the HPSG data.
+         */
+
+        /** The element describes a data object. */
+        public static final int KIND_OBJECT = 0;
+
+        /** The element describes a class object. */
+        public static final int KIND_CLASS_OBJECT = 1;
+
+        /** The element describes an array of 1-byte elements. */
+        public static final int KIND_ARRAY_1 = 2;
+
+        /** The element describes an array of 2-byte elements. */
+        public static final int KIND_ARRAY_2 = 3;
+
+        /** The element describes an array of 4-byte elements. */
+        public static final int KIND_ARRAY_4 = 4;
+
+        /** The element describes an array of 8-byte elements. */
+        public static final int KIND_ARRAY_8 = 5;
+
+        /** The element describes an unknown type of object. */
+        public static final int KIND_UNKNOWN = 6;
+
+        /** The element describes a native object. */
+        public static final int KIND_NATIVE = 7;
+
+        /** The object kind is unknown or unspecified. */
+        public static final int KIND_INVALID = -1;
+
+
+        /**
+         * A bit in the HPSG data that indicates that an element should
+         * be combined with the element that follows, typically because
+         * an element is too large to be described by a single element.
+         */
+        private static final int PARTIAL_MASK = 1 << 7;
+
+
+        /**
+         * Describes the reachability/solidity of the element.  Must
+         * be set to one of the SOLIDITY_* values.
+         */
+        private int mSolidity;
+
+        /**
+         * Describes the type/kind of the element.  Must be set to one
+         * of the KIND_* values.
+         */
+        private int mKind;
+
+        /**
+         * Describes the length of the element, in bytes.
+         */
+        private int mLength;
+
+
+        /**
+         * Creates an uninitialized element.
+         */
+        public HeapSegmentElement() {
+            setSolidity(SOLIDITY_INVALID);
+            setKind(KIND_INVALID);
+            setLength(-1);
+        }
+
+        /**
+         * Create an element describing the entry at the current
+         * position of hpsgData.
+         *
+         * @param hs The heap segment to pull the entry from.
+         * @throws BufferUnderflowException if there is not a whole entry
+         *                                  following the current position
+         *                                  of hpsgData.
+         * @throws ParseException           if the provided data is malformed.
+         */
+        public HeapSegmentElement(HeapSegment hs)
+                throws BufferUnderflowException, ParseException {
+            set(hs);
+        }
+
+        /**
+         * Replace the element with the entry at the current position of
+         * hpsgData.
+         *
+         * @param hs The heap segment to pull the entry from.
+         * @return this object.
+         * @throws BufferUnderflowException if there is not a whole entry
+         *                                  following the current position of
+         *                                  hpsgData.
+         * @throws ParseException           if the provided data is malformed.
+         */
+        public HeapSegmentElement set(HeapSegment hs)
+                throws BufferUnderflowException, ParseException {
+
+            /* TODO: Maybe keep track of the virtual address of each element
+             *       so that they can be examined independently.
+             */
+            ByteBuffer data = hs.mUsageData;
+            int eState = data.get() & 0x000000ff;
+            int eLen = (data.get() & 0x000000ff) + 1;
+
+            while ((eState & PARTIAL_MASK) != 0) {
+
+                /* If the partial bit was set, the next byte should describe
+                 * the same object as the current one.
+                 */
+                int nextState = data.get() & 0x000000ff;
+                if ((nextState & ~PARTIAL_MASK) != (eState & ~PARTIAL_MASK)) {
+                    throw new ParseException("State mismatch", data.position());
+                }
+                eState = nextState;
+                eLen += (data.get() & 0x000000ff) + 1;
+            }
+
+            setSolidity(eState & 0x7);
+            setKind((eState >> 3) & 0x7);
+            setLength(eLen * hs.mAllocationUnitSize);
+
+            return this;
+        }
+
+        public int getSolidity() {
+            return mSolidity;
+        }
+
+        public void setSolidity(int solidity) {
+            this.mSolidity = solidity;
+        }
+
+        public int getKind() {
+            return mKind;
+        }
+
+        public void setKind(int kind) {
+            this.mKind = kind;
+        }
+
+        public int getLength() {
+            return mLength;
+        }
+
+        public void setLength(int length) {
+            this.mLength = length;
+        }
+
+        @Override
+        public int compareTo(HeapSegmentElement other) {
+            if (mLength != other.mLength) {
+                return mLength < other.mLength ? -1 : 1;
+            }
+            return 0;
+        }
+    }
+
+    //* The ID of the heap that this segment belongs to.
+    protected int mHeapId;
+
+    //* The size of an allocation unit, in bytes. (e.g., 8 bytes)
+    protected int mAllocationUnitSize;
+
+    //* The virtual address of the start of this segment.
+    protected long mStartAddress;
+
+    //* The offset of this pices from mStartAddress, in bytes.
+    protected int mOffset;
+
+    //* The number of allocation units described in this segment.
+    protected int mAllocationUnitCount;
+
+    //* The raw data that describes the contents of this segment.
+    protected ByteBuffer mUsageData;
+
+    //* mStartAddress is set to this value when the segment becomes invalid.
+    private static final long INVALID_START_ADDRESS = -1;
+
+    /**
+     * Create a new HeapSegment based on the raw contents
+     * of an HPSG chunk.
+     *
+     * @param hpsgData The raw data from an HPSG chunk.
+     * @throws BufferUnderflowException if hpsgData is too small
+     *                                  to hold the HPSG chunk header data.
+     */
+    public HeapSegment(ByteBuffer hpsgData) throws BufferUnderflowException {
+        /* Read the HPSG chunk header.
+         * These get*() calls may throw a BufferUnderflowException
+         * if the underlying data isn't big enough.
+         */
+        hpsgData.order(ByteOrder.BIG_ENDIAN);
+        mHeapId = hpsgData.getInt();
+        mAllocationUnitSize = hpsgData.get();
+        mStartAddress = hpsgData.getInt() & 0x00000000ffffffffL;
+        mOffset = hpsgData.getInt();
+        mAllocationUnitCount = hpsgData.getInt();
+
+        // Hold onto the remainder of the data.
+        mUsageData = hpsgData.slice();
+        mUsageData.order(ByteOrder.BIG_ENDIAN);   // doesn't actually matter
+
+        // Validate the data.
+//xxx do it
+//xxx make sure the number of elements matches mAllocationUnitCount.
+//xxx make sure the last element doesn't have P set
+    }
+
+    /**
+     * See if this segment still contains data, and has not been
+     * appended to another segment.
+     *
+     * @return true if this segment has not been appended to
+     *         another segment.
+     */
+    public boolean isValid() {
+        return mStartAddress != INVALID_START_ADDRESS;
+    }
+
+    /**
+     * See if <code>other</code> comes immediately after this segment.
+     *
+     * @param other The HeapSegment to check.
+     * @return true if <code>other</code> comes immediately after this
+     *         segment.
+     */
+    public boolean canAppend(HeapSegment other) {
+        return isValid() && other.isValid() && mHeapId == other.mHeapId &&
+                mAllocationUnitSize == other.mAllocationUnitSize &&
+                getEndAddress() == other.getStartAddress();
+    }
+
+    /**
+     * Append the contents of <code>other</code> to this segment
+     * if it describes the segment immediately after this one.
+     *
+     * @param other The segment to append to this segment, if possible.
+     *              If appended, <code>other</code> will be invalid
+     *              when this method returns.
+     * @return true if <code>other</code> was successfully appended to
+     *         this segment.
+     */
+    public boolean append(HeapSegment other) {
+        if (canAppend(other)) {
+            /* Preserve the position.  The mark is not preserved,
+             * but we don't use it anyway.
+             */
+            int pos = mUsageData.position();
+
+            // Guarantee that we have enough room for the new data.
+            if (mUsageData.capacity() - mUsageData.limit() <
+                    other.mUsageData.limit()) {
+                /* Grow more than necessary in case another append()
+                 * is about to happen.
+                 */
+                int newSize = mUsageData.limit() + other.mUsageData.limit();
+                ByteBuffer newData = ByteBuffer.allocate(newSize * 2);
+
+                mUsageData.rewind();
+                newData.put(mUsageData);
+                mUsageData = newData;
+            }
+
+            // Copy the data from the other segment and restore the position.
+            other.mUsageData.rewind();
+            mUsageData.put(other.mUsageData);
+            mUsageData.position(pos);
+
+            // Fix this segment's header to cover the new data.
+            mAllocationUnitCount += other.mAllocationUnitCount;
+
+            // Mark the other segment as invalid.
+            other.mStartAddress = INVALID_START_ADDRESS;
+            other.mUsageData = null;
+
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    public long getStartAddress() {
+        return mStartAddress + mOffset;
+    }
+
+    public int getLength() {
+        return mAllocationUnitSize * mAllocationUnitCount;
+    }
+
+    public long getEndAddress() {
+        return getStartAddress() + getLength();
+    }
+
+    public void rewindElements() {
+        if (mUsageData != null) {
+            mUsageData.rewind();
+        }
+    }
+
+    public HeapSegmentElement getNextElement(HeapSegmentElement reuse) {
+        try {
+            if (reuse != null) {
+                return reuse.set(this);
+            } else {
+                return new HeapSegmentElement(this);
+            }
+        } catch (BufferUnderflowException ex) {
+            /* Normal "end of buffer" situation.
+             */
+        } catch (ParseException ex) {
+            /* Malformed data.
+             */
+//TODO: we should catch this in the constructor
+        }
+        return null;
+    }
+
+    /*
+     * Method overrides for Comparable
+     */
+    @Override
+    public boolean equals(Object o) {
+        if (o instanceof HeapSegment) {
+            return compareTo((HeapSegment) o) == 0;
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return mHeapId * 31 +
+                mAllocationUnitSize * 31 +
+                (int) mStartAddress * 31 +
+                mOffset * 31 +
+                mAllocationUnitCount * 31 +
+                mUsageData.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder str = new StringBuilder();
+
+        str.append("HeapSegment { heap ").append(mHeapId)
+                .append(", start 0x")
+                .append(Integer.toHexString((int) getStartAddress()))
+                .append(", length ").append(getLength())
+                .append(" }");
+
+        return str.toString();
+    }
+
+    @Override
+    public int compareTo(HeapSegment other) {
+        if (mHeapId != other.mHeapId) {
+            return mHeapId < other.mHeapId ? -1 : 1;
+        }
+        if (getStartAddress() != other.getStartAddress()) {
+            return getStartAddress() < other.getStartAddress() ? -1 : 1;
+        }
+
+        /* If two segments have the same start address, the rest of
+         * the fields should be equal.  Go through the motions, though.
+         * Note that we re-check the components of getStartAddress()
+         * (mStartAddress and mOffset) to make sure that all fields in
+         * an equal segment are equal.
+         */
+
+        if (mAllocationUnitSize != other.mAllocationUnitSize) {
+            return mAllocationUnitSize < other.mAllocationUnitSize ? -1 : 1;
+        }
+        if (mStartAddress != other.mStartAddress) {
+            return mStartAddress < other.mStartAddress ? -1 : 1;
+        }
+        if (mOffset != other.mOffset) {
+            return mOffset < other.mOffset ? -1 : 1;
+        }
+        if (mAllocationUnitCount != other.mAllocationUnitCount) {
+            return mAllocationUnitCount < other.mAllocationUnitCount ? -1 : 1;
+        }
+        if (mUsageData != other.mUsageData) {
+            return mUsageData.compareTo(other.mUsageData);
+        }
+        return 0;
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/IDevice.java b/ddmlib/src/main/java/com/android/ddmlib/IDevice.java
new file mode 100644
index 0000000..a9ebaad
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/IDevice.java
@@ -0,0 +1,527 @@
+/*
+ * Copyright (C) 2008 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.ddmlib;
+
+import com.android.ddmlib.log.LogReceiver;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ *  A Device. It can be a physical device or an emulator.
+ */
+public interface IDevice {
+
+    public static final String PROP_BUILD_VERSION = "ro.build.version.release";
+    public static final String PROP_BUILD_API_LEVEL = "ro.build.version.sdk";
+    public static final String PROP_BUILD_CODENAME = "ro.build.version.codename";
+    public static final String PROP_DEVICE_MODEL = "ro.product.model";
+    public static final String PROP_DEVICE_MANUFACTURER = "ro.product.manufacturer";
+
+    public static final String PROP_DEBUGGABLE = "ro.debuggable";
+
+    /** Serial number of the first connected emulator. */
+    public static final String FIRST_EMULATOR_SN = "emulator-5554"; //$NON-NLS-1$
+    /** Device change bit mask: {@link DeviceState} change. */
+    public static final int CHANGE_STATE = 0x0001;
+    /** Device change bit mask: {@link Client} list change. */
+    public static final int CHANGE_CLIENT_LIST = 0x0002;
+    /** Device change bit mask: build info change. */
+    public static final int CHANGE_BUILD_INFO = 0x0004;
+
+    /** @deprecated Use {@link #PROP_BUILD_API_LEVEL}. */
+    @Deprecated
+    public static final String PROP_BUILD_VERSION_NUMBER = PROP_BUILD_API_LEVEL;
+
+    public static final String MNT_EXTERNAL_STORAGE = "EXTERNAL_STORAGE"; //$NON-NLS-1$
+    public static final String MNT_ROOT = "ANDROID_ROOT"; //$NON-NLS-1$
+    public static final String MNT_DATA = "ANDROID_DATA"; //$NON-NLS-1$
+
+    /**
+     * The state of a device.
+     */
+    public static enum DeviceState {
+        BOOTLOADER("bootloader"), //$NON-NLS-1$
+        OFFLINE("offline"), //$NON-NLS-1$
+        ONLINE("device"), //$NON-NLS-1$
+        RECOVERY("recovery"); //$NON-NLS-1$
+
+        private String mState;
+
+        DeviceState(String state) {
+            mState = state;
+        }
+
+        /**
+         * Returns a {@link DeviceState} from the string returned by <code>adb devices</code>.
+         *
+         * @param state the device state.
+         * @return a {@link DeviceState} object or <code>null</code> if the state is unknown.
+         */
+        public static DeviceState getState(String state) {
+            for (DeviceState deviceState : values()) {
+                if (deviceState.mState.equals(state)) {
+                    return deviceState;
+                }
+            }
+            return null;
+        }
+    }
+
+    /**
+     * Namespace of a Unix Domain Socket created on the device.
+     */
+    public static enum DeviceUnixSocketNamespace {
+        ABSTRACT("localabstract"),      //$NON-NLS-1$
+        FILESYSTEM("localfilesystem"),  //$NON-NLS-1$
+        RESERVED("localreserved");      //$NON-NLS-1$
+
+        private String mType;
+
+        private DeviceUnixSocketNamespace(String type) {
+            mType = type;
+        }
+
+        String getType() {
+            return mType;
+        }
+    }
+
+    /**
+     * Returns the serial number of the device.
+     */
+    public String getSerialNumber();
+
+    /**
+     * Returns the name of the AVD the emulator is running.
+     * <p/>This is only valid if {@link #isEmulator()} returns true.
+     * <p/>If the emulator is not running any AVD (for instance it's running from an Android source
+     * tree build), this method will return "<code><build></code>".
+     *
+     * @return the name of the AVD or <code>null</code> if there isn't any.
+     */
+    public String getAvdName();
+
+    /**
+     * Returns a (humanized) name for this device. Typically this is the AVD name for AVD's, and
+     * a combination of the manufacturer name, model name & serial number for devices.
+     */
+    public String getName();
+
+    /**
+     * Returns the state of the device.
+     */
+    public DeviceState getState();
+
+    /**
+     * Returns the device properties. It contains the whole output of 'getprop'
+     */
+    public Map<String, String> getProperties();
+
+    /**
+     * Returns the number of property for this device.
+     */
+    public int getPropertyCount();
+
+    /**
+     * Returns the cached property value.
+     *
+     * @param name the name of the value to return.
+     * @return the value or <code>null</code> if the property does not exist or has not yet been
+     * cached.
+     */
+    public String getProperty(String name);
+
+    /**
+     * Returns <code>true></code> if properties have been cached
+     */
+    public boolean arePropertiesSet();
+
+    /**
+     * A variant of {@link #getProperty(String)} that will attempt to retrieve the given
+     * property from device directly, without using cache.
+     *
+     * @param name the name of the value to return.
+     * @return the value or <code>null</code> if the property does not exist
+     * @throws TimeoutException in case of timeout on the connection.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     * @throws ShellCommandUnresponsiveException in case the shell command doesn't send output for a
+     *             given time.
+     * @throws IOException in case of I/O error on the connection.
+     */
+    public String getPropertySync(String name) throws TimeoutException,
+            AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException;
+
+    /**
+     * A combination of {@link #getProperty(String)} and {@link #getPropertySync(String)} that
+     * will attempt to retrieve the property from cache if available, and if not, will query the
+     * device directly.
+     *
+     * @param name the name of the value to return.
+     * @return the value or <code>null</code> if the property does not exist
+     * @throws TimeoutException in case of timeout on the connection.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     * @throws ShellCommandUnresponsiveException in case the shell command doesn't send output for a
+     *             given time.
+     * @throws IOException in case of I/O error on the connection.
+     */
+    public String getPropertyCacheOrSync(String name) throws TimeoutException,
+            AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException;
+
+    /**
+     * Returns a mount point.
+     *
+     * @param name the name of the mount point to return
+     *
+     * @see #MNT_EXTERNAL_STORAGE
+     * @see #MNT_ROOT
+     * @see #MNT_DATA
+     */
+    public String getMountPoint(String name);
+
+    /**
+     * Returns if the device is ready.
+     *
+     * @return <code>true</code> if {@link #getState()} returns {@link DeviceState#ONLINE}.
+     */
+    public boolean isOnline();
+
+    /**
+     * Returns <code>true</code> if the device is an emulator.
+     */
+    public boolean isEmulator();
+
+    /**
+     * Returns if the device is offline.
+     *
+     * @return <code>true</code> if {@link #getState()} returns {@link DeviceState#OFFLINE}.
+     */
+    public boolean isOffline();
+
+    /**
+     * Returns if the device is in bootloader mode.
+     *
+     * @return <code>true</code> if {@link #getState()} returns {@link DeviceState#BOOTLOADER}.
+     */
+    public boolean isBootLoader();
+
+    /**
+     * Returns whether the {@link Device} has {@link Client}s.
+     */
+    public boolean hasClients();
+
+    /**
+     * Returns the array of clients.
+     */
+    public Client[] getClients();
+
+    /**
+     * Returns a {@link Client} by its application name.
+     *
+     * @param applicationName the name of the application
+     * @return the <code>Client</code> object or <code>null</code> if no match was found.
+     */
+    public Client getClient(String applicationName);
+
+    /**
+     * Returns a {@link SyncService} object to push / pull files to and from the device.
+     *
+     * @return <code>null</code> if the SyncService couldn't be created. This can happen if adb
+     *            refuse to open the connection because the {@link IDevice} is invalid
+     *            (or got disconnected).
+     * @throws TimeoutException in case of timeout on the connection.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     * @throws IOException if the connection with adb failed.
+     */
+    public SyncService getSyncService()
+            throws TimeoutException, AdbCommandRejectedException, IOException;
+
+    /**
+     * Returns a {@link FileListingService} for this device.
+     */
+    public FileListingService getFileListingService();
+
+    /**
+     * Takes a screen shot of the device and returns it as a {@link RawImage}.
+     *
+     * @return the screenshot as a <code>RawImage</code> or <code>null</code> if something
+     *            went wrong.
+     * @throws TimeoutException in case of timeout on the connection.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     * @throws IOException in case of I/O error on the connection.
+     */
+    public RawImage getScreenshot() throws TimeoutException, AdbCommandRejectedException,
+            IOException;
+
+    /**
+     * Executes a shell command on the device, and sends the result to a <var>receiver</var>
+     * <p/>This is similar to calling
+     * <code>executeShellCommand(command, receiver, DdmPreferences.getTimeOut())</code>.
+     *
+     * @param command the shell command to execute
+     * @param receiver the {@link IShellOutputReceiver} that will receives the output of the shell
+     *            command
+     * @throws TimeoutException in case of timeout on the connection.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     * @throws ShellCommandUnresponsiveException in case the shell command doesn't send output
+     *            for a given time.
+     * @throws IOException in case of I/O error on the connection.
+     *
+     * @see #executeShellCommand(String, IShellOutputReceiver, int)
+     * @see DdmPreferences#getTimeOut()
+     */
+    public void executeShellCommand(String command, IShellOutputReceiver receiver)
+            throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException,
+            IOException;
+
+    /**
+     * Executes a shell command on the device, and sends the result to a <var>receiver</var>.
+     * <p/><var>maxTimeToOutputResponse</var> is used as a maximum waiting time when expecting the
+     * command output from the device.<br>
+     * At any time, if the shell command does not output anything for a period longer than
+     * <var>maxTimeToOutputResponse</var>, then the method will throw
+     * {@link ShellCommandUnresponsiveException}.
+     * <p/>For commands like log output, a <var>maxTimeToOutputResponse</var> value of 0, meaning
+     * that the method will never throw and will block until the receiver's
+     * {@link IShellOutputReceiver#isCancelled()} returns <code>true</code>, should be
+     * used.
+     *
+     * @param command the shell command to execute
+     * @param receiver the {@link IShellOutputReceiver} that will receives the output of the shell
+     *            command
+     * @param maxTimeToOutputResponse the maximum amount of time during which the command is allowed
+     *            to not output any response. A value of 0 means the method will wait forever
+     *            (until the <var>receiver</var> cancels the execution) for command output and
+     *            never throw.
+     * @throws TimeoutException in case of timeout on the connection when sending the command.
+     * @throws AdbCommandRejectedException if adb rejects the command.
+     * @throws ShellCommandUnresponsiveException in case the shell command doesn't send any output
+     *            for a period longer than <var>maxTimeToOutputResponse</var>.
+     * @throws IOException in case of I/O error on the connection.
+     *
+     * @see DdmPreferences#getTimeOut()
+     */
+    public void executeShellCommand(String command, IShellOutputReceiver receiver,
+            int maxTimeToOutputResponse)
+            throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException,
+            IOException;
+
+    /**
+     * Runs the event log service and outputs the event log to the {@link LogReceiver}.
+     * <p/>This call is blocking until {@link LogReceiver#isCancelled()} returns true.
+     * @param receiver the receiver to receive the event log entries.
+     * @throws TimeoutException in case of timeout on the connection. This can only be thrown if the
+     * timeout happens during setup. Once logs start being received, no timeout will occur as it's
+     * not possible to detect a difference between no log and timeout.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     * @throws IOException in case of I/O error on the connection.
+     */
+    public void runEventLogService(LogReceiver receiver)
+            throws TimeoutException, AdbCommandRejectedException, IOException;
+
+    /**
+     * Runs the log service for the given log and outputs the log to the {@link LogReceiver}.
+     * <p/>This call is blocking until {@link LogReceiver#isCancelled()} returns true.
+     *
+     * @param logname the logname of the log to read from.
+     * @param receiver the receiver to receive the event log entries.
+     * @throws TimeoutException in case of timeout on the connection. This can only be thrown if the
+     *            timeout happens during setup. Once logs start being received, no timeout will
+     *            occur as it's not possible to detect a difference between no log and timeout.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     * @throws IOException in case of I/O error on the connection.
+     */
+    public void runLogService(String logname, LogReceiver receiver)
+            throws TimeoutException, AdbCommandRejectedException, IOException;
+
+    /**
+     * Creates a port forwarding between a local and a remote port.
+     *
+     * @param localPort the local port to forward
+     * @param remotePort the remote port.
+     * @throws TimeoutException in case of timeout on the connection.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     * @throws IOException in case of I/O error on the connection.
+     */
+    public void createForward(int localPort, int remotePort)
+            throws TimeoutException, AdbCommandRejectedException, IOException;
+
+    /**
+     * Creates a port forwarding between a local TCP port and a remote Unix Domain Socket.
+     *
+     * @param localPort the local port to forward
+     * @param remoteSocketName name of the unix domain socket created on the device
+     * @param namespace namespace in which the unix domain socket was created
+     * @throws TimeoutException in case of timeout on the connection.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     * @throws IOException in case of I/O error on the connection.
+     */
+    public void createForward(int localPort, String remoteSocketName,
+            DeviceUnixSocketNamespace namespace)
+            throws TimeoutException, AdbCommandRejectedException, IOException;
+
+    /**
+     * Removes a port forwarding between a local and a remote port.
+     *
+     * @param localPort the local port to forward
+     * @param remotePort the remote port.
+     * @throws TimeoutException in case of timeout on the connection.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     * @throws IOException in case of I/O error on the connection.
+     */
+    public void removeForward(int localPort, int remotePort)
+            throws TimeoutException, AdbCommandRejectedException, IOException;
+
+    /**
+     * Removes an existing port forwarding between a local and a remote port.
+     *
+     * @param localPort the local port to forward
+     * @param remoteSocketName the remote unix domain socket name.
+     * @param namespace namespace in which the unix domain socket was created
+     * @throws TimeoutException in case of timeout on the connection.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     * @throws IOException in case of I/O error on the connection.
+     */
+    public void removeForward(int localPort, String remoteSocketName,
+            DeviceUnixSocketNamespace namespace)
+            throws TimeoutException, AdbCommandRejectedException, IOException;
+
+    /**
+     * Returns the name of the client by pid or <code>null</code> if pid is unknown
+     * @param pid the pid of the client.
+     */
+    public String getClientName(int pid);
+
+    /**
+     * Push a single file.
+     * @param local the local filepath.
+     * @param remote The remote filepath.
+     *
+     * @throws IOException in case of I/O error on the connection.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     * @throws TimeoutException in case of a timeout reading responses from the device.
+     * @throws SyncException if file could not be pushed
+     */
+    public void pushFile(String local, String remote)
+            throws IOException, AdbCommandRejectedException, TimeoutException, SyncException;
+
+    /**
+     * Pulls a single file.
+     *
+     * @param remote the full path to the remote file
+     * @param local The local destination.
+     *
+     * @throws IOException in case of an IO exception.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     * @throws TimeoutException in case of a timeout reading responses from the device.
+     * @throws SyncException in case of a sync exception.
+     */
+    public void pullFile(String remote, String local)
+            throws IOException, AdbCommandRejectedException, TimeoutException, SyncException;
+
+    /**
+     * Installs an Android application on device. This is a helper method that combines the
+     * syncPackageToDevice, installRemotePackage, and removePackage steps
+     *
+     * @param packageFilePath the absolute file system path to file on local host to install
+     * @param reinstall set to <code>true</code> if re-install of app should be performed
+     * @param extraArgs optional extra arguments to pass. See 'adb shell pm install --help' for
+     *            available options.
+     * @return a {@link String} with an error code, or <code>null</code> if success.
+     * @throws InstallException if the installation fails.
+     */
+    public String installPackage(String packageFilePath, boolean reinstall, String... extraArgs)
+            throws InstallException;
+
+    /**
+     * Pushes a file to device
+     *
+     * @param localFilePath the absolute path to file on local host
+     * @return {@link String} destination path on device for file
+     * @throws TimeoutException in case of timeout on the connection.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     * @throws IOException in case of I/O error on the connection.
+     * @throws SyncException if an error happens during the push of the package on the device.
+     */
+    public String syncPackageToDevice(String localFilePath)
+            throws TimeoutException, AdbCommandRejectedException, IOException, SyncException;
+
+    /**
+     * Installs the application package that was pushed to a temporary location on the device.
+     *
+     * @param remoteFilePath absolute file path to package file on device
+     * @param reinstall set to <code>true</code> if re-install of app should be performed
+     * @param extraArgs optional extra arguments to pass. See 'adb shell pm install --help' for
+     *            available options.
+     * @throws InstallException if the installation fails.
+     */
+    public String installRemotePackage(String remoteFilePath, boolean reinstall,
+            String... extraArgs) throws InstallException;
+
+    /**
+     * Removes a file from device.
+     *
+     * @param remoteFilePath path on device of file to remove
+     * @throws InstallException if the installation fails.
+     */
+    public void removeRemotePackage(String remoteFilePath) throws InstallException;
+
+    /**
+     * Uninstalls an package from the device.
+     *
+     * @param packageName the Android application package name to uninstall
+     * @return a {@link String} with an error code, or <code>null</code> if success.
+     * @throws InstallException if the uninstallation fails.
+     */
+    public String uninstallPackage(String packageName) throws InstallException;
+
+    /**
+     * Reboot the device.
+     *
+     * @param into the bootloader name to reboot into, or null to just reboot the device.
+     * @throws TimeoutException in case of timeout on the connection.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     * @throws IOException
+     */
+    public void reboot(String into)
+            throws TimeoutException, AdbCommandRejectedException, IOException;
+
+    /**
+     * Return the device's battery level, from 0 to 100 percent.
+     * <p/>
+     * The battery level may be cached. Only queries the device for its
+     * battery level if 5 minutes have expired since the last successful query.
+     *
+     * @return the battery level or <code>null</code> if it could not be retrieved
+     */
+    public Integer getBatteryLevel() throws TimeoutException,
+            AdbCommandRejectedException, IOException, ShellCommandUnresponsiveException;
+
+    /**
+     * Return the device's battery level, from 0 to 100 percent.
+     * <p/>
+     * The battery level may be cached. Only queries the device for its
+     * battery level if <code>freshnessMs</code> ms have expired since the last successful query.
+     *
+     * @param freshnessMs
+     * @return the battery level or <code>null</code> if it could not be retrieved
+     * @throws ShellCommandUnresponsiveException
+     */
+    public Integer getBatteryLevel(long freshnessMs) throws TimeoutException,
+            AdbCommandRejectedException, IOException, ShellCommandUnresponsiveException;
+
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/IShellOutputReceiver.java b/ddmlib/src/main/java/com/android/ddmlib/IShellOutputReceiver.java
new file mode 100644
index 0000000..6d9d1d7
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/IShellOutputReceiver.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+/**
+ * Classes which implement this interface provide methods that deal with out from a remote shell
+ * command on a device/emulator.
+ */
+public interface IShellOutputReceiver {
+    /**
+     * Called every time some new data is available.
+     * @param data The new data.
+     * @param offset The offset at which the new data starts.
+     * @param length The length of the new data.
+     */
+    public void addOutput(byte[] data, int offset, int length);
+
+    /**
+     * Called at the end of the process execution (unless the process was
+     * canceled). This allows the receiver to terminate and flush whatever
+     * data was not yet processed.
+     */
+    public void flush();
+
+    /**
+     * Cancel method to stop the execution of the remote shell command.
+     * @return true to cancel the execution of the command.
+     */
+    public boolean isCancelled();
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/IStackTraceInfo.java b/ddmlib/src/main/java/com/android/ddmlib/IStackTraceInfo.java
new file mode 100644
index 0000000..3b9d730
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/IStackTraceInfo.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+/**
+ * Classes which implement this interface provide a method that returns a stack trace.
+ */
+public interface IStackTraceInfo {
+
+    /**
+     * Returns the stack trace. This can be <code>null</code>.
+     */
+    public StackTraceElement[] getStackTrace();
+
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/InstallException.java b/ddmlib/src/main/java/com/android/ddmlib/InstallException.java
new file mode 100644
index 0000000..7aa718f
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/InstallException.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2010 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.ddmlib;
+
+/**
+ * Thrown if installation or uninstallation of application fails.
+ */
+public class InstallException extends CanceledException {
+    private static final long serialVersionUID = 1L;
+
+    public InstallException(Throwable cause) {
+        super(cause.getMessage(), cause);
+    }
+
+    public InstallException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    /**
+     * Returns true if the installation was canceled by user input. This can typically only
+     * happen in the sync phase.
+     */
+    @Override
+    public boolean wasCanceled() {
+        Throwable cause = getCause();
+        return cause instanceof SyncException && ((SyncException)cause).wasCanceled();
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/JdwpPacket.java b/ddmlib/src/main/java/com/android/ddmlib/JdwpPacket.java
new file mode 100644
index 0000000..23b0249
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/JdwpPacket.java
@@ -0,0 +1,371 @@
+/* //device/tools/ddms/libs/ddmlib/src/com/android/ddmlib/JdwpPacket.java
+**
+** Copyright 2007, 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.ddmlib;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.channels.SocketChannel;
+
+/**
+ * A JDWP packet, sitting at the start of a ByteBuffer somewhere.
+ *
+ * This allows us to wrap a "pointer" to the data with the results of
+ * decoding the packet.
+ *
+ * None of the operations here are synchronized.  If multiple threads will
+ * be accessing the same ByteBuffers, external sync will be required.
+ *
+ * Use the constructor to create an empty packet, or "findPacket()" to
+ * wrap a JdwpPacket around existing data.
+ */
+final class JdwpPacket {
+    // header len
+    public static final int JDWP_HEADER_LEN = 11;
+
+    // results from findHandshake
+    public static final int HANDSHAKE_GOOD = 1;
+    public static final int HANDSHAKE_NOTYET = 2;
+    public static final int HANDSHAKE_BAD = 3;
+
+    // our cmdSet/cmd
+    private static final int DDMS_CMD_SET = 0xc7;       // 'G' + 128
+    private static final int DDMS_CMD = 0x01;
+
+    // "flags" field
+    private static final int REPLY_PACKET = 0x80;
+
+    // this is sent and expected at the start of a JDWP connection
+    private static final byte[] mHandshake = {
+        'J', 'D', 'W', 'P', '-', 'H', 'a', 'n', 'd', 's', 'h', 'a', 'k', 'e'
+    };
+
+    public static final int HANDSHAKE_LEN = mHandshake.length;
+
+    private ByteBuffer mBuffer;
+    private int mLength, mId, mFlags, mCmdSet, mCmd, mErrCode;
+    private boolean mIsNew;
+
+    private static int sSerialId = 0x40000000;
+
+
+    /**
+     * Create a new, empty packet, in "buf".
+     */
+    JdwpPacket(ByteBuffer buf) {
+        mBuffer = buf;
+        mIsNew = true;
+    }
+
+    /**
+     * Finish a packet created with newPacket().
+     *
+     * This always creates a command packet, with the next serial number
+     * in sequence.
+     *
+     * We have to take "payloadLength" as an argument because we can't
+     * see the position in the "slice" returned by getPayload().  We could
+     * fish it out of the chunk header, but it's legal for there to be
+     * more than one chunk in a JDWP packet.
+     *
+     * On exit, "position" points to the end of the data.
+     */
+    void finishPacket(int payloadLength) {
+        assert mIsNew;
+
+        ByteOrder oldOrder = mBuffer.order();
+        mBuffer.order(ChunkHandler.CHUNK_ORDER);
+
+        mLength = JDWP_HEADER_LEN + payloadLength;
+        mId = getNextSerial();
+        mFlags = 0;
+        mCmdSet = DDMS_CMD_SET;
+        mCmd = DDMS_CMD;
+
+        mBuffer.putInt(0x00, mLength);
+        mBuffer.putInt(0x04, mId);
+        mBuffer.put(0x08, (byte) mFlags);
+        mBuffer.put(0x09, (byte) mCmdSet);
+        mBuffer.put(0x0a, (byte) mCmd);
+
+        mBuffer.order(oldOrder);
+        mBuffer.position(mLength);
+    }
+
+    /**
+     * Get the next serial number.  This creates a unique serial number
+     * across all connections, not just for the current connection.  This
+     * is a useful property when debugging, but isn't necessary.
+     *
+     * We can't synchronize on an int, so we use a sync method.
+     */
+    private static synchronized int getNextSerial() {
+        return sSerialId++;
+    }
+
+    /**
+     * Return a slice of the byte buffer, positioned past the JDWP header
+     * to the start of the chunk header.  The buffer's limit will be set
+     * to the size of the payload if the size is known; if this is a
+     * packet under construction the limit will be set to the end of the
+     * buffer.
+     *
+     * Doesn't examine the packet at all -- works on empty buffers.
+     */
+    ByteBuffer getPayload() {
+        ByteBuffer buf;
+        int oldPosn = mBuffer.position();
+
+        mBuffer.position(JDWP_HEADER_LEN);
+        buf = mBuffer.slice();     // goes from position to limit
+        mBuffer.position(oldPosn);
+
+        if (mLength > 0)
+            buf.limit(mLength - JDWP_HEADER_LEN);
+        else
+            assert mIsNew;
+        buf.order(ChunkHandler.CHUNK_ORDER);
+        return buf;
+    }
+
+    /**
+     * Returns "true" if this JDWP packet has a JDWP command type.
+     *
+     * This never returns "true" for reply packets.
+     */
+    boolean isDdmPacket() {
+        return (mFlags & REPLY_PACKET) == 0 &&
+               mCmdSet == DDMS_CMD_SET &&
+               mCmd == DDMS_CMD;
+    }
+
+    /**
+     * Returns "true" if this JDWP packet is tagged as a reply.
+     */
+    boolean isReply() {
+        return (mFlags & REPLY_PACKET) != 0;
+    }
+
+    /**
+     * Returns "true" if this JDWP packet is a reply with a nonzero
+     * error code.
+     */
+    boolean isError() {
+        return isReply() && mErrCode != 0;
+    }
+
+    /**
+     * Returns "true" if this JDWP packet has no data.
+     */
+    boolean isEmpty() {
+        return (mLength == JDWP_HEADER_LEN);
+    }
+
+    /**
+     * Return the packet's ID.  For a reply packet, this allows us to
+     * match the reply with the original request.
+     */
+    int getId() {
+        return mId;
+    }
+
+    /**
+     * Return the length of a packet.  This includes the header, so an
+     * empty packet is 11 bytes long.
+     */
+    int getLength() {
+        return mLength;
+    }
+
+    /**
+     * Write our packet to "chan".  Consumes the packet as part of the
+     * write.
+     *
+     * The JDWP packet starts at offset 0 and ends at mBuffer.position().
+     */
+    void writeAndConsume(SocketChannel chan) throws IOException {
+        int oldLimit;
+
+        //Log.i("ddms", "writeAndConsume: pos=" + mBuffer.position()
+        //    + ", limit=" + mBuffer.limit());
+
+        assert mLength > 0;
+
+        mBuffer.flip();         // limit<-posn, posn<-0
+        oldLimit = mBuffer.limit();
+        mBuffer.limit(mLength);
+        while (mBuffer.position() != mBuffer.limit()) {
+            chan.write(mBuffer);
+        }
+        // position should now be at end of packet
+        assert mBuffer.position() == mLength;
+
+        mBuffer.limit(oldLimit);
+        mBuffer.compact();      // shift posn...limit, posn<-pending data
+
+        //Log.i("ddms", "               : pos=" + mBuffer.position()
+        //    + ", limit=" + mBuffer.limit());
+    }
+
+    /**
+     * "Move" the packet data out of the buffer we're sitting on and into
+     * buf at the current position.
+     */
+    void movePacket(ByteBuffer buf) {
+        Log.v("ddms", "moving " + mLength + " bytes");
+        int oldPosn = mBuffer.position();
+
+        mBuffer.position(0);
+        mBuffer.limit(mLength);
+        buf.put(mBuffer);
+        mBuffer.position(mLength);
+        mBuffer.limit(oldPosn);
+        mBuffer.compact();      // shift posn...limit, posn<-pending data
+    }
+
+    /**
+     * Consume the JDWP packet.
+     *
+     * On entry and exit, "position" is the #of bytes in the buffer.
+     */
+    void consume()
+    {
+        //Log.d("ddms", "consuming " + mLength + " bytes");
+        //Log.d("ddms", "  posn=" + mBuffer.position()
+        //    + ", limit=" + mBuffer.limit());
+
+        /*
+         * The "flip" call sets "limit" equal to the position (usually the
+         * end of data) and "position" equal to zero.
+         *
+         * compact() copies everything from "position" and "limit" to the
+         * start of the buffer, sets "position" to the end of data, and
+         * sets "limit" to the capacity.
+         *
+         * On entry, "position" is set to the amount of data in the buffer
+         * and "limit" is set to the capacity.  We want to call flip()
+         * so that position..limit spans our data, advance "position" past
+         * the current packet, then compact.
+         */
+        mBuffer.flip();         // limit<-posn, posn<-0
+        mBuffer.position(mLength);
+        mBuffer.compact();      // shift posn...limit, posn<-pending data
+        mLength = 0;
+        //Log.d("ddms", "  after compact, posn=" + mBuffer.position()
+        //    + ", limit=" + mBuffer.limit());
+    }
+
+    /**
+     * Find the JDWP packet at the start of "buf".  The start is known,
+     * but the length has to be parsed out.
+     *
+     * On entry, the packet data in "buf" must start at offset 0 and end
+     * at "position".  "limit" should be set to the buffer capacity.  This
+     * method does not alter "buf"s attributes.
+     *
+     * Returns a new JdwpPacket if a full one is found in the buffer.  If
+     * not, returns null.  Throws an exception if the data doesn't look like
+     * a valid JDWP packet.
+     */
+    static JdwpPacket findPacket(ByteBuffer buf) {
+        int count = buf.position();
+        int length, id, flags, cmdSet, cmd;
+
+        if (count < JDWP_HEADER_LEN)
+            return null;
+
+        ByteOrder oldOrder = buf.order();
+        buf.order(ChunkHandler.CHUNK_ORDER);
+
+        length = buf.getInt(0x00);
+        id = buf.getInt(0x04);
+        flags = buf.get(0x08) & 0xff;
+        cmdSet = buf.get(0x09) & 0xff;
+        cmd = buf.get(0x0a) & 0xff;
+
+        buf.order(oldOrder);
+
+        if (length < JDWP_HEADER_LEN)
+            throw new BadPacketException();
+        if (count < length)
+            return null;
+
+        JdwpPacket pkt = new JdwpPacket(buf);
+        //pkt.mBuffer = buf;
+        pkt.mLength = length;
+        pkt.mId = id;
+        pkt.mFlags = flags;
+
+        if ((flags & REPLY_PACKET) == 0) {
+            pkt.mCmdSet = cmdSet;
+            pkt.mCmd = cmd;
+            pkt.mErrCode = -1;
+        } else {
+            pkt.mCmdSet = -1;
+            pkt.mCmd = -1;
+            pkt.mErrCode = cmdSet | (cmd << 8);
+        }
+
+        return pkt;
+    }
+
+    /**
+     * Like findPacket(), but when we're expecting the JDWP handshake.
+     *
+     * Returns one of:
+     *   HANDSHAKE_GOOD   - found handshake, looks good
+     *   HANDSHAKE_BAD    - found enough data, but it's wrong
+     *   HANDSHAKE_NOTYET - not enough data has been read yet
+     */
+    static int findHandshake(ByteBuffer buf) {
+        int count = buf.position();
+        int i;
+
+        if (count < mHandshake.length)
+            return HANDSHAKE_NOTYET;
+
+        for (i = mHandshake.length -1; i >= 0; --i) {
+            if (buf.get(i) != mHandshake[i])
+                return HANDSHAKE_BAD;
+        }
+
+        return HANDSHAKE_GOOD;
+    }
+
+    /**
+     * Remove the handshake string from the buffer.
+     *
+     * On entry and exit, "position" is the #of bytes in the buffer.
+     */
+    static void consumeHandshake(ByteBuffer buf) {
+        // in theory, nothing else can have arrived, so this is overkill
+        buf.flip();         // limit<-posn, posn<-0
+        buf.position(mHandshake.length);
+        buf.compact();      // shift posn...limit, posn<-pending data
+    }
+
+    /**
+     * Copy the handshake string into the output buffer.
+     *
+     * On exit, "buf"s position will be advanced.
+     */
+    static void putHandshake(ByteBuffer buf) {
+        buf.put(mHandshake);
+    }
+}
+
diff --git a/ddmlib/src/main/java/com/android/ddmlib/Log.java b/ddmlib/src/main/java/com/android/ddmlib/Log.java
new file mode 100644
index 0000000..67ef50a
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/Log.java
@@ -0,0 +1,359 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * Log class that mirrors the API in main Android sources.
+ * <p/>Default behavior outputs the log to {@link System#out}. Use
+ * {@link #setLogOutput(com.android.ddmlib.Log.ILogOutput)} to redirect the log somewhere else.
+ */
+public final class Log {
+
+    /**
+     * Log Level enum.
+     */
+    public enum LogLevel {
+        VERBOSE(2, "verbose", 'V'), //$NON-NLS-1$
+        DEBUG(3, "debug", 'D'), //$NON-NLS-1$
+        INFO(4, "info", 'I'), //$NON-NLS-1$
+        WARN(5, "warn", 'W'), //$NON-NLS-1$
+        ERROR(6, "error", 'E'), //$NON-NLS-1$
+        ASSERT(7, "assert", 'A'); //$NON-NLS-1$
+
+        private int mPriorityLevel;
+        private String mStringValue;
+        private char mPriorityLetter;
+
+        LogLevel(int intPriority, String stringValue, char priorityChar) {
+            mPriorityLevel = intPriority;
+            mStringValue = stringValue;
+            mPriorityLetter = priorityChar;
+        }
+
+        public static LogLevel getByString(String value) {
+            for (LogLevel mode : values()) {
+                if (mode.mStringValue.equals(value)) {
+                    return mode;
+                }
+            }
+
+            return null;
+        }
+        
+        /**
+         * Returns the {@link LogLevel} enum matching the specified letter.
+         * @param letter the letter matching a <code>LogLevel</code> enum
+         * @return a <code>LogLevel</code> object or <code>null</code> if no match were found.
+         */
+        public static LogLevel getByLetter(char letter) {
+            for (LogLevel mode : values()) {
+                if (mode.mPriorityLetter == letter) {
+                    return mode;
+                }
+            }
+
+            return null;
+        }
+
+        /**
+         * Returns the {@link LogLevel} enum matching the specified letter.
+         * <p/>
+         * The letter is passed as a {@link String} argument, but only the first character
+         * is used. 
+         * @param letter the letter matching a <code>LogLevel</code> enum
+         * @return a <code>LogLevel</code> object or <code>null</code> if no match were found.
+         */
+        public static LogLevel getByLetterString(String letter) {
+            if (!letter.isEmpty()) {
+                return getByLetter(letter.charAt(0));
+            }
+
+            return null;
+        }
+
+        /**
+         * Returns the letter identifying the priority of the {@link LogLevel}.
+         */
+        public char getPriorityLetter() {
+            return mPriorityLetter;
+        }
+
+        /**
+         * Returns the numerical value of the priority.
+         */
+        public int getPriority() {
+            return mPriorityLevel;
+        }
+
+        /**
+         * Returns a non translated string representing the LogLevel.
+         */
+        public String getStringValue() {
+            return mStringValue;
+        }
+    }
+    
+    /**
+     * Classes which implement this interface provides methods that deal with outputting log
+     * messages.
+     */
+    public interface ILogOutput {
+        /**
+         * Sent when a log message needs to be printed.
+         * @param logLevel The {@link LogLevel} enum representing the priority of the message.
+         * @param tag The tag associated with the message.
+         * @param message The message to display.
+         */
+        public void printLog(LogLevel logLevel, String tag, String message);
+
+        /**
+         * Sent when a log message needs to be printed, and, if possible, displayed to the user
+         * in a dialog box.
+         * @param logLevel The {@link LogLevel} enum representing the priority of the message.
+         * @param tag The tag associated with the message.
+         * @param message The message to display.
+         */
+        public void printAndPromptLog(LogLevel logLevel, String tag, String message);
+    }
+
+    private static LogLevel sLevel = DdmPreferences.getLogLevel();
+
+    private static ILogOutput sLogOutput;
+
+    private static final char[] mSpaceLine = new char[72];
+    private static final char[] mHexDigit = new char[]
+        { '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f' };
+    static {
+        /* prep for hex dump */
+        int i = mSpaceLine.length-1;
+        while (i >= 0)
+            mSpaceLine[i--] = ' ';
+        mSpaceLine[0] = mSpaceLine[1] = mSpaceLine[2] = mSpaceLine[3] = '0';
+        mSpaceLine[4] = '-';
+    }
+
+    static final class Config {
+        static final boolean LOGV = true;
+        static final boolean LOGD = true;
+    }
+
+    private Log() {}
+
+    /**
+     * Outputs a {@link LogLevel#VERBOSE} level message.
+     * @param tag The tag associated with the message.
+     * @param message The message to output.
+     */
+    public static void v(String tag, String message) {
+        println(LogLevel.VERBOSE, tag, message);
+    }
+
+    /**
+     * Outputs a {@link LogLevel#DEBUG} level message.
+     * @param tag The tag associated with the message.
+     * @param message The message to output.
+     */
+    public static void d(String tag, String message) {
+        println(LogLevel.DEBUG, tag, message);
+    }
+
+    /**
+     * Outputs a {@link LogLevel#INFO} level message.
+     * @param tag The tag associated with the message.
+     * @param message The message to output.
+     */
+    public static void i(String tag, String message) {
+        println(LogLevel.INFO, tag, message);
+    }
+
+    /**
+     * Outputs a {@link LogLevel#WARN} level message.
+     * @param tag The tag associated with the message.
+     * @param message The message to output.
+     */
+    public static void w(String tag, String message) {
+        println(LogLevel.WARN, tag, message);
+    }
+
+    /**
+     * Outputs a {@link LogLevel#ERROR} level message.
+     * @param tag The tag associated with the message.
+     * @param message The message to output.
+     */
+    public static void e(String tag, String message) {
+        println(LogLevel.ERROR, tag, message);
+    }
+
+    /**
+     * Outputs a log message and attempts to display it in a dialog.
+     * @param tag The tag associated with the message.
+     * @param message The message to output.
+     */
+    public static void logAndDisplay(LogLevel logLevel, String tag, String message) {
+        if (sLogOutput != null) {
+            sLogOutput.printAndPromptLog(logLevel, tag, message);
+        } else {
+            println(logLevel, tag, message);
+        }
+    }
+
+    /**
+     * Outputs a {@link LogLevel#ERROR} level {@link Throwable} information.
+     * @param tag The tag associated with the message.
+     * @param throwable The {@link Throwable} to output.
+     */
+    public static void e(String tag, Throwable throwable) {
+        if (throwable != null) {
+            StringWriter sw = new StringWriter();
+            PrintWriter pw = new PrintWriter(sw);
+
+            throwable.printStackTrace(pw);
+            println(LogLevel.ERROR, tag, throwable.getMessage() + '\n' + sw.toString());
+        }
+    }
+
+    static void setLevel(LogLevel logLevel) {
+        sLevel = logLevel;
+    }
+
+    /**
+     * Sets the {@link ILogOutput} to use to print the logs. If not set, {@link System#out}
+     * will be used.
+     * @param logOutput The {@link ILogOutput} to use to print the log.
+     */
+    public static void setLogOutput(ILogOutput logOutput) {
+        sLogOutput = logOutput;
+    }
+
+    /**
+     * Show hex dump.
+     * <p/>
+     * Local addition.  Output looks like:
+     * 1230- 00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff  0123456789abcdef
+     * <p/>
+     * Uses no string concatenation; creates one String object per line.
+     */
+    static void hexDump(String tag, LogLevel level, byte[] data, int offset, int length) {
+
+        int kHexOffset = 6;
+        int kAscOffset = 55;
+        char[] line = new char[mSpaceLine.length];
+        int addr, baseAddr, count;
+        int i, ch;
+        boolean needErase = true;
+
+        //Log.w(tag, "HEX DUMP: off=" + offset + ", length=" + length);
+
+        baseAddr = 0;
+        while (length != 0) {
+            if (length > 16) {
+                // full line
+                count = 16;
+            } else {
+                // partial line; re-copy blanks to clear end
+                count = length;
+                needErase = true;
+            }
+
+            if (needErase) {
+                System.arraycopy(mSpaceLine, 0, line, 0, mSpaceLine.length);
+                needErase = false;
+            }
+
+            // output the address (currently limited to 4 hex digits)
+            addr = baseAddr;
+            addr &= 0xffff;
+            ch = 3;
+            while (addr != 0) {
+                line[ch] = mHexDigit[addr & 0x0f];
+                ch--;
+                addr >>>= 4;
+            }
+
+            // output hex digits and ASCII chars
+            ch = kHexOffset;
+            for (i = 0; i < count; i++) {
+                byte val = data[offset + i];
+
+                line[ch++] = mHexDigit[(val >>> 4) & 0x0f];
+                line[ch++] = mHexDigit[val & 0x0f];
+                ch++;
+
+                if (val >= 0x20 && val < 0x7f)
+                    line[kAscOffset + i] = (char) val;
+                else
+                    line[kAscOffset + i] = '.';
+            }
+
+            println(level, tag, new String(line));
+
+            // advance to next chunk of data
+            length -= count;
+            offset += count;
+            baseAddr += count;
+        }
+
+    }
+
+    /**
+     * Dump the entire contents of a byte array with DEBUG priority.
+     */
+    static void hexDump(byte[] data) {
+        hexDump("ddms", LogLevel.DEBUG, data, 0, data.length);
+    }
+
+    /* currently prints to stdout; could write to a log window */
+    private static void println(LogLevel logLevel, String tag, String message) {
+        if (logLevel.getPriority() >= sLevel.getPriority()) {
+            if (sLogOutput != null) {
+                sLogOutput.printLog(logLevel, tag, message);
+            } else {
+                printLog(logLevel, tag, message);
+            }
+        }
+    }
+    
+    /**
+     * Prints a log message.
+     * @param logLevel
+     * @param tag
+     * @param message
+     */
+    public static void printLog(LogLevel logLevel, String tag, String message) {
+        System.out.print(getLogFormatString(logLevel, tag, message));
+    }
+
+    /**
+     * Formats a log message.
+     * @param logLevel
+     * @param tag
+     * @param message
+     */
+    public static String getLogFormatString(LogLevel logLevel, String tag, String message) {
+        SimpleDateFormat formatter = new SimpleDateFormat("hh:mm:ss", Locale.getDefault());
+        return String.format("%s %c/%s: %s\n", formatter.format(new Date()),
+                logLevel.getPriorityLetter(), tag, message);
+    }
+}
+
+
diff --git a/ddmlib/src/main/java/com/android/ddmlib/MonitorThread.java b/ddmlib/src/main/java/com/android/ddmlib/MonitorThread.java
new file mode 100644
index 0000000..a4ff115
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/MonitorThread.java
@@ -0,0 +1,790 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+
+import com.android.ddmlib.DebugPortManager.IDebugPortProvider;
+import com.android.ddmlib.Log.LogLevel;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.nio.channels.CancelledKeyException;
+import java.nio.channels.NotYetBoundException;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+/**
+ * Monitor open connections.
+ */
+final class MonitorThread extends Thread {
+
+    // For broadcasts to message handlers
+    //private static final int CLIENT_CONNECTED = 1;
+
+    private static final int CLIENT_READY = 2;
+
+    private static final int CLIENT_DISCONNECTED = 3;
+
+    private volatile boolean mQuit = false;
+
+    // List of clients we're paying attention to
+    private ArrayList<Client> mClientList;
+
+    // The almighty mux
+    private Selector mSelector;
+
+    // Map chunk types to handlers
+    private HashMap<Integer, ChunkHandler> mHandlerMap;
+
+    // port for "debug selected"
+    private ServerSocketChannel mDebugSelectedChan;
+
+    private int mNewDebugSelectedPort;
+
+    private int mDebugSelectedPort = -1;
+
+    /**
+     * "Selected" client setup to answer debugging connection to the mNewDebugSelectedPort port.
+     */
+    private Client mSelectedClient = null;
+
+    // singleton
+    private static MonitorThread sInstance;
+
+    /**
+     * Generic constructor.
+     */
+    private MonitorThread() {
+        super("Monitor");
+        mClientList = new ArrayList<Client>();
+        mHandlerMap = new HashMap<Integer, ChunkHandler>();
+
+        mNewDebugSelectedPort = DdmPreferences.getSelectedDebugPort();
+    }
+
+    /**
+     * Creates and return the singleton instance of the client monitor thread.
+     */
+    static MonitorThread createInstance() {
+        return sInstance = new MonitorThread();
+    }
+
+    /**
+     * Get singleton instance of the client monitor thread.
+     */
+    static MonitorThread getInstance() {
+        return sInstance;
+    }
+
+
+    /**
+     * Sets or changes the port number for "debug selected".
+     */
+    synchronized void setDebugSelectedPort(int port) throws IllegalStateException {
+        if (sInstance == null) {
+            return;
+        }
+
+        if (!AndroidDebugBridge.getClientSupport()) {
+            return;
+        }
+
+        if (mDebugSelectedChan != null) {
+            Log.d("ddms", "Changing debug-selected port to " + port);
+            mNewDebugSelectedPort = port;
+            wakeup();
+        } else {
+            // we set mNewDebugSelectedPort instead of mDebugSelectedPort so that it's automatically
+            // opened on the first run loop.
+            mNewDebugSelectedPort = port;
+        }
+    }
+
+    /**
+     * Sets the client to accept debugger connection on the custom "Selected debug port".
+     * @param selectedClient the client. Can be null.
+     */
+    synchronized void setSelectedClient(Client selectedClient) {
+        if (sInstance == null) {
+            return;
+        }
+
+        if (mSelectedClient != selectedClient) {
+            Client oldClient = mSelectedClient;
+            mSelectedClient = selectedClient;
+
+            if (oldClient != null) {
+                oldClient.update(Client.CHANGE_PORT);
+            }
+
+            if (mSelectedClient != null) {
+                mSelectedClient.update(Client.CHANGE_PORT);
+            }
+        }
+    }
+
+    /**
+     * Returns the client accepting debugger connection on the custom "Selected debug port".
+     */
+    Client getSelectedClient() {
+        return mSelectedClient;
+    }
+
+
+    /**
+     * Returns "true" if we want to retry connections to clients if we get a bad
+     * JDWP handshake back, "false" if we want to just mark them as bad and
+     * leave them alone.
+     */
+    boolean getRetryOnBadHandshake() {
+        return true; // TODO? make configurable
+    }
+
+    /**
+     * Get an array of known clients.
+     */
+    Client[] getClients() {
+        synchronized (mClientList) {
+            return mClientList.toArray(new Client[mClientList.size()]);
+        }
+    }
+
+    /**
+     * Register "handler" as the handler for type "type".
+     */
+    synchronized void registerChunkHandler(int type, ChunkHandler handler) {
+        if (sInstance == null) {
+            return;
+        }
+
+        synchronized (mHandlerMap) {
+            if (mHandlerMap.get(type) == null) {
+                mHandlerMap.put(type, handler);
+            }
+        }
+    }
+
+    /**
+     * Watch for activity from clients and debuggers.
+     */
+    @Override
+    public void run() {
+        Log.d("ddms", "Monitor is up");
+
+        // create a selector
+        try {
+            mSelector = Selector.open();
+        } catch (IOException ioe) {
+            Log.logAndDisplay(LogLevel.ERROR, "ddms",
+                    "Failed to initialize Monitor Thread: " + ioe.getMessage());
+            return;
+        }
+
+        while (!mQuit) {
+
+            try {
+                /*
+                 * sync with new registrations: we wait until addClient is done before going through
+                 * and doing mSelector.select() again.
+                 * @see {@link #addClient(Client)}
+                 */
+                synchronized (mClientList) {
+                }
+
+                // (re-)open the "debug selected" port, if it's not opened yet or
+                // if the port changed.
+                try {
+                    if (AndroidDebugBridge.getClientSupport()) {
+                        if ((mDebugSelectedChan == null ||
+                                mNewDebugSelectedPort != mDebugSelectedPort) &&
+                                mNewDebugSelectedPort != -1) {
+                            if (reopenDebugSelectedPort()) {
+                                mDebugSelectedPort = mNewDebugSelectedPort;
+                            }
+                        }
+                    }
+                } catch (IOException ioe) {
+                    Log.e("ddms",
+                            "Failed to reopen debug port for Selected Client to: " + mNewDebugSelectedPort);
+                    Log.e("ddms", ioe);
+                    mNewDebugSelectedPort = mDebugSelectedPort; // no retry
+                }
+
+                int count;
+                try {
+                    count = mSelector.select();
+                } catch (IOException ioe) {
+                    ioe.printStackTrace();
+                    continue;
+                } catch (CancelledKeyException cke) {
+                    continue;
+                }
+
+                if (count == 0) {
+                    // somebody called wakeup() ?
+                    // Log.i("ddms", "selector looping");
+                    continue;
+                }
+
+                Set<SelectionKey> keys = mSelector.selectedKeys();
+                Iterator<SelectionKey> iter = keys.iterator();
+
+                while (iter.hasNext()) {
+                    SelectionKey key = iter.next();
+                    iter.remove();
+
+                    try {
+                        if (key.attachment() instanceof Client) {
+                            processClientActivity(key);
+                        }
+                        else if (key.attachment() instanceof Debugger) {
+                            processDebuggerActivity(key);
+                        }
+                        else if (key.attachment() instanceof MonitorThread) {
+                            processDebugSelectedActivity(key);
+                        }
+                        else {
+                            Log.e("ddms", "unknown activity key");
+                        }
+                    } catch (Exception e) {
+                        // we don't want to have our thread be killed because of any uncaught
+                        // exception, so we intercept all here.
+                        Log.e("ddms", "Exception during activity from Selector.");
+                        Log.e("ddms", e);
+                    }
+                }
+            } catch (Exception e) {
+                // we don't want to have our thread be killed because of any uncaught
+                // exception, so we intercept all here.
+                Log.e("ddms", "Exception MonitorThread.run()");
+                Log.e("ddms", e);
+            }
+        }
+    }
+
+
+    /**
+     * Returns the port on which the selected client listen for debugger
+     */
+    int getDebugSelectedPort() {
+        return mDebugSelectedPort;
+    }
+
+    /*
+     * Something happened. Figure out what.
+     */
+    private void processClientActivity(SelectionKey key) {
+        Client client = (Client)key.attachment();
+
+        try {
+            if (!key.isReadable() || !key.isValid()) {
+                Log.d("ddms", "Invalid key from " + client + ". Dropping client.");
+                dropClient(client, true /* notify */);
+                return;
+            }
+
+            client.read();
+
+            /*
+             * See if we have a full packet in the buffer. It's possible we have
+             * more than one packet, so we have to loop.
+             */
+            JdwpPacket packet = client.getJdwpPacket();
+            while (packet != null) {
+                if (packet.isDdmPacket()) {
+                    // unsolicited DDM request - hand it off
+                    assert !packet.isReply();
+                    callHandler(client, packet, null);
+                    packet.consume();
+                } else if (packet.isReply()
+                        && client.isResponseToUs(packet.getId()) != null) {
+                    // reply to earlier DDM request
+                    ChunkHandler handler = client
+                            .isResponseToUs(packet.getId());
+                    if (packet.isError())
+                        client.packetFailed(packet);
+                    else if (packet.isEmpty())
+                        Log.d("ddms", "Got empty reply for 0x"
+                                + Integer.toHexString(packet.getId())
+                                + " from " + client);
+                    else
+                        callHandler(client, packet, handler);
+                    packet.consume();
+                    client.removeRequestId(packet.getId());
+                } else {
+                    Log.v("ddms", "Forwarding client "
+                            + (packet.isReply() ? "reply" : "event") + " 0x"
+                            + Integer.toHexString(packet.getId()) + " to "
+                            + client.getDebugger());
+                    client.forwardPacketToDebugger(packet);
+                }
+
+                // find next
+                packet = client.getJdwpPacket();
+            }
+        } catch (CancelledKeyException e) {
+            // key was canceled probably due to a disconnected client before we could
+            // read stuff coming from the client, so we drop it.
+            dropClient(client, true /* notify */);
+        } catch (IOException ex) {
+            // something closed down, no need to print anything. The client is simply dropped.
+            dropClient(client, true /* notify */);
+        } catch (Exception ex) {
+            Log.e("ddms", ex);
+
+            /* close the client; automatically un-registers from selector */
+            dropClient(client, true /* notify */);
+
+            if (ex instanceof BufferOverflowException) {
+                Log.w("ddms",
+                        "Client data packet exceeded maximum buffer size "
+                                + client);
+            } else {
+                // don't know what this is, display it
+                Log.e("ddms", ex);
+            }
+        }
+    }
+
+    /*
+     * Process an incoming DDM packet. If this is a reply to an earlier request,
+     * "handler" will be set to the handler responsible for the original
+     * request. The spec allows a JDWP message to include multiple DDM chunks.
+     */
+    private void callHandler(Client client, JdwpPacket packet,
+            ChunkHandler handler) {
+
+        // on first DDM packet received, broadcast a "ready" message
+        if (!client.ddmSeen())
+            broadcast(CLIENT_READY, client);
+
+        ByteBuffer buf = packet.getPayload();
+        int type, length;
+        boolean reply = true;
+
+        type = buf.getInt();
+        length = buf.getInt();
+
+        if (handler == null) {
+            // not a reply, figure out who wants it
+            synchronized (mHandlerMap) {
+                handler = mHandlerMap.get(type);
+                reply = false;
+            }
+        }
+
+        if (handler == null) {
+            Log.w("ddms", "Received unsupported chunk type "
+                    + ChunkHandler.name(type) + " (len=" + length + ")");
+        } else {
+            Log.d("ddms", "Calling handler for " + ChunkHandler.name(type)
+                    + " [" + handler + "] (len=" + length + ")");
+            ByteBuffer ibuf = buf.slice();
+            ByteBuffer roBuf = ibuf.asReadOnlyBuffer(); // enforce R/O
+            roBuf.order(ChunkHandler.CHUNK_ORDER);
+            // do the handling of the chunk synchronized on the client list
+            // to be sure there's no concurrency issue when we look for HOME
+            // in hasApp()
+            synchronized (mClientList) {
+                handler.handleChunk(client, type, roBuf, reply, packet.getId());
+            }
+        }
+    }
+
+    /**
+     * Drops a client from the monitor.
+     * <p/>This will lock the {@link Client} list of the {@link Device} running <var>client</var>.
+     * @param client
+     * @param notify
+     */
+    synchronized void dropClient(Client client, boolean notify) {
+        if (sInstance == null) {
+            return;
+        }
+
+        synchronized (mClientList) {
+            if (!mClientList.remove(client)) {
+                return;
+            }
+        }
+        client.close(notify);
+        broadcast(CLIENT_DISCONNECTED, client);
+
+        /*
+         * http://forum.java.sun.com/thread.jspa?threadID=726715&start=0
+         * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5073504
+         */
+        wakeup();
+    }
+
+    /**
+     * Drops the provided list of clients from the monitor. This will lock the {@link Client}
+     * list of the {@link Device} running each of the clients.
+     */
+    synchronized void dropClients(Collection<? extends Client> clients, boolean notify) {
+        for (Client c : clients) {
+            dropClient(c, notify);
+        }
+    }
+
+    /*
+     * Process activity from one of the debugger sockets. This could be a new
+     * connection or a data packet.
+     */
+    private void processDebuggerActivity(SelectionKey key) {
+        Debugger dbg = (Debugger)key.attachment();
+
+        try {
+            if (key.isAcceptable()) {
+                try {
+                    acceptNewDebugger(dbg, null);
+                } catch (IOException ioe) {
+                    Log.w("ddms", "debugger accept() failed");
+                    ioe.printStackTrace();
+                }
+            } else if (key.isReadable()) {
+                processDebuggerData(key);
+            } else {
+                Log.d("ddm-debugger", "key in unknown state");
+            }
+        } catch (CancelledKeyException cke) {
+            // key has been cancelled we can ignore that.
+        }
+    }
+
+    /*
+     * Accept a new connection from a debugger. If successful, register it with
+     * the Selector.
+     */
+    private void acceptNewDebugger(Debugger dbg, ServerSocketChannel acceptChan)
+            throws IOException {
+
+        synchronized (mClientList) {
+            SocketChannel chan;
+
+            if (acceptChan == null)
+                chan = dbg.accept();
+            else
+                chan = dbg.accept(acceptChan);
+
+            if (chan != null) {
+                chan.socket().setTcpNoDelay(true);
+
+                wakeup();
+
+                try {
+                    chan.register(mSelector, SelectionKey.OP_READ, dbg);
+                } catch (IOException ioe) {
+                    // failed, drop the connection
+                    dbg.closeData();
+                    throw ioe;
+                } catch (RuntimeException re) {
+                    // failed, drop the connection
+                    dbg.closeData();
+                    throw re;
+                }
+            } else {
+                Log.w("ddms", "ignoring duplicate debugger");
+                // new connection already closed
+            }
+        }
+    }
+
+    /*
+     * We have incoming data from the debugger. Forward it to the client.
+     */
+    private void processDebuggerData(SelectionKey key) {
+        Debugger dbg = (Debugger)key.attachment();
+
+        try {
+            /*
+             * Read pending data.
+             */
+            dbg.read();
+
+            /*
+             * See if we have a full packet in the buffer. It's possible we have
+             * more than one packet, so we have to loop.
+             */
+            JdwpPacket packet = dbg.getJdwpPacket();
+            while (packet != null) {
+                Log.v("ddms", "Forwarding dbg req 0x"
+                        + Integer.toHexString(packet.getId()) + " to "
+                        + dbg.getClient());
+
+                dbg.forwardPacketToClient(packet);
+
+                packet = dbg.getJdwpPacket();
+            }
+        } catch (IOException ioe) {
+            /*
+             * Close data connection; automatically un-registers dbg from
+             * selector. The failure could be caused by the debugger going away,
+             * or by the client going away and failing to accept our data.
+             * Either way, the debugger connection does not need to exist any
+             * longer. We also need to recycle the connection to the client, so
+             * that the VM sees the debugger disconnect. For a DDM-aware client
+             * this won't be necessary, and we can just send a "debugger
+             * disconnected" message.
+             */
+            Log.d("ddms", "Closing connection to debugger " + dbg);
+            dbg.closeData();
+            Client client = dbg.getClient();
+            if (client.isDdmAware()) {
+                // TODO: soft-disconnect DDM-aware clients
+                Log.d("ddms", " (recycling client connection as well)");
+
+                // we should drop the client, but also attempt to reopen it.
+                // This is done by the DeviceMonitor.
+                client.getDeviceImpl().getMonitor().addClientToDropAndReopen(client,
+                        IDebugPortProvider.NO_STATIC_PORT);
+            } else {
+                Log.d("ddms", " (recycling client connection as well)");
+                // we should drop the client, but also attempt to reopen it.
+                // This is done by the DeviceMonitor.
+                client.getDeviceImpl().getMonitor().addClientToDropAndReopen(client,
+                        IDebugPortProvider.NO_STATIC_PORT);
+            }
+        }
+    }
+
+    /*
+     * Tell the thread that something has changed.
+     */
+    private void wakeup() {
+        mSelector.wakeup();
+    }
+
+    /**
+     * Tell the thread to stop. Called from UI thread.
+     */
+    synchronized void quit() {
+        mQuit = true;
+        wakeup();
+        Log.d("ddms", "Waiting for Monitor thread");
+        try {
+            this.join();
+            // since we're quitting, lets drop all the client and disconnect
+            // the DebugSelectedPort
+            synchronized (mClientList) {
+                for (Client c : mClientList) {
+                    c.close(false /* notify */);
+                    broadcast(CLIENT_DISCONNECTED, c);
+                }
+                mClientList.clear();
+            }
+
+            if (mDebugSelectedChan != null) {
+                mDebugSelectedChan.close();
+                mDebugSelectedChan.socket().close();
+                mDebugSelectedChan = null;
+            }
+            mSelector.close();
+        } catch (InterruptedException ie) {
+            ie.printStackTrace();
+        } catch (IOException e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        }
+
+        sInstance = null;
+    }
+
+    /**
+     * Add a new Client to the list of things we monitor. Also adds the client's
+     * channel and the client's debugger listener to the selection list. This
+     * should only be called from one thread (the VMWatcherThread) to avoid a
+     * race between "alreadyOpen" and Client creation.
+     */
+    synchronized void addClient(Client client) {
+        if (sInstance == null) {
+            return;
+        }
+
+        Log.d("ddms", "Adding new client " + client);
+
+        synchronized (mClientList) {
+            mClientList.add(client);
+
+            /*
+             * Register the Client's socket channel with the selector. We attach
+             * the Client to the SelectionKey. If you try to register a new
+             * channel with the Selector while it is waiting for I/O, you will
+             * block. The solution is to call wakeup() and then hold a lock to
+             * ensure that the registration happens before the Selector goes
+             * back to sleep.
+             */
+            try {
+                wakeup();
+
+                client.register(mSelector);
+
+                Debugger dbg = client.getDebugger();
+                if (dbg != null) {
+                    dbg.registerListener(mSelector);
+                }
+            } catch (IOException ioe) {
+                // not really expecting this to happen
+                ioe.printStackTrace();
+            }
+        }
+    }
+
+    /*
+     * Broadcast an event to all message handlers.
+     */
+    private void broadcast(int event, Client client) {
+        Log.d("ddms", "broadcast " + event + ": " + client);
+
+        /*
+         * The handler objects appear once in mHandlerMap for each message they
+         * handle. We want to notify them once each, so we convert the HashMap
+         * to a HashSet before we iterate.
+         */
+        HashSet<ChunkHandler> set;
+        synchronized (mHandlerMap) {
+            Collection<ChunkHandler> values = mHandlerMap.values();
+            set = new HashSet<ChunkHandler>(values);
+        }
+
+        Iterator<ChunkHandler> iter = set.iterator();
+        while (iter.hasNext()) {
+            ChunkHandler handler = iter.next();
+            switch (event) {
+                case CLIENT_READY:
+                    try {
+                        handler.clientReady(client);
+                    } catch (IOException ioe) {
+                        // Something failed with the client. It should
+                        // fall out of the list the next time we try to
+                        // do something with it, so we discard the
+                        // exception here and assume cleanup will happen
+                        // later. May need to propagate farther. The
+                        // trouble is that not all values for "event" may
+                        // actually throw an exception.
+                        Log.w("ddms",
+                                "Got exception while broadcasting 'ready'");
+                        return;
+                    }
+                    break;
+                case CLIENT_DISCONNECTED:
+                    handler.clientDisconnected(client);
+                    break;
+                default:
+                    throw new UnsupportedOperationException();
+            }
+        }
+
+    }
+
+    /**
+     * Opens (or reopens) the "debug selected" port and listen for connections.
+     * @return true if the port was opened successfully.
+     * @throws IOException
+     */
+    private boolean reopenDebugSelectedPort() throws IOException {
+
+        Log.d("ddms", "reopen debug-selected port: " + mNewDebugSelectedPort);
+        if (mDebugSelectedChan != null) {
+            mDebugSelectedChan.close();
+        }
+
+        mDebugSelectedChan = ServerSocketChannel.open();
+        mDebugSelectedChan.configureBlocking(false); // required for Selector
+
+        InetSocketAddress addr = new InetSocketAddress(
+                InetAddress.getByName("localhost"), //$NON-NLS-1$
+                mNewDebugSelectedPort);
+        mDebugSelectedChan.socket().setReuseAddress(true); // enable SO_REUSEADDR
+
+        try {
+            mDebugSelectedChan.socket().bind(addr);
+            if (mSelectedClient != null) {
+                mSelectedClient.update(Client.CHANGE_PORT);
+            }
+
+            mDebugSelectedChan.register(mSelector, SelectionKey.OP_ACCEPT, this);
+
+            return true;
+        } catch (java.net.BindException e) {
+            displayDebugSelectedBindError(mNewDebugSelectedPort);
+
+            // do not attempt to reopen it.
+            mDebugSelectedChan = null;
+            mNewDebugSelectedPort = -1;
+
+            return false;
+        }
+    }
+
+    /*
+     * We have some activity on the "debug selected" port. Handle it.
+     */
+    private void processDebugSelectedActivity(SelectionKey key) {
+        assert key.isAcceptable();
+
+        ServerSocketChannel acceptChan = (ServerSocketChannel)key.channel();
+
+        /*
+         * Find the debugger associated with the currently-selected client.
+         */
+        if (mSelectedClient != null) {
+            Debugger dbg = mSelectedClient.getDebugger();
+
+            if (dbg != null) {
+                Log.d("ddms", "Accepting connection on 'debug selected' port");
+                try {
+                    acceptNewDebugger(dbg, acceptChan);
+                } catch (IOException ioe) {
+                    // client should be gone, keep going
+                }
+
+                return;
+            }
+        }
+
+        Log.w("ddms",
+                "Connection on 'debug selected' port, but none selected");
+        try {
+            SocketChannel chan = acceptChan.accept();
+            chan.close();
+        } catch (IOException ioe) {
+            // not expected; client should be gone, keep going
+        } catch (NotYetBoundException e) {
+            displayDebugSelectedBindError(mDebugSelectedPort);
+        }
+    }
+
+    private void displayDebugSelectedBindError(int port) {
+        String message = String.format(
+                "Could not open Selected VM debug port (%1$d). Make sure you do not have another instance of DDMS or of the eclipse plugin running. If it's being used by something else, choose a new port number in the preferences.",
+                port);
+
+        Log.logAndDisplay(LogLevel.ERROR, "ddms", message);
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/MultiLineReceiver.java b/ddmlib/src/main/java/com/android/ddmlib/MultiLineReceiver.java
new file mode 100644
index 0000000..52e0416
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/MultiLineReceiver.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+
+/**
+ * Base implementation of {@link IShellOutputReceiver}, that takes the raw data coming from the
+ * socket, and convert it into {@link String} objects.
+ * <p/>Additionally, it splits the string by lines.
+ * <p/>Classes extending it must implement {@link #processNewLines(String[])} which receives
+ * new parsed lines as they become available.
+ */
+public abstract class MultiLineReceiver implements IShellOutputReceiver {
+
+    private boolean mTrimLines = true;
+
+    /** unfinished message line, stored for next packet */
+    private String mUnfinishedLine = null;
+
+    private final ArrayList<String> mArray = new ArrayList<String>();
+
+    /**
+     * Set the trim lines flag.
+     * @param trim whether the lines are trimmed, or not.
+     */
+    public void setTrimLine(boolean trim) {
+        mTrimLines = trim;
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.ddmlib.adb.IShellOutputReceiver#addOutput(
+     *      byte[], int, int)
+     */
+    @Override
+    public final void addOutput(byte[] data, int offset, int length) {
+        if (!isCancelled()) {
+            String s = null;
+            try {
+                s = new String(data, offset, length, "UTF-8"); //$NON-NLS-1$
+            } catch (UnsupportedEncodingException e) {
+                // normal encoding didn't work, try the default one
+                s = new String(data, offset,length);
+            }
+
+            // ok we've got a string
+            // if we had an unfinished line we add it.
+            if (mUnfinishedLine != null) {
+                s = mUnfinishedLine + s;
+                mUnfinishedLine = null;
+            }
+
+            // now we split the lines
+            mArray.clear();
+            int start = 0;
+            do {
+                int index = s.indexOf("\r\n", start); //$NON-NLS-1$
+
+                // if \r\n was not found, this is an unfinished line
+                // and we store it to be processed for the next packet
+                if (index == -1) {
+                    mUnfinishedLine = s.substring(start);
+                    break;
+                }
+
+                // so we found a \r\n;
+                // extract the line
+                String line = s.substring(start, index);
+                if (mTrimLines) {
+                    line = line.trim();
+                }
+                mArray.add(line);
+
+                // move start to after the \r\n we found
+                start = index + 2;
+            } while (true);
+
+            if (!mArray.isEmpty()) {
+                // at this point we've split all the lines.
+                // make the array
+                String[] lines = mArray.toArray(new String[mArray.size()]);
+
+                // send it for final processing
+                processNewLines(lines);
+            }
+        }
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.ddmlib.adb.IShellOutputReceiver#flush()
+     */
+    @Override
+    public final void flush() {
+        if (mUnfinishedLine != null) {
+            processNewLines(new String[] { mUnfinishedLine });
+        }
+
+        done();
+    }
+
+    /**
+     * Terminates the process. This is called after the last lines have been through
+     * {@link #processNewLines(String[])}.
+     */
+    public void done() {
+        // do nothing.
+    }
+
+    /**
+     * Called when new lines are being received by the remote process.
+     * <p/>It is guaranteed that the lines are complete when they are given to this method.
+     * @param lines The array containing the new lines.
+     */
+    public abstract void processNewLines(String[] lines);
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/NativeAllocationInfo.java b/ddmlib/src/main/java/com/android/ddmlib/NativeAllocationInfo.java
new file mode 100644
index 0000000..baefa81
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/NativeAllocationInfo.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Stores native allocation information.
+ * <p/>Contains number of allocations, their size and the stack trace.
+ * <p/>Note: the ddmlib does not resolve the stack trace automatically. While this class provides
+ * storage for resolved stack trace, this is merely for convenience.
+ */
+public final class NativeAllocationInfo {
+    /* Keywords used as delimiters in the string representation of a NativeAllocationInfo */
+    public static final String END_STACKTRACE_KW = "EndStacktrace";
+    public static final String BEGIN_STACKTRACE_KW = "BeginStacktrace:";
+    public static final String TOTAL_SIZE_KW = "TotalSize:";
+    public static final String SIZE_KW = "Size:";
+    public static final String ALLOCATIONS_KW = "Allocations:";
+
+    /* constants for flag bits */
+    private static final int FLAG_ZYGOTE_CHILD  = (1<<31);
+    private static final int FLAG_MASK          = (FLAG_ZYGOTE_CHILD);
+
+    /** Libraries whose methods will be assumed to be not part of the user code. */
+    private static final List<String> FILTERED_LIBRARIES = Arrays.asList(
+            "libc.so",
+            "libc_malloc_debug_leak.so"
+    );
+
+    /** Method names that should be assumed to be not part of the user code. */
+    private static final List<Pattern> FILTERED_METHOD_NAME_PATTERNS = Arrays.asList(
+            Pattern.compile("malloc", Pattern.CASE_INSENSITIVE),
+            Pattern.compile("calloc", Pattern.CASE_INSENSITIVE),
+            Pattern.compile("realloc", Pattern.CASE_INSENSITIVE),
+            Pattern.compile("operator new", Pattern.CASE_INSENSITIVE),
+            Pattern.compile("memalign", Pattern.CASE_INSENSITIVE)
+    );
+
+    private final int mSize;
+
+    private final boolean mIsZygoteChild;
+
+    private final int mAllocations;
+
+    private final ArrayList<Long> mStackCallAddresses = new ArrayList<Long>();
+
+    private ArrayList<NativeStackCallInfo> mResolvedStackCall = null;
+
+    private boolean mIsStackCallResolved = false;
+
+    /**
+     * Constructs a new {@link NativeAllocationInfo}.
+     * @param size The size of the allocations.
+     * @param allocations the allocation count
+     */
+    public NativeAllocationInfo(int size, int allocations) {
+        this.mSize = size & ~FLAG_MASK;
+        this.mIsZygoteChild = ((size & FLAG_ZYGOTE_CHILD) != 0);
+        this.mAllocations = allocations;
+    }
+
+    /**
+     * Adds a stack call address for this allocation.
+     * @param address The address to add.
+     */
+    public void addStackCallAddress(long address) {
+        mStackCallAddresses.add(address);
+    }
+
+    /**
+     * Returns the total size of this allocation.
+     */
+    public int getSize() {
+        return mSize;
+    }
+
+    /**
+     * Returns whether the allocation happened in a child of the zygote
+     * process.
+     */
+    public boolean isZygoteChild() {
+        return mIsZygoteChild;
+    }
+
+    /**
+     * Returns the allocation count.
+     */
+    public int getAllocationCount() {
+        return mAllocations;
+    }
+
+    /**
+     * Returns whether the stack call addresses have been resolved into
+     * {@link NativeStackCallInfo} objects.
+     */
+    public boolean isStackCallResolved() {
+        return mIsStackCallResolved;
+    }
+
+    /**
+     * Returns the stack call of this allocation as raw addresses.
+     * @return the list of addresses where the allocation happened.
+     */
+    public List<Long> getStackCallAddresses() {
+        return mStackCallAddresses;
+    }
+
+    /**
+     * Sets the resolved stack call for this allocation.
+     * <p/>
+     * If <code>resolvedStackCall</code> is non <code>null</code> then
+     * {@link #isStackCallResolved()} will return <code>true</code> after this call.
+     * @param resolvedStackCall The list of {@link NativeStackCallInfo}.
+     */
+    public synchronized void setResolvedStackCall(List<NativeStackCallInfo> resolvedStackCall) {
+        if (mResolvedStackCall == null) {
+            mResolvedStackCall = new ArrayList<NativeStackCallInfo>();
+        } else {
+            mResolvedStackCall.clear();
+        }
+        mResolvedStackCall.addAll(resolvedStackCall);
+        mIsStackCallResolved = !mResolvedStackCall.isEmpty();
+    }
+
+    /**
+     * Returns the resolved stack call.
+     * @return An array of {@link NativeStackCallInfo} or <code>null</code> if the stack call
+     * was not resolved.
+     * @see #setResolvedStackCall(List)
+     * @see #isStackCallResolved()
+     */
+    public synchronized List<NativeStackCallInfo> getResolvedStackCall() {
+        if (mIsStackCallResolved) {
+            return mResolvedStackCall;
+        }
+
+        return null;
+    }
+
+    /**
+     * Indicates whether some other object is "equal to" this one.
+     * @param obj the reference object with which to compare.
+     * @return <code>true</code> if this object is equal to the obj argument;
+     * <code>false</code> otherwise.
+     * @see java.lang.Object#equals(java.lang.Object)
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == this)
+            return true;
+        if (obj instanceof NativeAllocationInfo) {
+            NativeAllocationInfo mi = (NativeAllocationInfo)obj;
+            // quick compare of size, alloc, and stackcall size
+            if (mSize != mi.mSize || mAllocations != mi.mAllocations ||
+                    mStackCallAddresses.size() != mi.mStackCallAddresses.size()) {
+                return false;
+            }
+            // compare the stack addresses
+            int count = mStackCallAddresses.size();
+            for (int i = 0 ; i < count ; i++) {
+                long a = mStackCallAddresses.get(i);
+                long b = mi.mStackCallAddresses.get(i);
+                if (a != b) {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+        return false;
+    }
+
+
+    @Override
+    public int hashCode() {
+        // Follow Effective Java's recipe re hash codes.
+        // Includes all the fields looked at by equals().
+
+        int result = 17;    // arbitrary starting point
+
+        result = 31 * result + mSize;
+        result = 31 * result + mAllocations;
+        result = 31 * result + mStackCallAddresses.size();
+
+        for (long addr : mStackCallAddresses) {
+            result = 31 * result + (int) (addr ^ (addr >>> 32));
+        }
+
+        return result;
+    }
+
+    /**
+     * Returns a string representation of the object.
+     * @see java.lang.Object#toString()
+     */
+    @Override
+    public String toString() {
+        StringBuilder buffer = new StringBuilder();
+        buffer.append(ALLOCATIONS_KW);
+        buffer.append(' ');
+        buffer.append(mAllocations);
+        buffer.append('\n');
+
+        buffer.append(SIZE_KW);
+        buffer.append(' ');
+        buffer.append(mSize);
+        buffer.append('\n');
+
+        buffer.append(TOTAL_SIZE_KW);
+        buffer.append(' ');
+        buffer.append(mSize * mAllocations);
+        buffer.append('\n');
+
+        if (mResolvedStackCall != null) {
+            buffer.append(BEGIN_STACKTRACE_KW);
+            buffer.append('\n');
+            for (NativeStackCallInfo source : mResolvedStackCall) {
+                long addr = source.getAddress();
+                if (addr == 0) {
+                    continue;
+                }
+
+                if (source.getLineNumber() != -1) {
+                    buffer.append(String.format("\t%1$08x\t%2$s --- %3$s --- %4$s:%5$d\n", addr,
+                            source.getLibraryName(), source.getMethodName(),
+                            source.getSourceFile(), source.getLineNumber()));
+                } else {
+                    buffer.append(String.format("\t%1$08x\t%2$s --- %3$s --- %4$s\n", addr,
+                            source.getLibraryName(), source.getMethodName(), source.getSourceFile()));
+                }
+            }
+            buffer.append(END_STACKTRACE_KW);
+            buffer.append('\n');
+        }
+
+        return buffer.toString();
+    }
+
+    /**
+     * Returns the first {@link NativeStackCallInfo} that is relevant.
+     * <p/>
+     * A relevant <code>NativeStackCallInfo</code> is a stack call that is not deep in the
+     * lower level of the libc, but the actual method that performed the allocation.
+     * @return a <code>NativeStackCallInfo</code> or <code>null</code> if the stack call has not
+     * been processed from the raw addresses.
+     * @see #setResolvedStackCall(List)
+     * @see #isStackCallResolved()
+     */
+    public synchronized NativeStackCallInfo getRelevantStackCallInfo() {
+        if (mIsStackCallResolved && mResolvedStackCall != null) {
+            for (NativeStackCallInfo info : mResolvedStackCall) {
+                if (isRelevantLibrary(info.getLibraryName())
+                        && isRelevantMethod(info.getMethodName())) {
+                    return info;
+                }
+            }
+
+            // couldn't find a relevant one, so we'll return the first one if it exists.
+            if (!mResolvedStackCall.isEmpty())
+                return mResolvedStackCall.get(0);
+        }
+
+        return null;
+    }
+
+    private boolean isRelevantLibrary(String libPath) {
+        for (String l : FILTERED_LIBRARIES) {
+            if (libPath.endsWith(l)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    private boolean isRelevantMethod(String methodName) {
+        for (Pattern p : FILTERED_METHOD_NAME_PATTERNS) {
+            Matcher m = p.matcher(methodName);
+            if (m.find()) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/NativeLibraryMapInfo.java b/ddmlib/src/main/java/com/android/ddmlib/NativeLibraryMapInfo.java
new file mode 100644
index 0000000..5a26317
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/NativeLibraryMapInfo.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+/**
+ * Memory address to library mapping for native libraries.
+ * <p/>
+ * Each instance represents a single native library and its start and end memory addresses. 
+ */
+public final class NativeLibraryMapInfo {
+    private long mStartAddr;
+    private long mEndAddr;
+
+    private String mLibrary;
+
+    /**
+     * Constructs a new native library map info.
+     * @param startAddr The start address of the library.
+     * @param endAddr The end address of the library.
+     * @param library The name of the library.
+     */
+    NativeLibraryMapInfo(long startAddr, long endAddr, String library) {
+        this.mStartAddr = startAddr;
+        this.mEndAddr = endAddr;
+        this.mLibrary = library;
+    }
+    
+    /**
+     * Returns the name of the library.
+     */
+    public String getLibraryName() {
+        return mLibrary;
+    }
+    
+    /**
+     * Returns the start address of the library.
+     */
+    public long getStartAddress() {
+        return mStartAddr;
+    }
+    
+    /**
+     * Returns the end address of the library.
+     */
+    public long getEndAddress() {
+        return mEndAddr;
+    }
+
+    /**
+     * Returns whether the specified address is inside the library.
+     * @param address The address to test.
+     * @return <code>true</code> if the address is between the start and end address of the library.
+     * @see #getStartAddress()
+     * @see #getEndAddress()
+     */
+    public boolean isWithinLibrary(long address) {
+        return address >= mStartAddr && address <= mEndAddr;
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/NativeStackCallInfo.java b/ddmlib/src/main/java/com/android/ddmlib/NativeStackCallInfo.java
new file mode 100644
index 0000000..be365bf
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/NativeStackCallInfo.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Represents a stack call. This is used to return all of the call
+ * information as one object.
+ */
+public final class NativeStackCallInfo {
+    private static final Pattern SOURCE_NAME_PATTERN = Pattern.compile("^(.+):(\\d+)$");
+
+    /** address of this stack frame */
+    private long mAddress;
+
+    /** name of the library */
+    private String mLibrary;
+
+    /** name of the method */
+    private String mMethod;
+
+    /**
+     * name of the source file + line number in the format<br>
+     * <sourcefile>:<linenumber>
+     */
+    private String mSourceFile;
+
+    private int mLineNumber = -1;
+
+    /**
+     * Basic constructor with library, method, and sourcefile information
+     *
+     * @param address address of this stack frame
+     * @param lib The name of the library
+     * @param method the name of the method
+     * @param sourceFile the name of the source file and the line number
+     * as "[sourcefile]:[fileNumber]"
+     */
+    public NativeStackCallInfo(long address, String lib, String method, String sourceFile) {
+        mAddress = address;
+        mLibrary = lib;
+        mMethod = method;
+
+        Matcher m = SOURCE_NAME_PATTERN.matcher(sourceFile);
+        if (m.matches()) {
+            mSourceFile = m.group(1);
+            try {
+                mLineNumber = Integer.parseInt(m.group(2));
+            } catch (NumberFormatException e) {
+                // do nothing, the line number will stay at -1
+            }
+        } else {
+            mSourceFile = sourceFile;
+        }
+    }
+
+    /**
+     * Returns the address of this stack frame.
+     */
+    public long getAddress() {
+        return mAddress;
+    }
+
+    /**
+     * Returns the name of the library name.
+     */
+    public String getLibraryName() {
+        return mLibrary;
+    }
+
+    /**
+     * Returns the name of the method.
+     */
+    public String getMethodName() {
+        return mMethod;
+    }
+
+    /**
+     * Returns the name of the source file.
+     */
+    public String getSourceFile() {
+        return mSourceFile;
+    }
+
+    /**
+     * Returns the line number, or -1 if unknown.
+     */
+    public int getLineNumber() {
+        return mLineNumber;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("\t%1$08x\t%2$s --- %3$s --- %4$s:%5$d",
+                getAddress(), getLibraryName(), getMethodName(), getSourceFile(), getLineNumber());
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/NullOutputReceiver.java b/ddmlib/src/main/java/com/android/ddmlib/NullOutputReceiver.java
new file mode 100644
index 0000000..a963a64
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/NullOutputReceiver.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+/**
+ * Implementation of {@link IShellOutputReceiver} that does nothing.
+ * <p/>This can be used to execute a remote shell command when the output is not needed.
+ */
+public final class NullOutputReceiver implements IShellOutputReceiver {
+
+    private static NullOutputReceiver sReceiver = new NullOutputReceiver();
+
+    public static IShellOutputReceiver getReceiver() {
+        return sReceiver;
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.ddmlib.adb.IShellOutputReceiver#addOutput(byte[], int, int)
+     */
+    @Override
+    public void addOutput(byte[] data, int offset, int length) {
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.ddmlib.adb.IShellOutputReceiver#flush()
+     */
+    @Override
+    public void flush() {
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.ddmlib.adb.IShellOutputReceiver#isCancelled()
+     */
+    @Override
+    public boolean isCancelled() {
+        return false;
+    }
+
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/RawImage.java b/ddmlib/src/main/java/com/android/ddmlib/RawImage.java
new file mode 100644
index 0000000..adb0cc9
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/RawImage.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Data representing an image taken from a device frame buffer.
+ */
+public final class RawImage {
+    public int version;
+    public int bpp;
+    public int size;
+    public int width;
+    public int height;
+    public int red_offset;
+    public int red_length;
+    public int blue_offset;
+    public int blue_length;
+    public int green_offset;
+    public int green_length;
+    public int alpha_offset;
+    public int alpha_length;
+
+    public byte[] data;
+
+    /**
+     * Reads the header of a RawImage from a {@link ByteBuffer}.
+     * <p/>The way the data is sent over adb is defined in system/core/adb/framebuffer_service.c
+     * @param version the version of the protocol.
+     * @param buf the buffer to read from.
+     * @return true if success
+     */
+    public boolean readHeader(int version, ByteBuffer buf) {
+        this.version = version;
+
+        if (version == 16) {
+            // compatibility mode with original protocol
+            this.bpp = 16;
+
+            // read actual values.
+            this.size = buf.getInt();
+            this.width = buf.getInt();
+            this.height = buf.getInt();
+
+            // create default values for the rest. Format is 565
+            this.red_offset = 11;
+            this.red_length = 5;
+            this.green_offset = 5;
+            this.green_length = 6;
+            this.blue_offset = 0;
+            this.blue_length = 5;
+            this.alpha_offset = 0;
+            this.alpha_length = 0;
+        } else if (version == 1) {
+            this.bpp = buf.getInt();
+            this.size = buf.getInt();
+            this.width = buf.getInt();
+            this.height = buf.getInt();
+            this.red_offset = buf.getInt();
+            this.red_length = buf.getInt();
+            this.blue_offset = buf.getInt();
+            this.blue_length = buf.getInt();
+            this.green_offset = buf.getInt();
+            this.green_length = buf.getInt();
+            this.alpha_offset = buf.getInt();
+            this.alpha_length = buf.getInt();
+        } else {
+            // unsupported protocol!
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns the mask value for the red color.
+     * <p/>This value is compatible with org.eclipse.swt.graphics.PaletteData
+     */
+    public int getRedMask() {
+        return getMask(red_length, red_offset);
+    }
+
+    /**
+     * Returns the mask value for the green color.
+     * <p/>This value is compatible with org.eclipse.swt.graphics.PaletteData
+     */
+    public int getGreenMask() {
+        return getMask(green_length, green_offset);
+    }
+
+    /**
+     * Returns the mask value for the blue color.
+     * <p/>This value is compatible with org.eclipse.swt.graphics.PaletteData
+     */
+    public int getBlueMask() {
+        return getMask(blue_length, blue_offset);
+    }
+
+    /**
+     * Returns the size of the header for a specific version of the framebuffer adb protocol.
+     * @param version the version of the protocol
+     * @return the number of int that makes up the header.
+     */
+    public static int getHeaderSize(int version) {
+        switch (version) {
+            case 16: // compatibility mode
+                return 3; // size, width, height
+            case 1:
+                return 12; // bpp, size, width, height, 4*(length, offset)
+        }
+
+        return 0;
+    }
+
+    /**
+     * Returns a rotated version of the image
+     * The image is rotated counter-clockwise.
+     */
+    public RawImage getRotated() {
+        RawImage rotated = new RawImage();
+        rotated.version = this.version;
+        rotated.bpp = this.bpp;
+        rotated.size = this.size;
+        rotated.red_offset = this.red_offset;
+        rotated.red_length = this.red_length;
+        rotated.blue_offset = this.blue_offset;
+        rotated.blue_length = this.blue_length;
+        rotated.green_offset = this.green_offset;
+        rotated.green_length = this.green_length;
+        rotated.alpha_offset = this.alpha_offset;
+        rotated.alpha_length = this.alpha_length;
+
+        rotated.width = this.height;
+        rotated.height = this.width;
+
+        int count = this.data.length;
+        rotated.data = new byte[count];
+
+        int byteCount = this.bpp >> 3; // bpp is in bits, we want bytes to match our array
+        final int w = this.width;
+        final int h = this.height;
+        for (int y = 0 ; y < h ; y++) {
+            for (int x = 0 ; x < w ; x++) {
+                System.arraycopy(
+                        this.data, (y * w + x) * byteCount,
+                        rotated.data, ((w-x-1) * h + y) * byteCount,
+                        byteCount);
+            }
+        }
+
+        return rotated;
+    }
+
+    /**
+     * Returns an ARGB integer value for the pixel at <var>index</var> in {@link #data}.
+     */
+    public int getARGB(int index) {
+        int value;
+        if (bpp == 16) {
+            value = data[index] & 0x00FF;
+            value |= (data[index+1] << 8) & 0x0FF00;
+        } else if (bpp == 32) {
+            value = data[index] & 0x00FF;
+            value |= (data[index+1] & 0x00FF) << 8;
+            value |= (data[index+2] & 0x00FF) << 16;
+            value |= (data[index+3] & 0x00FF) << 24;
+        } else {
+            throw new UnsupportedOperationException("RawImage.getARGB(int) only works in 16 and 32 bit mode.");
+        }
+
+        int r = ((value >>> red_offset) & getMask(red_length)) << (8 - red_length);
+        int g = ((value >>> green_offset) & getMask(green_length)) << (8 - green_length);
+        int b = ((value >>> blue_offset) & getMask(blue_length)) << (8 - blue_length);
+        int a;
+        if (alpha_length == 0) {
+            a = 0xFF; // force alpha to opaque if there's no alpha value in the framebuffer.
+        } else {
+            a = ((value >>> alpha_offset) & getMask(alpha_length)) << (8 - alpha_length);
+        }
+
+        return a << 24 | r << 16 | g << 8 | b;
+    }
+
+    /**
+     * creates a mask value based on a length and offset.
+     * <p/>This value is compatible with org.eclipse.swt.graphics.PaletteData
+     */
+    private int getMask(int length, int offset) {
+        int res = getMask(length) << offset;
+
+        // if the bpp is 32 bits then we need to invert it because the buffer is in little endian
+        if (bpp == 32) {
+            return Integer.reverseBytes(res);
+        }
+
+        return res;
+    }
+
+    /**
+     * Creates a mask value based on a length.
+     * @param length
+     * @return
+     */
+    private static int getMask(int length) {
+        return (1 << length) - 1;
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/ShellCommandUnresponsiveException.java b/ddmlib/src/main/java/com/android/ddmlib/ShellCommandUnresponsiveException.java
new file mode 100644
index 0000000..09823c4
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/ShellCommandUnresponsiveException.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2010 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.ddmlib;
+
+
+/**
+ * Exception thrown when a shell command executed on a device takes too long to send its output.
+ * <p/>The command may not actually be unresponsive, it just has spent too much time not outputting
+ * any thing to the console.
+ */
+public class ShellCommandUnresponsiveException extends Exception {
+    private static final long serialVersionUID = 1L;
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/SyncException.java b/ddmlib/src/main/java/com/android/ddmlib/SyncException.java
new file mode 100644
index 0000000..76de367
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/SyncException.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2010 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.ddmlib;
+
+import java.io.IOException;
+
+/**
+ * Exception thrown when a transfer using {@link SyncService} doesn't complete.
+ * <p/>This is different from an {@link IOException} because it's not the underlying connection
+ * that triggered the error, but the adb transfer protocol that didn't work somehow, or that the
+ * targets (local and/or remote) were wrong.
+ */
+public class SyncException extends CanceledException {
+    private static final long serialVersionUID = 1L;
+
+    public enum SyncError {
+        /** canceled transfer */
+        CANCELED("Operation was canceled by the user."),
+        /** Transfer error */
+        TRANSFER_PROTOCOL_ERROR("Adb Transfer Protocol Error."),
+        /** unknown remote object during a pull */
+        NO_REMOTE_OBJECT("Remote object doesn't exist!"),
+        /** Result code when attempting to pull multiple files into a file */
+        TARGET_IS_FILE("Target object is a file."),
+        /** Result code when attempting to pull multiple into a directory that does not exist. */
+        NO_DIR_TARGET("Target directory doesn't exist."),
+        /** wrong encoding on the remote path. */
+        REMOTE_PATH_ENCODING("Remote Path encoding is not supported."),
+        /** remote path that is too long. */
+        REMOTE_PATH_LENGTH("Remote path is too long."),
+        /** error while reading local file. */
+        FILE_READ_ERROR("Reading local file failed!"),
+        /** error while writing local file. */
+        FILE_WRITE_ERROR("Writing local file failed!"),
+        /** attempting to push a directory. */
+        LOCAL_IS_DIRECTORY("Local path is a directory."),
+        /** attempting to push a non-existent file. */
+        NO_LOCAL_FILE("Local path doesn't exist."),
+        /** when the target path of a multi file push is a file. */
+        REMOTE_IS_FILE("Remote path is a file."),
+        /** receiving too much data from the remove device at once */
+        BUFFER_OVERRUN("Receiving too much data.");
+
+        private final String mMessage;
+
+        private SyncError(String message) {
+            mMessage = message;
+        }
+
+        public String getMessage() {
+            return mMessage;
+        }
+    }
+
+    private final SyncError mError;
+
+    public SyncException(SyncError error) {
+        super(error.getMessage());
+        mError = error;
+    }
+
+    public SyncException(SyncError error, String message) {
+        super(message);
+        mError = error;
+    }
+
+    public SyncException(SyncError error, Throwable cause) {
+        super(error.getMessage(), cause);
+        mError = error;
+    }
+
+    public SyncError getErrorCode() {
+        return mError;
+    }
+
+    /**
+     * Returns true if the sync was canceled by user input.
+     */
+   @Override
+   public boolean wasCanceled() {
+        return mError == SyncError.CANCELED;
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/SyncService.java b/ddmlib/src/main/java/com/android/ddmlib/SyncService.java
new file mode 100644
index 0000000..3884917
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/SyncService.java
@@ -0,0 +1,887 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+import com.android.ddmlib.AdbHelper.AdbResponse;
+import com.android.ddmlib.FileListingService.FileEntry;
+import com.android.ddmlib.SyncException.SyncError;
+import com.android.ddmlib.utils.ArrayHelper;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.InetSocketAddress;
+import java.nio.channels.SocketChannel;
+import java.util.ArrayList;
+
+/**
+ * Sync service class to push/pull to/from devices/emulators, through the debug bridge.
+ * <p/>
+ * To get a {@link SyncService} object, use {@link Device#getSyncService()}.
+ */
+public final class SyncService {
+
+    private static final byte[] ID_OKAY = { 'O', 'K', 'A', 'Y' };
+    private static final byte[] ID_FAIL = { 'F', 'A', 'I', 'L' };
+    private static final byte[] ID_STAT = { 'S', 'T', 'A', 'T' };
+    private static final byte[] ID_RECV = { 'R', 'E', 'C', 'V' };
+    private static final byte[] ID_DATA = { 'D', 'A', 'T', 'A' };
+    private static final byte[] ID_DONE = { 'D', 'O', 'N', 'E' };
+    private static final byte[] ID_SEND = { 'S', 'E', 'N', 'D' };
+//    private final static byte[] ID_LIST = { 'L', 'I', 'S', 'T' };
+//    private final static byte[] ID_DENT = { 'D', 'E', 'N', 'T' };
+
+    private static final NullSyncProgressMonitor sNullSyncProgressMonitor =
+            new NullSyncProgressMonitor();
+
+    private static final int S_ISOCK = 0xC000; // type: symbolic link
+    private static final int S_IFLNK = 0xA000; // type: symbolic link
+    private static final int S_IFREG = 0x8000; // type: regular file
+    private static final int S_IFBLK = 0x6000; // type: block device
+    private static final int S_IFDIR = 0x4000; // type: directory
+    private static final int S_IFCHR = 0x2000; // type: character device
+    private static final int S_IFIFO = 0x1000; // type: fifo
+/*
+    private final static int S_ISUID = 0x0800; // set-uid bit
+    private final static int S_ISGID = 0x0400; // set-gid bit
+    private final static int S_ISVTX = 0x0200; // sticky bit
+    private final static int S_IRWXU = 0x01C0; // user permissions
+    private final static int S_IRUSR = 0x0100; // user: read
+    private final static int S_IWUSR = 0x0080; // user: write
+    private final static int S_IXUSR = 0x0040; // user: execute
+    private final static int S_IRWXG = 0x0038; // group permissions
+    private final static int S_IRGRP = 0x0020; // group: read
+    private final static int S_IWGRP = 0x0010; // group: write
+    private final static int S_IXGRP = 0x0008; // group: execute
+    private final static int S_IRWXO = 0x0007; // other permissions
+    private final static int S_IROTH = 0x0004; // other: read
+    private final static int S_IWOTH = 0x0002; // other: write
+    private final static int S_IXOTH = 0x0001; // other: execute
+*/
+
+    private static final int SYNC_DATA_MAX = 64*1024;
+    private static final int REMOTE_PATH_MAX_LENGTH = 1024;
+
+    /**
+     * Classes which implement this interface provide methods that deal
+     * with displaying transfer progress.
+     */
+    public interface ISyncProgressMonitor {
+        /**
+         * Sent when the transfer starts
+         * @param totalWork the total amount of work.
+         */
+        public void start(int totalWork);
+        /**
+         * Sent when the transfer is finished or interrupted.
+         */
+        public void stop();
+        /**
+         * Sent to query for possible cancellation.
+         * @return true if the transfer should be stopped.
+         */
+        public boolean isCanceled();
+        /**
+         * Sent when a sub task is started.
+         * @param name the name of the sub task.
+         */
+        public void startSubTask(String name);
+        /**
+         * Sent when some progress have been made.
+         * @param work the amount of work done.
+         */
+        public void advance(int work);
+    }
+
+    /**
+     * A Sync progress monitor that does nothing
+     */
+    private static class NullSyncProgressMonitor implements ISyncProgressMonitor {
+        @Override
+        public void advance(int work) {
+        }
+        @Override
+        public boolean isCanceled() {
+            return false;
+        }
+
+        @Override
+        public void start(int totalWork) {
+        }
+        @Override
+        public void startSubTask(String name) {
+        }
+        @Override
+        public void stop() {
+        }
+    }
+
+    private InetSocketAddress mAddress;
+    private Device mDevice;
+    private SocketChannel mChannel;
+
+    /**
+     * Buffer used to send data. Allocated when needed and reused afterward.
+     */
+    private byte[] mBuffer;
+
+    /**
+     * Creates a Sync service object.
+     * @param address The address to connect to
+     * @param device the {@link Device} that the service connects to.
+     */
+    SyncService(InetSocketAddress address, Device device) {
+        mAddress = address;
+        mDevice = device;
+    }
+
+    /**
+     * Opens the sync connection. This must be called before any calls to push[File] / pull[File].
+     * @return true if the connection opened, false if adb refuse the connection. This can happen
+     * if the {@link Device} is invalid.
+     * @throws TimeoutException in case of timeout on the connection.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     * @throws IOException If the connection to adb failed.
+     */
+    boolean openSync() throws TimeoutException, AdbCommandRejectedException, IOException {
+        try {
+            mChannel = SocketChannel.open(mAddress);
+            mChannel.configureBlocking(false);
+
+            // target a specific device
+            AdbHelper.setDevice(mChannel, mDevice);
+
+            byte[] request = AdbHelper.formAdbRequest("sync:"); //$NON-NLS-1$
+            AdbHelper.write(mChannel, request, -1, DdmPreferences.getTimeOut());
+
+            AdbResponse resp = AdbHelper.readAdbResponse(mChannel, false /* readDiagString */);
+
+            if (!resp.okay) {
+                Log.w("ddms", "Got unhappy response from ADB sync req: " + resp.message);
+                mChannel.close();
+                mChannel = null;
+                return false;
+            }
+        } catch (TimeoutException e) {
+            if (mChannel != null) {
+                try {
+                    mChannel.close();
+                } catch (IOException e2) {
+                    // we want to throw the original exception, so we ignore this one.
+                }
+                mChannel = null;
+            }
+
+            throw e;
+        } catch (IOException e) {
+            if (mChannel != null) {
+                try {
+                    mChannel.close();
+                } catch (IOException e2) {
+                    // we want to throw the original exception, so we ignore this one.
+                }
+                mChannel = null;
+            }
+
+            throw e;
+        }
+
+        return true;
+    }
+
+    /**
+     * Closes the connection.
+     */
+    public void close() {
+        if (mChannel != null) {
+            try {
+                mChannel.close();
+            } catch (IOException e) {
+                // nothing to be done really...
+            }
+            mChannel = null;
+        }
+    }
+
+    /**
+     * Returns a sync progress monitor that does nothing. This allows background tasks that don't
+     * want/need to display ui, to pass a valid {@link ISyncProgressMonitor}.
+     * <p/>This object can be reused multiple times and can be used by concurrent threads.
+     */
+    public static ISyncProgressMonitor getNullProgressMonitor() {
+        return sNullSyncProgressMonitor;
+    }
+
+    /**
+     * Pulls file(s) or folder(s).
+     * @param entries the remote item(s) to pull
+     * @param localPath The local destination. If the entries count is > 1 or
+     *      if the unique entry is a folder, this should be a folder.
+     * @param monitor The progress monitor. Cannot be null.
+     * @throws SyncException
+     * @throws IOException
+     * @throws TimeoutException
+     *
+     * @see FileListingService.FileEntry
+     * @see #getNullProgressMonitor()
+     */
+    public void pull(FileEntry[] entries, String localPath, ISyncProgressMonitor monitor)
+            throws SyncException, IOException, TimeoutException {
+
+        // first we check the destination is a directory and exists
+        File f = new File(localPath);
+        if (!f.exists()) {
+            throw new SyncException(SyncError.NO_DIR_TARGET);
+        }
+        if (!f.isDirectory()) {
+            throw new SyncException(SyncError.TARGET_IS_FILE);
+        }
+
+        // get a FileListingService object
+        FileListingService fls = new FileListingService(mDevice);
+
+        // compute the number of file to move
+        int total = getTotalRemoteFileSize(entries, fls);
+
+        // start the monitor
+        monitor.start(total);
+
+        doPull(entries, localPath, fls, monitor);
+
+        monitor.stop();
+    }
+
+    /**
+     * Pulls a single file.
+     * @param remote the remote file
+     * @param localFilename The local destination.
+     * @param monitor The progress monitor. Cannot be null.
+     *
+     * @throws IOException in case of an IO exception.
+     * @throws TimeoutException in case of a timeout reading responses from the device.
+     * @throws SyncException in case of a sync exception.
+     *
+     * @see FileListingService.FileEntry
+     * @see #getNullProgressMonitor()
+     */
+    public void pullFile(FileEntry remote, String localFilename, ISyncProgressMonitor monitor)
+            throws IOException, SyncException, TimeoutException {
+        int total = remote.getSizeValue();
+        monitor.start(total);
+
+        doPullFile(remote.getFullPath(), localFilename, monitor);
+
+        monitor.stop();
+    }
+
+    /**
+     * Pulls a single file.
+     * <p/>Because this method just deals with a String for the remote file instead of a
+     * {@link FileEntry}, the size of the file being pulled is unknown and the
+     * {@link ISyncProgressMonitor} will not properly show the progress
+     * @param remoteFilepath the full path to the remote file
+     * @param localFilename The local destination.
+     * @param monitor The progress monitor. Cannot be null.
+     *
+     * @throws IOException in case of an IO exception.
+     * @throws TimeoutException in case of a timeout reading responses from the device.
+     * @throws SyncException in case of a sync exception.
+     *
+     * @see #getNullProgressMonitor()
+     */
+    public void pullFile(String remoteFilepath, String localFilename,
+            ISyncProgressMonitor monitor) throws TimeoutException, IOException, SyncException {
+        Integer mode = readMode(remoteFilepath);
+        if (mode == null) {
+            // attempts to download anyway
+        } else if (mode == 0) {
+            throw new SyncException(SyncError.NO_REMOTE_OBJECT);
+        }
+
+        monitor.start(0);
+        //TODO: use the {@link FileListingService} to get the file size.
+
+        doPullFile(remoteFilepath, localFilename, monitor);
+
+        monitor.stop();
+    }
+
+    /**
+     * Push several files.
+     * @param local An array of loca files to push
+     * @param remote the remote {@link FileEntry} representing a directory.
+     * @param monitor The progress monitor. Cannot be null.
+     * @throws SyncException if file could not be pushed
+     * @throws IOException in case of I/O error on the connection.
+     * @throws TimeoutException in case of a timeout reading responses from the device.
+     */
+    public void push(String[] local, FileEntry remote, ISyncProgressMonitor monitor)
+            throws SyncException, IOException, TimeoutException {
+        if (!remote.isDirectory()) {
+            throw new SyncException(SyncError.REMOTE_IS_FILE);
+        }
+
+        // make a list of File from the list of String
+        ArrayList<File> files = new ArrayList<File>();
+        for (String path : local) {
+            files.add(new File(path));
+        }
+
+        // get the total count of the bytes to transfer
+        File[] fileArray = files.toArray(new File[files.size()]);
+        int total = getTotalLocalFileSize(fileArray);
+
+        monitor.start(total);
+
+        doPush(fileArray, remote.getFullPath(), monitor);
+
+        monitor.stop();
+    }
+
+    /**
+     * Push a single file.
+     * @param local the local filepath.
+     * @param remote The remote filepath.
+     * @param monitor The progress monitor. Cannot be null.
+     *
+     * @throws SyncException if file could not be pushed
+     * @throws IOException in case of I/O error on the connection.
+     * @throws TimeoutException in case of a timeout reading responses from the device.
+     */
+    public void pushFile(String local, String remote, ISyncProgressMonitor monitor)
+            throws SyncException, IOException, TimeoutException {
+        File f = new File(local);
+        if (!f.exists()) {
+            throw new SyncException(SyncError.NO_LOCAL_FILE);
+        }
+
+        if (f.isDirectory()) {
+            throw new SyncException(SyncError.LOCAL_IS_DIRECTORY);
+        }
+
+        monitor.start((int)f.length());
+
+        doPushFile(local, remote, monitor);
+
+        monitor.stop();
+    }
+
+    /**
+     * compute the recursive file size of all the files in the list. Folder
+     * have a weight of 1.
+     * @param entries
+     * @param fls
+     * @return
+     */
+    private int getTotalRemoteFileSize(FileEntry[] entries, FileListingService fls) {
+        int count = 0;
+        for (FileEntry e : entries) {
+            int type = e.getType();
+            if (type == FileListingService.TYPE_DIRECTORY) {
+                // get the children
+                FileEntry[] children = fls.getChildren(e, false, null);
+                count += getTotalRemoteFileSize(children, fls) + 1;
+            } else if (type == FileListingService.TYPE_FILE) {
+                count += e.getSizeValue();
+            }
+        }
+
+        return count;
+    }
+
+    /**
+     * compute the recursive file size of all the files in the list. Folder
+     * have a weight of 1.
+     * This does not check for circular links.
+     * @param files
+     * @return
+     */
+    private int getTotalLocalFileSize(File[] files) {
+        int count = 0;
+
+        for (File f : files) {
+            if (f.exists()) {
+                if (f.isDirectory()) {
+                    return getTotalLocalFileSize(f.listFiles()) + 1;
+                } else if (f.isFile()) {
+                    count += f.length();
+                }
+            }
+        }
+
+        return count;
+    }
+
+    /**
+     * Pulls multiple files/folders recursively.
+     * @param entries The list of entry to pull
+     * @param localPath the localpath to a directory
+     * @param fileListingService a FileListingService object to browse through remote directories.
+     * @param monitor the progress monitor. Must be started already.
+     *
+     * @throws SyncException if file could not be pushed
+     * @throws IOException in case of I/O error on the connection.
+     * @throws TimeoutException in case of a timeout reading responses from the device.
+     */
+    private void doPull(FileEntry[] entries, String localPath,
+            FileListingService fileListingService,
+            ISyncProgressMonitor monitor) throws SyncException, IOException, TimeoutException {
+
+        for (FileEntry e : entries) {
+            // check if we're cancelled
+            if (monitor.isCanceled()) {
+                throw new SyncException(SyncError.CANCELED);
+            }
+
+            // get type (we only pull directory and files for now)
+            int type = e.getType();
+            if (type == FileListingService.TYPE_DIRECTORY) {
+                monitor.startSubTask(e.getFullPath());
+                String dest = localPath + File.separator + e.getName();
+
+                // make the directory
+                File d = new File(dest);
+                d.mkdir();
+
+                // then recursively call the content. Since we did a ls command
+                // to get the number of files, we can use the cache
+                FileEntry[] children = fileListingService.getChildren(e, true, null);
+                doPull(children, dest, fileListingService, monitor);
+                monitor.advance(1);
+            } else if (type == FileListingService.TYPE_FILE) {
+                monitor.startSubTask(e.getFullPath());
+                String dest = localPath + File.separator + e.getName();
+                doPullFile(e.getFullPath(), dest, monitor);
+            }
+        }
+    }
+
+    /**
+     * Pulls a remote file
+     * @param remotePath the remote file (length max is 1024)
+     * @param localPath the local destination
+     * @param monitor the monitor. The monitor must be started already.
+     * @throws SyncException if file could not be pushed
+     * @throws IOException in case of I/O error on the connection.
+     * @throws TimeoutException in case of a timeout reading responses from the device.
+     */
+    private void doPullFile(String remotePath, String localPath,
+            ISyncProgressMonitor monitor) throws IOException, SyncException, TimeoutException {
+        byte[] msg = null;
+        byte[] pullResult = new byte[8];
+
+        final int timeOut = DdmPreferences.getTimeOut();
+
+        try {
+            byte[] remotePathContent = remotePath.getBytes(AdbHelper.DEFAULT_ENCODING);
+
+            if (remotePathContent.length > REMOTE_PATH_MAX_LENGTH) {
+                throw new SyncException(SyncError.REMOTE_PATH_LENGTH);
+            }
+
+            // create the full request message
+            msg = createFileReq(ID_RECV, remotePathContent);
+
+            // and send it.
+            AdbHelper.write(mChannel, msg, -1, timeOut);
+
+            // read the result, in a byte array containing 2 ints
+            // (id, size)
+            AdbHelper.read(mChannel, pullResult, -1, timeOut);
+
+            // check we have the proper data back
+            if (!checkResult(pullResult, ID_DATA) &&
+                    !checkResult(pullResult, ID_DONE)) {
+                throw new SyncException(SyncError.TRANSFER_PROTOCOL_ERROR,
+                        readErrorMessage(pullResult, timeOut));
+            }
+        } catch (UnsupportedEncodingException e) {
+            throw new SyncException(SyncError.REMOTE_PATH_ENCODING, e);
+        }
+
+        // access the destination file
+        File f = new File(localPath);
+
+        // create the stream to write in the file. We use a new try/catch block to differentiate
+        // between file and network io exceptions.
+        FileOutputStream fos = null;
+        try {
+            fos = new FileOutputStream(f);
+
+            // the buffer to read the data
+            byte[] data = new byte[SYNC_DATA_MAX];
+
+            // loop to get data until we're done.
+            while (true) {
+                // check if we're cancelled
+                if (monitor.isCanceled()) {
+                    throw new SyncException(SyncError.CANCELED);
+                }
+
+                // if we're done, we stop the loop
+                if (checkResult(pullResult, ID_DONE)) {
+                    break;
+                }
+                if (!checkResult(pullResult, ID_DATA)) {
+                    // hmm there's an error
+                    throw new SyncException(SyncError.TRANSFER_PROTOCOL_ERROR,
+                            readErrorMessage(pullResult, timeOut));
+                }
+                int length = ArrayHelper.swap32bitFromArray(pullResult, 4);
+                if (length > SYNC_DATA_MAX) {
+                    // buffer overrun!
+                    // error and exit
+                    throw new SyncException(SyncError.BUFFER_OVERRUN);
+                }
+
+                // now read the length we received
+                AdbHelper.read(mChannel, data, length, timeOut);
+
+                // get the header for the next packet.
+                AdbHelper.read(mChannel, pullResult, -1, timeOut);
+
+                // write the content in the file
+                fos.write(data, 0, length);
+
+                monitor.advance(length);
+            }
+
+            fos.flush();
+        } catch (IOException e) {
+            Log.e("ddms", String.format("Failed to open local file %s for writing, Reason: %s",
+                    f.getAbsolutePath(), e.toString()));
+            throw new SyncException(SyncError.FILE_WRITE_ERROR);
+        } finally {
+            if (fos != null) {
+                fos.close();
+            }
+        }
+    }
+
+
+    /**
+     * Push multiple files
+     * @param fileArray
+     * @param remotePath
+     * @param monitor
+     *
+     * @throws SyncException if file could not be pushed
+     * @throws IOException in case of I/O error on the connection.
+     * @throws TimeoutException in case of a timeout reading responses from the device.
+     */
+    private void doPush(File[] fileArray, String remotePath, ISyncProgressMonitor monitor)
+            throws SyncException, IOException, TimeoutException {
+        for (File f : fileArray) {
+            // check if we're canceled
+            if (monitor.isCanceled()) {
+                throw new SyncException(SyncError.CANCELED);
+            }
+            if (f.exists()) {
+                if (f.isDirectory()) {
+                    // append the name of the directory to the remote path
+                    String dest = remotePath + "/" + f.getName(); // $NON-NLS-1S
+                    monitor.startSubTask(dest);
+                    doPush(f.listFiles(), dest, monitor);
+
+                    monitor.advance(1);
+                } else if (f.isFile()) {
+                    // append the name of the file to the remote path
+                    String remoteFile = remotePath + "/" + f.getName(); // $NON-NLS-1S
+                    monitor.startSubTask(remoteFile);
+                    doPushFile(f.getAbsolutePath(), remoteFile, monitor);
+                }
+            }
+        }
+    }
+
+    /**
+     * Push a single file
+     * @param localPath the local file to push
+     * @param remotePath the remote file (length max is 1024)
+     * @param monitor the monitor. The monitor must be started already.
+     *
+     * @throws SyncException if file could not be pushed
+     * @throws IOException in case of I/O error on the connection.
+     * @throws TimeoutException in case of a timeout reading responses from the device.
+     */
+    private void doPushFile(String localPath, String remotePath,
+            ISyncProgressMonitor monitor) throws SyncException, IOException, TimeoutException {
+        FileInputStream fis = null;
+        byte[] msg;
+
+        final int timeOut = DdmPreferences.getTimeOut();
+
+        try {
+            byte[] remotePathContent = remotePath.getBytes(AdbHelper.DEFAULT_ENCODING);
+
+            if (remotePathContent.length > REMOTE_PATH_MAX_LENGTH) {
+                throw new SyncException(SyncError.REMOTE_PATH_LENGTH);
+            }
+
+            File f = new File(localPath);
+
+            // create the stream to read the file
+            fis = new FileInputStream(f);
+
+            // create the header for the action
+            msg = createSendFileReq(ID_SEND, remotePathContent, 0644);
+
+            // and send it. We use a custom try/catch block to make the difference between
+            // file and network IO exceptions.
+            AdbHelper.write(mChannel, msg, -1, timeOut);
+
+            System.arraycopy(ID_DATA, 0, getBuffer(), 0, ID_DATA.length);
+
+            // look while there is something to read
+            while (true) {
+                // check if we're canceled
+                if (monitor.isCanceled()) {
+                    throw new SyncException(SyncError.CANCELED);
+                }
+
+                // read up to SYNC_DATA_MAX
+                int readCount = fis.read(getBuffer(), 8, SYNC_DATA_MAX);
+
+                if (readCount == -1) {
+                    // we reached the end of the file
+                    break;
+                }
+
+                // now send the data to the device
+                // first write the amount read
+                ArrayHelper.swap32bitsToArray(readCount, getBuffer(), 4);
+
+                // now write it
+                AdbHelper.write(mChannel, getBuffer(), readCount+8, timeOut);
+
+                // and advance the monitor
+                monitor.advance(readCount);
+            }
+        } catch (UnsupportedEncodingException e) {
+            throw new SyncException(SyncError.REMOTE_PATH_ENCODING, e);
+        } finally {
+            // close the local file
+            if (fis != null) {
+                fis.close();
+            }
+        }
+
+        // create the DONE message
+        long time = System.currentTimeMillis() / 1000;
+        msg = createReq(ID_DONE, (int)time);
+
+        // and send it.
+        AdbHelper.write(mChannel, msg, -1, timeOut);
+
+        // read the result, in a byte array containing 2 ints
+        // (id, size)
+        byte[] result = new byte[8];
+        AdbHelper.read(mChannel, result, -1 /* full length */, timeOut);
+
+        if (!checkResult(result, ID_OKAY)) {
+            throw new SyncException(SyncError.TRANSFER_PROTOCOL_ERROR,
+                    readErrorMessage(result, timeOut));
+        }
+    }
+
+    /**
+     * Reads an error message from the opened {@link #mChannel}.
+     * @param result the current adb result. Must contain both FAIL and the length of the message.
+     * @param timeOut
+     * @return
+     * @throws TimeoutException in case of a timeout reading responses from the device.
+     * @throws IOException
+     */
+    private String readErrorMessage(byte[] result, final int timeOut) throws TimeoutException,
+            IOException {
+        if (checkResult(result, ID_FAIL)) {
+            int len = ArrayHelper.swap32bitFromArray(result, 4);
+
+            if (len > 0) {
+                AdbHelper.read(mChannel, getBuffer(), len, timeOut);
+
+                String message = new String(getBuffer(), 0, len);
+                Log.e("ddms", "transfer error: " + message);
+
+                return message;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns the mode of the remote file.
+     * @param path the remote file
+     * @return an Integer containing the mode if all went well or null
+     *      otherwise
+     * @throws IOException
+     * @throws TimeoutException in case of a timeout reading responses from the device.
+     */
+    private Integer readMode(String path) throws TimeoutException, IOException {
+        // create the stat request message.
+        byte[] msg = createFileReq(ID_STAT, path);
+
+        AdbHelper.write(mChannel, msg, -1 /* full length */, DdmPreferences.getTimeOut());
+
+        // read the result, in a byte array containing 4 ints
+        // (id, mode, size, time)
+        byte[] statResult = new byte[16];
+        AdbHelper.read(mChannel, statResult, -1 /* full length */, DdmPreferences.getTimeOut());
+
+        // check we have the proper data back
+        if (!checkResult(statResult, ID_STAT)) {
+            return null;
+        }
+
+        // we return the mode (2nd int in the array)
+        return ArrayHelper.swap32bitFromArray(statResult, 4);
+    }
+
+    /**
+     * Create a command with a code and an int values
+     * @param command
+     * @param value
+     * @return
+     */
+    private static byte[] createReq(byte[] command, int value) {
+        byte[] array = new byte[8];
+
+        System.arraycopy(command, 0, array, 0, 4);
+        ArrayHelper.swap32bitsToArray(value, array, 4);
+
+        return array;
+    }
+
+    /**
+     * Creates the data array for a stat request.
+     * @param command the 4 byte command (ID_STAT, ID_RECV, ...)
+     * @param path The path of the remote file on which to execute the command
+     * @return the byte[] to send to the device through adb
+     */
+    private static byte[] createFileReq(byte[] command, String path) {
+        byte[] pathContent = null;
+        try {
+            pathContent = path.getBytes(AdbHelper.DEFAULT_ENCODING);
+        } catch (UnsupportedEncodingException e) {
+            return null;
+        }
+
+        return createFileReq(command, pathContent);
+    }
+
+    /**
+     * Creates the data array for a file request. This creates an array with a 4 byte command + the
+     * remote file name.
+     * @param command the 4 byte command (ID_STAT, ID_RECV, ...).
+     * @param path The path, as a byte array, of the remote file on which to
+     *      execute the command.
+     * @return the byte[] to send to the device through adb
+     */
+    private static byte[] createFileReq(byte[] command, byte[] path) {
+        byte[] array = new byte[8 + path.length];
+
+        System.arraycopy(command, 0, array, 0, 4);
+        ArrayHelper.swap32bitsToArray(path.length, array, 4);
+        System.arraycopy(path, 0, array, 8, path.length);
+
+        return array;
+    }
+
+    private static byte[] createSendFileReq(byte[] command, byte[] path, int mode) {
+        // make the mode into a string
+        String modeStr = "," + (mode & 0777); // $NON-NLS-1S
+        byte[] modeContent = null;
+        try {
+            modeContent = modeStr.getBytes(AdbHelper.DEFAULT_ENCODING);
+        } catch (UnsupportedEncodingException e) {
+            return null;
+        }
+
+        byte[] array = new byte[8 + path.length + modeContent.length];
+
+        System.arraycopy(command, 0, array, 0, 4);
+        ArrayHelper.swap32bitsToArray(path.length + modeContent.length, array, 4);
+        System.arraycopy(path, 0, array, 8, path.length);
+        System.arraycopy(modeContent, 0, array, 8 + path.length, modeContent.length);
+
+        return array;
+
+
+    }
+
+    /**
+     * Checks the result array starts with the provided code
+     * @param result The result array to check
+     * @param code The 4 byte code.
+     * @return true if the code matches.
+     */
+    private static boolean checkResult(byte[] result, byte[] code) {
+        return !(result[0] != code[0] ||
+                result[1] != code[1] ||
+                result[2] != code[2] ||
+                result[3] != code[3]);
+
+    }
+
+    private static int getFileType(int mode) {
+        if ((mode & S_ISOCK) == S_ISOCK) {
+            return FileListingService.TYPE_SOCKET;
+        }
+
+        if ((mode & S_IFLNK) == S_IFLNK) {
+            return FileListingService.TYPE_LINK;
+        }
+
+        if ((mode & S_IFREG) == S_IFREG) {
+            return FileListingService.TYPE_FILE;
+        }
+
+        if ((mode & S_IFBLK) == S_IFBLK) {
+            return FileListingService.TYPE_BLOCK;
+        }
+
+        if ((mode & S_IFDIR) == S_IFDIR) {
+            return FileListingService.TYPE_DIRECTORY;
+        }
+
+        if ((mode & S_IFCHR) == S_IFCHR) {
+            return FileListingService.TYPE_CHARACTER;
+        }
+
+        if ((mode & S_IFIFO) == S_IFIFO) {
+            return FileListingService.TYPE_FIFO;
+        }
+
+        return FileListingService.TYPE_OTHER;
+    }
+
+    /**
+     * Retrieve the buffer, allocating if necessary
+     * @return
+     */
+    private byte[] getBuffer() {
+        if (mBuffer == null) {
+            // create the buffer used to read.
+            // we read max SYNC_DATA_MAX, but we need 2 4 bytes at the beginning.
+            mBuffer = new byte[SYNC_DATA_MAX + 8];
+        }
+        return mBuffer;
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/ThreadInfo.java b/ddmlib/src/main/java/com/android/ddmlib/ThreadInfo.java
new file mode 100644
index 0000000..93db931
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/ThreadInfo.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2007 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.ddmlib;
+
+/**
+ * Holds a thread information.
+ */
+public final class ThreadInfo implements IStackTraceInfo {
+    private int mThreadId;
+    private String mThreadName;
+    private int mStatus;
+    private int mTid;
+    private int mUtime;
+    private int mStime;
+    private boolean mIsDaemon;
+    private StackTraceElement[] mTrace;
+    private long mTraceTime;
+
+    // priority?
+    // total CPU used?
+    // method at top of stack?
+
+    /**
+     * Construct with basic identification.
+     */
+    ThreadInfo(int threadId, String threadName) {
+        mThreadId = threadId;
+        mThreadName = threadName;
+
+        mStatus = -1;
+        //mTid = mUtime = mStime = 0;
+        //mIsDaemon = false;
+    }
+
+    /**
+     * Set with the values we get from a THST chunk.
+     */
+    void updateThread(int status, int tid, int utime, int stime, boolean isDaemon) {
+
+        mStatus = status;
+        mTid = tid;
+        mUtime = utime;
+        mStime = stime;
+        mIsDaemon = isDaemon;
+    }
+
+    /**
+     * Sets the stack call of the thread.
+     * @param trace stackcall information.
+     */
+    void setStackCall(StackTraceElement[] trace) {
+        mTrace = trace;
+        mTraceTime = System.currentTimeMillis();
+    }
+
+    /**
+     * Returns the thread's ID.
+     */
+    public int getThreadId() {
+        return mThreadId;
+    }
+
+    /**
+     * Returns the thread's name.
+     */
+    public String getThreadName() {
+        return mThreadName;
+    }
+
+    void setThreadName(String name) {
+        mThreadName = name;
+    }
+
+    /**
+     * Returns the system tid.
+     */
+    public int getTid() {
+        return mTid;
+    }
+
+    /**
+     * Returns the VM thread status.
+     */
+    public int getStatus() {
+        return mStatus;
+    }
+
+    /**
+     * Returns the cumulative user time.
+     */
+    public int getUtime() {
+        return mUtime;
+    }
+
+    /**
+     * Returns the cumulative system time.
+     */
+    public int getStime() {
+        return mStime;
+    }
+
+    /**
+     * Returns whether this is a daemon thread.
+     */
+    public boolean isDaemon() {
+        return mIsDaemon;
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.android.ddmlib.IStackTraceInfo#getStackTrace()
+     */
+    @Override
+    public StackTraceElement[] getStackTrace() {
+        return mTrace;
+    }
+
+    /**
+     * Returns the approximate time of the stacktrace data.
+     * @see #getStackTrace()
+     */
+    public long getStackCallTime() {
+        return mTraceTime;
+    }
+}
+
diff --git a/ddmlib/src/main/java/com/android/ddmlib/TimeoutException.java b/ddmlib/src/main/java/com/android/ddmlib/TimeoutException.java
new file mode 100644
index 0000000..78f5db7
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/TimeoutException.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2010 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.ddmlib;
+
+
+/**
+ * Exception thrown when a connection to Adb failed with a timeout.
+ *
+ */
+public class TimeoutException extends Exception {
+    private static final long serialVersionUID = 1L;
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/log/EventContainer.java b/ddmlib/src/main/java/com/android/ddmlib/log/EventContainer.java
new file mode 100644
index 0000000..ce80005
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/log/EventContainer.java
@@ -0,0 +1,462 @@
+/*
+ * Copyright (C) 2008 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.ddmlib.log;
+
+import com.android.ddmlib.log.LogReceiver.LogEntry;
+
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Represents an event and its data.
+ */
+public class EventContainer {
+
+    /**
+     * Comparison method for {@link EventContainer#testValue(int, Object, com.android.ddmlib.log.EventContainer.CompareMethod)}
+     *
+     */
+    public enum CompareMethod {
+        EQUAL_TO("equals", "=="),
+        LESSER_THAN("less than or equals to", "<="),
+        LESSER_THAN_STRICT("less than", "<"),
+        GREATER_THAN("greater than or equals to", ">="),
+        GREATER_THAN_STRICT("greater than", ">"),
+        BIT_CHECK("bit check", "&");
+
+        private final String mName;
+        private final String mTestString;
+
+        private CompareMethod(String name, String testString) {
+            mName = name;
+            mTestString = testString;
+        }
+
+        /**
+         * Returns the display string.
+         */
+        @Override
+        public String toString() {
+            return mName;
+        }
+
+        /**
+         * Returns a short string representing the comparison.
+         */
+        public String testString() {
+            return mTestString;
+        }
+    }
+
+
+    /**
+     * Type for event data.
+     */
+    public static enum EventValueType {
+        UNKNOWN(0),
+        INT(1),
+        LONG(2),
+        STRING(3),
+        LIST(4),
+        TREE(5);
+
+        private static final Pattern STORAGE_PATTERN = Pattern.compile("^(\\d+)@(.*)$"); //$NON-NLS-1$
+
+        private int mValue;
+
+        /**
+         * Returns a {@link EventValueType} from an integer value, or <code>null</code> if no match
+         * was found.
+         * @param value the integer value.
+         */
+        static EventValueType getEventValueType(int value) {
+            for (EventValueType type : values()) {
+                if (type.mValue == value) {
+                    return type;
+                }
+            }
+
+            return null;
+        }
+
+        /**
+         * Returns a storage string for an {@link Object} of type supported by
+         * {@link EventValueType}.
+         * <p/>
+         * Strings created by this method can be reloaded with
+         * {@link #getObjectFromStorageString(String)}.
+         * <p/>
+         * NOTE: for now, only {@link #STRING}, {@link #INT}, and {@link #LONG} are supported.
+         * @param object the object to "convert" into a storage string.
+         * @return a string storing the object and its type or null if the type was not recognized.
+         */
+        public static String getStorageString(Object object) {
+            if (object instanceof String) {
+                return STRING.mValue + "@" + object; //$NON-NLS-1$
+            } else if (object instanceof Integer) {
+                return INT.mValue + "@" + object.toString(); //$NON-NLS-1$
+            } else if (object instanceof Long) {
+                return LONG.mValue + "@" + object.toString(); //$NON-NLS-1$
+            }
+
+            return null;
+        }
+
+        /**
+         * Creates an {@link Object} from a storage string created with
+         * {@link #getStorageString(Object)}.
+         * @param value the storage string
+         * @return an {@link Object} or null if the string or type were not recognized.
+         */
+        public static Object getObjectFromStorageString(String value) {
+            Matcher m = STORAGE_PATTERN.matcher(value);
+            if (m.matches()) {
+                try {
+                    EventValueType type = getEventValueType(Integer.parseInt(m.group(1)));
+
+                    if (type == null) {
+                        return null;
+                    }
+
+                    switch (type) {
+                        case STRING:
+                            return m.group(2);
+                        case INT:
+                            return Integer.valueOf(m.group(2));
+                        case LONG:
+                            return Long.valueOf(m.group(2));
+                    }
+                } catch (NumberFormatException nfe) {
+                    return null;
+                }
+            }
+
+            return null;
+        }
+
+
+        /**
+         * Returns the integer value of the enum.
+         */
+        public int getValue() {
+            return mValue;
+        }
+
+        @Override
+        public String toString() {
+            return super.toString().toLowerCase(Locale.US);
+        }
+
+        private EventValueType(int value) {
+            mValue = value;
+        }
+    }
+
+    public int mTag;
+    public int pid;    /* generating process's pid */
+    public int tid;    /* generating process's tid */
+    public int sec;    /* seconds since Epoch */
+    public int nsec;   /* nanoseconds */
+
+    private Object mData;
+
+    /**
+     * Creates an {@link EventContainer} from a {@link LogEntry}.
+     * @param entry  the LogEntry from which pid, tid, and time info is copied.
+     * @param tag the event tag value
+     * @param data the data of the EventContainer.
+     */
+    EventContainer(LogEntry entry, int tag, Object data) {
+        getType(data);
+        mTag = tag;
+        mData = data;
+
+        pid = entry.pid;
+        tid = entry.tid;
+        sec = entry.sec;
+        nsec = entry.nsec;
+    }
+
+    /**
+     * Creates an {@link EventContainer} with raw data
+     */
+    EventContainer(int tag, int pid, int tid, int sec, int nsec, Object data) {
+        getType(data);
+        mTag = tag;
+        mData = data;
+
+        this.pid = pid;
+        this.tid = tid;
+        this.sec = sec;
+        this.nsec = nsec;
+    }
+
+    /**
+     * Returns the data as an int.
+     * @throws InvalidTypeException if the data type is not {@link EventValueType#INT}.
+     * @see #getType()
+     */
+    public final Integer getInt() throws InvalidTypeException {
+        if (getType(mData) == EventValueType.INT) {
+            return (Integer)mData;
+        }
+
+        throw new InvalidTypeException();
+    }
+
+    /**
+     * Returns the data as a long.
+     * @throws InvalidTypeException if the data type is not {@link EventValueType#LONG}.
+     * @see #getType()
+     */
+    public final Long getLong() throws InvalidTypeException {
+        if (getType(mData) == EventValueType.LONG) {
+            return (Long)mData;
+        }
+
+        throw new InvalidTypeException();
+    }
+
+    /**
+     * Returns the data as a String.
+     * @throws InvalidTypeException if the data type is not {@link EventValueType#STRING}.
+     * @see #getType()
+     */
+    public final String getString() throws InvalidTypeException {
+        if (getType(mData) == EventValueType.STRING) {
+            return (String)mData;
+        }
+
+        throw new InvalidTypeException();
+    }
+
+    /**
+     * Returns a value by index. The return type is defined by its type.
+     * @param valueIndex the index of the value. If the data is not a list, this is ignored.
+     */
+    public Object getValue(int valueIndex) {
+        return getValue(mData, valueIndex, true);
+    }
+
+    /**
+     * Returns a value by index as a double.
+     * @param valueIndex the index of the value. If the data is not a list, this is ignored.
+     * @throws InvalidTypeException if the data type is not {@link EventValueType#INT},
+     * {@link EventValueType#LONG}, {@link EventValueType#LIST}, or if the item in the
+     * list at index <code>valueIndex</code> is not of type {@link EventValueType#INT} or
+     * {@link EventValueType#LONG}.
+     * @see #getType()
+     */
+    public double getValueAsDouble(int valueIndex) throws InvalidTypeException {
+        return getValueAsDouble(mData, valueIndex, true);
+    }
+
+    /**
+     * Returns a value by index as a String.
+     * @param valueIndex the index of the value. If the data is not a list, this is ignored.
+     * @throws InvalidTypeException if the data type is not {@link EventValueType#INT},
+     * {@link EventValueType#LONG}, {@link EventValueType#STRING}, {@link EventValueType#LIST},
+     * or if the item in the list at index <code>valueIndex</code> is not of type
+     * {@link EventValueType#INT}, {@link EventValueType#LONG}, or {@link EventValueType#STRING}
+     * @see #getType()
+     */
+    public String getValueAsString(int valueIndex) throws InvalidTypeException {
+        return getValueAsString(mData, valueIndex, true);
+    }
+
+    /**
+     * Returns the type of the data.
+     */
+    public EventValueType getType() {
+        return getType(mData);
+    }
+
+    /**
+     * Returns the type of an object.
+     */
+    public final EventValueType getType(Object data) {
+        if (data instanceof Integer) {
+            return EventValueType.INT;
+        } else if (data instanceof Long) {
+            return EventValueType.LONG;
+        } else if (data instanceof String) {
+            return EventValueType.STRING;
+        } else if (data instanceof Object[]) {
+            // loop through the list to see if we have another list
+            Object[] objects = (Object[])data;
+            for (Object obj : objects) {
+                EventValueType type = getType(obj);
+                if (type == EventValueType.LIST || type == EventValueType.TREE) {
+                    return EventValueType.TREE;
+                }
+            }
+            return EventValueType.LIST;
+        }
+
+        return EventValueType.UNKNOWN;
+    }
+
+    /**
+     * Checks that the <code>index</code>-th value of this event against a provided value.
+     * @param index the index of the value to test
+     * @param value the value to test against
+     * @param compareMethod the method of testing
+     * @return true if the test passed.
+     * @throws InvalidTypeException in case of type mismatch between the value to test and the value
+     * to test against, or if the compare method is incompatible with the type of the values.
+     * @see CompareMethod
+     */
+    public boolean testValue(int index, Object value,
+            CompareMethod compareMethod) throws InvalidTypeException {
+        EventValueType type = getType(mData);
+        if (index > 0 && type != EventValueType.LIST) {
+            throw new InvalidTypeException();
+        }
+
+        Object data = mData;
+        if (type == EventValueType.LIST) {
+            data = ((Object[])mData)[index];
+        }
+
+        if (!data.getClass().equals(data.getClass())) {
+            throw new InvalidTypeException();
+        }
+
+        switch (compareMethod) {
+            case EQUAL_TO:
+                return data.equals(value);
+            case LESSER_THAN:
+                if (data instanceof Integer) {
+                    return (((Integer)data).compareTo((Integer)value) <= 0);
+                } else if (data instanceof Long) {
+                    return (((Long)data).compareTo((Long)value) <= 0);
+                }
+
+                // other types can't use this compare method.
+                throw new InvalidTypeException();
+            case LESSER_THAN_STRICT:
+                if (data instanceof Integer) {
+                    return (((Integer)data).compareTo((Integer)value) < 0);
+                } else if (data instanceof Long) {
+                    return (((Long)data).compareTo((Long)value) < 0);
+                }
+
+                // other types can't use this compare method.
+                throw new InvalidTypeException();
+            case GREATER_THAN:
+                if (data instanceof Integer) {
+                    return (((Integer)data).compareTo((Integer)value) >= 0);
+                } else if (data instanceof Long) {
+                    return (((Long)data).compareTo((Long)value) >= 0);
+                }
+
+                // other types can't use this compare method.
+                throw new InvalidTypeException();
+            case GREATER_THAN_STRICT:
+                if (data instanceof Integer) {
+                    return (((Integer)data).compareTo((Integer)value) > 0);
+                } else if (data instanceof Long) {
+                    return (((Long)data).compareTo((Long)value) > 0);
+                }
+
+                // other types can't use this compare method.
+                throw new InvalidTypeException();
+            case BIT_CHECK:
+                if (data instanceof Integer) {
+                    return ((Integer) data & (Integer) value) != 0;
+                } else if (data instanceof Long) {
+                    return ((Long) data & (Long) value) != 0;
+                }
+
+                // other types can't use this compare method.
+                throw new InvalidTypeException();
+            default :
+                throw new InvalidTypeException();
+        }
+    }
+
+    private final Object getValue(Object data, int valueIndex, boolean recursive) {
+        EventValueType type = getType(data);
+
+        switch (type) {
+            case INT:
+            case LONG:
+            case STRING:
+                return data;
+            case LIST:
+                if (recursive) {
+                    Object[] list = (Object[]) data;
+                    if (valueIndex >= 0 && valueIndex < list.length) {
+                        return getValue(list[valueIndex], valueIndex, false);
+                    }
+                }
+        }
+
+        return null;
+    }
+
+    private final double getValueAsDouble(Object data, int valueIndex, boolean recursive)
+            throws InvalidTypeException {
+        EventValueType type = getType(data);
+
+        switch (type) {
+            case INT:
+                return ((Integer)data).doubleValue();
+            case LONG:
+                return ((Long)data).doubleValue();
+            case STRING:
+                throw new InvalidTypeException();
+            case LIST:
+                if (recursive) {
+                    Object[] list = (Object[]) data;
+                    if (valueIndex >= 0 && valueIndex < list.length) {
+                        return getValueAsDouble(list[valueIndex], valueIndex, false);
+                    }
+                }
+        }
+
+        throw new InvalidTypeException();
+    }
+
+    private final String getValueAsString(Object data, int valueIndex, boolean recursive)
+            throws InvalidTypeException {
+        EventValueType type = getType(data);
+
+        switch (type) {
+            case INT:
+                return data.toString();
+            case LONG:
+                return data.toString();
+            case STRING:
+                return (String)data;
+            case LIST:
+                if (recursive) {
+                    Object[] list = (Object[]) data;
+                    if (valueIndex >= 0 && valueIndex < list.length) {
+                        return getValueAsString(list[valueIndex], valueIndex, false);
+                    }
+                } else {
+                    throw new InvalidTypeException(
+                            "getValueAsString() doesn't support EventValueType.TREE");
+                }
+        }
+
+        throw new InvalidTypeException(
+                "getValueAsString() unsupported type:" + type);
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/log/EventLogParser.java b/ddmlib/src/main/java/com/android/ddmlib/log/EventLogParser.java
new file mode 100644
index 0000000..568c1be
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/log/EventLogParser.java
@@ -0,0 +1,588 @@
+/*
+ * Copyright (C) 2008 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.ddmlib.log;
+
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.MultiLineReceiver;
+import com.android.ddmlib.log.EventContainer.EventValueType;
+import com.android.ddmlib.log.EventValueDescription.ValueType;
+import com.android.ddmlib.log.LogReceiver.LogEntry;
+import com.android.ddmlib.utils.ArrayHelper;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Parser for the "event" log.
+ */
+public final class EventLogParser {
+
+    /** Location of the tag map file on the device */
+    private static final String EVENT_TAG_MAP_FILE = "/system/etc/event-log-tags"; //$NON-NLS-1$
+
+    /**
+     * Event log entry types.  These must match up with the declarations in
+     * java/android/android/util/EventLog.java.
+     */
+    private static final int EVENT_TYPE_INT      = 0;
+    private static final int EVENT_TYPE_LONG     = 1;
+    private static final int EVENT_TYPE_STRING   = 2;
+    private static final int EVENT_TYPE_LIST     = 3;
+
+    private static final Pattern PATTERN_SIMPLE_TAG = Pattern.compile(
+    "^(\\d+)\\s+([A-Za-z0-9_]+)\\s*$"); //$NON-NLS-1$
+    private static final Pattern PATTERN_TAG_WITH_DESC = Pattern.compile(
+            "^(\\d+)\\s+([A-Za-z0-9_]+)\\s*(.*)\\s*$"); //$NON-NLS-1$
+    private static final Pattern PATTERN_DESCRIPTION = Pattern.compile(
+            "\\(([A-Za-z0-9_\\s]+)\\|(\\d+)(\\|\\d+){0,1}\\)"); //$NON-NLS-1$
+
+    private static final Pattern TEXT_LOG_LINE = Pattern.compile(
+            "(\\d\\d)-(\\d\\d)\\s(\\d\\d):(\\d\\d):(\\d\\d).(\\d{3})\\s+I/([a-zA-Z0-9_]+)\\s*\\(\\s*(\\d+)\\):\\s+(.*)"); //$NON-NLS-1$
+
+    private final TreeMap<Integer, String> mTagMap = new TreeMap<Integer, String>();
+
+    private final TreeMap<Integer, EventValueDescription[]> mValueDescriptionMap =
+        new TreeMap<Integer, EventValueDescription[]>();
+
+    public EventLogParser() {
+    }
+
+    /**
+     * Inits the parser for a specific Device.
+     * <p/>
+     * This methods reads the event-log-tags located on the device to find out
+     * what tags are being written to the event log and what their format is.
+     * @param device The device.
+     * @return <code>true</code> if success, <code>false</code> if failure or cancellation.
+     */
+    public boolean init(IDevice device) {
+        // read the event tag map file on the device.
+        try {
+            device.executeShellCommand("cat " + EVENT_TAG_MAP_FILE, //$NON-NLS-1$
+                    new MultiLineReceiver() {
+                @Override
+                public void processNewLines(String[] lines) {
+                    for (String line : lines) {
+                        processTagLine(line);
+                    }
+                }
+                @Override
+                public boolean isCancelled() {
+                    return false;
+                }
+            });
+        } catch (Exception e) {
+            // catch all possible exceptions and return false.
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Inits the parser with the content of a tag file.
+     * @param tagFileContent the lines of a tag file.
+     * @return <code>true</code> if success, <code>false</code> if failure.
+     */
+    public boolean init(String[] tagFileContent) {
+        for (String line : tagFileContent) {
+            processTagLine(line);
+        }
+        return true;
+    }
+
+    /**
+     * Inits the parser with a specified event-log-tags file.
+     * @param filePath
+     * @return <code>true</code> if success, <code>false</code> if failure.
+     */
+    public boolean init(String filePath)  {
+        BufferedReader reader = null;
+        try {
+            reader = new BufferedReader(new FileReader(filePath));
+
+            String line = null;
+            do {
+                line = reader.readLine();
+                if (line != null) {
+                    processTagLine(line);
+                }
+            } while (line != null);
+
+            return true;
+        } catch (IOException e) {
+            return false;
+        } finally {
+            try {
+                if (reader != null) {
+                    reader.close();
+                }
+            } catch (IOException e) {
+                // ignore
+            }
+        }
+    }
+
+    /**
+     * Processes a line from the event-log-tags file.
+     * @param line the line to process
+     */
+    private void processTagLine(String line) {
+        // ignore empty lines and comment lines
+        if (!line.isEmpty() && line.charAt(0) != '#') {
+            Matcher m = PATTERN_TAG_WITH_DESC.matcher(line);
+            if (m.matches()) {
+                try {
+                    int value = Integer.parseInt(m.group(1));
+                    String name = m.group(2);
+                    if (name != null && mTagMap.get(value) == null) {
+                        mTagMap.put(value, name);
+                    }
+
+                    // special case for the GC tag. We ignore what is in the file,
+                    // and take what the custom GcEventContainer class tells us.
+                    // This is due to the event encoding several values on 2 longs.
+                    // @see GcEventContainer
+                    if (value == GcEventContainer.GC_EVENT_TAG) {
+                        mValueDescriptionMap.put(value,
+                            GcEventContainer.getValueDescriptions());
+                    } else {
+
+                        String description = m.group(3);
+                        if (description != null && !description.isEmpty()) {
+                            EventValueDescription[] desc =
+                                processDescription(description);
+
+                            if (desc != null) {
+                                mValueDescriptionMap.put(value, desc);
+                            }
+                        }
+                    }
+                } catch (NumberFormatException e) {
+                    // failed to convert the number into a string. just ignore it.
+                }
+            } else {
+                m = PATTERN_SIMPLE_TAG.matcher(line);
+                if (m.matches()) {
+                    int value = Integer.parseInt(m.group(1));
+                    String name = m.group(2);
+                    if (name != null && mTagMap.get(value) == null) {
+                        mTagMap.put(value, name);
+                    }
+                }
+            }
+        }
+    }
+
+    private EventValueDescription[] processDescription(String description) {
+        String[] descriptions = description.split("\\s*,\\s*"); //$NON-NLS-1$
+
+        ArrayList<EventValueDescription> list = new ArrayList<EventValueDescription>();
+
+        for (String desc : descriptions) {
+            Matcher m = PATTERN_DESCRIPTION.matcher(desc);
+            if (m.matches()) {
+                try {
+                    String name = m.group(1);
+
+                    String typeString = m.group(2);
+                    int typeValue = Integer.parseInt(typeString);
+                    EventValueType eventValueType = EventValueType.getEventValueType(typeValue);
+                    if (eventValueType == null) {
+                        // just ignore this description if the value is not recognized.
+                        // TODO: log the error.
+                    }
+
+                    typeString = m.group(3);
+                    if (typeString != null && !typeString.isEmpty()) {
+                        //skip the |
+                        typeString = typeString.substring(1);
+
+                        typeValue = Integer.parseInt(typeString);
+                        ValueType valueType = ValueType.getValueType(typeValue);
+
+                        list.add(new EventValueDescription(name, eventValueType, valueType));
+                    } else {
+                        list.add(new EventValueDescription(name, eventValueType));
+                    }
+                } catch (NumberFormatException nfe) {
+                    // just ignore this description if one number is malformed.
+                    // TODO: log the error.
+                } catch (InvalidValueTypeException e) {
+                    // just ignore this description if data type and data unit don't match
+                    // TODO: log the error.
+                }
+            } else {
+                Log.e("EventLogParser",  //$NON-NLS-1$
+                    String.format("Can't parse %1$s", description));  //$NON-NLS-1$
+            }
+        }
+
+        if (list.isEmpty()) {
+            return null;
+        }
+
+        return list.toArray(new EventValueDescription[list.size()]);
+
+    }
+
+    public EventContainer parse(LogEntry entry) {
+        if (entry.len < 4) {
+            return null;
+        }
+
+        int inOffset = 0;
+
+        int tagValue = ArrayHelper.swap32bitFromArray(entry.data, inOffset);
+        inOffset += 4;
+
+        String tag = mTagMap.get(tagValue);
+        if (tag == null) {
+            Log.e("EventLogParser", String.format("unknown tag number: %1$d", tagValue));
+        }
+
+        ArrayList<Object> list = new ArrayList<Object>();
+        if (parseBinaryEvent(entry.data, inOffset, list) == -1) {
+            return null;
+        }
+
+        Object data;
+        if (list.size() == 1) {
+            data = list.get(0);
+        } else{
+            data = list.toArray();
+        }
+
+        EventContainer event = null;
+        if (tagValue == GcEventContainer.GC_EVENT_TAG) {
+            event = new GcEventContainer(entry, tagValue, data);
+        } else {
+            event = new EventContainer(entry, tagValue, data);
+        }
+
+        return event;
+    }
+
+    public EventContainer parse(String textLogLine) {
+        // line will look like
+        // 04-29 23:16:16.691 I/dvm_gc_info(  427): <data>
+        // where <data> is either
+        // [value1,value2...]
+        // or
+        // value
+        if (textLogLine.isEmpty()) {
+            return null;
+        }
+
+        // parse the header first
+        Matcher m = TEXT_LOG_LINE.matcher(textLogLine);
+        if (m.matches()) {
+            try {
+                int month = Integer.parseInt(m.group(1));
+                int day = Integer.parseInt(m.group(2));
+                int hours = Integer.parseInt(m.group(3));
+                int minutes = Integer.parseInt(m.group(4));
+                int seconds = Integer.parseInt(m.group(5));
+                int milliseconds = Integer.parseInt(m.group(6));
+
+                // convert into seconds since epoch and nano-seconds.
+                Calendar cal = Calendar.getInstance();
+                cal.set(cal.get(Calendar.YEAR), month-1, day, hours, minutes, seconds);
+                int sec = (int)Math.floor(cal.getTimeInMillis()/1000);
+                int nsec = milliseconds * 1000000;
+
+                String tag = m.group(7);
+
+                // get the numerical tag value
+                int tagValue = -1;
+                Set<Entry<Integer, String>> tagSet = mTagMap.entrySet();
+                for (Entry<Integer, String> entry : tagSet) {
+                    if (tag.equals(entry.getValue())) {
+                        tagValue = entry.getKey();
+                        break;
+                    }
+                }
+
+                if (tagValue == -1) {
+                    return null;
+                }
+
+                int pid = Integer.parseInt(m.group(8));
+
+                Object data = parseTextData(m.group(9), tagValue);
+                if (data == null) {
+                    return null;
+                }
+
+                // now we can allocate and return the EventContainer
+                EventContainer event = null;
+                if (tagValue == GcEventContainer.GC_EVENT_TAG) {
+                    event = new GcEventContainer(tagValue, pid, -1 /* tid */, sec, nsec, data);
+                } else {
+                    event = new EventContainer(tagValue, pid, -1 /* tid */, sec, nsec, data);
+                }
+
+                return event;
+            } catch (NumberFormatException e) {
+                return null;
+            }
+        }
+
+        return null;
+    }
+
+    public Map<Integer, String> getTagMap() {
+        return mTagMap;
+    }
+
+    public Map<Integer, EventValueDescription[]> getEventInfoMap() {
+        return mValueDescriptionMap;
+    }
+
+    /**
+     * Recursively convert binary log data to printable form.
+     *
+     * This needs to be recursive because you can have lists of lists.
+     *
+     * If we run out of room, we stop processing immediately.  It's important
+     * for us to check for space on every output element to avoid producing
+     * garbled output.
+     *
+     * Returns the amount read on success, -1 on failure.
+     */
+    private static int parseBinaryEvent(byte[] eventData, int dataOffset, ArrayList<Object> list) {
+
+        if (eventData.length - dataOffset < 1)
+            return -1;
+
+        int offset = dataOffset;
+
+        int type = eventData[offset++];
+
+        //fprintf(stderr, "--- type=%d (rem len=%d)\n", type, eventDataLen);
+
+        switch (type) {
+        case EVENT_TYPE_INT: { /* 32-bit signed int */
+                int ival;
+
+                if (eventData.length - offset < 4)
+                    return -1;
+                ival = ArrayHelper.swap32bitFromArray(eventData, offset);
+                offset += 4;
+
+                list.add(ival);
+            }
+            break;
+        case EVENT_TYPE_LONG: { /* 64-bit signed long */
+                long lval;
+
+                if (eventData.length - offset < 8)
+                    return -1;
+                lval = ArrayHelper.swap64bitFromArray(eventData, offset);
+                offset += 8;
+
+                list.add(lval);
+            }
+            break;
+        case EVENT_TYPE_STRING: { /* UTF-8 chars, not NULL-terminated */
+                int strLen;
+
+                if (eventData.length - offset < 4)
+                    return -1;
+                strLen = ArrayHelper.swap32bitFromArray(eventData, offset);
+                offset += 4;
+
+                if (eventData.length - offset < strLen)
+                    return -1;
+
+                // get the string
+                try {
+                    String str = new String(eventData, offset, strLen, "UTF-8"); //$NON-NLS-1$
+                    list.add(str);
+                } catch (UnsupportedEncodingException e) {
+                }
+                offset += strLen;
+                break;
+            }
+        case EVENT_TYPE_LIST: { /* N items, all different types */
+
+                if (eventData.length - offset < 1)
+                    return -1;
+
+                int count = eventData[offset++];
+
+                // make a new temp list
+                ArrayList<Object> subList = new ArrayList<Object>();
+                for (int i = 0; i < count; i++) {
+                    int result = parseBinaryEvent(eventData, offset, subList);
+                    if (result == -1) {
+                        return result;
+                    }
+
+                    offset += result;
+                }
+
+                list.add(subList.toArray());
+            }
+            break;
+        default:
+            Log.e("EventLogParser",  //$NON-NLS-1$
+                    String.format("Unknown binary event type %1$d", type));  //$NON-NLS-1$
+            return -1;
+        }
+
+        return offset - dataOffset;
+    }
+
+    private Object parseTextData(String data, int tagValue) {
+        // first, get the description of what we're supposed to parse
+        EventValueDescription[] desc = mValueDescriptionMap.get(tagValue);
+
+        if (desc == null) {
+            // TODO parse and create string values.
+            return null;
+        }
+
+        if (desc.length == 1) {
+            return getObjectFromString(data, desc[0].getEventValueType());
+        } else if (data.startsWith("[") && data.endsWith("]")) {
+            data = data.substring(1, data.length() - 1);
+
+            // get each individual values as String
+            String[] values = data.split(",");
+
+            if (tagValue == GcEventContainer.GC_EVENT_TAG) {
+                // special case for the GC event!
+                Object[] objects = new Object[2];
+
+                objects[0] = getObjectFromString(values[0], EventValueType.LONG);
+                objects[1] = getObjectFromString(values[1], EventValueType.LONG);
+
+                return objects;
+            } else {
+                // must be the same number as the number of descriptors.
+                if (values.length != desc.length) {
+                    return null;
+                }
+
+                Object[] objects = new Object[values.length];
+
+                for (int i = 0 ; i < desc.length ; i++) {
+                    Object obj = getObjectFromString(values[i], desc[i].getEventValueType());
+                    if (obj == null) {
+                        return null;
+                    }
+                    objects[i] = obj;
+                }
+
+                return objects;
+            }
+        }
+
+        return null;
+    }
+
+
+    private Object getObjectFromString(String value, EventValueType type) {
+        try {
+            switch (type) {
+                case INT:
+                    return Integer.valueOf(value);
+                case LONG:
+                    return Long.valueOf(value);
+                case STRING:
+                    return value;
+            }
+        } catch (NumberFormatException e) {
+            // do nothing, we'll return null.
+        }
+
+        return null;
+    }
+
+    /**
+     * Recreates the event-log-tags at the specified file path.
+     * @param filePath the file path to write the file.
+     * @throws IOException
+     */
+    public void saveTags(String filePath) throws IOException {
+        File destFile = new File(filePath);
+        destFile.createNewFile();
+        FileOutputStream fos = null;
+
+        try {
+
+            fos = new FileOutputStream(destFile);
+
+            for (Integer key : mTagMap.keySet()) {
+                // get the tag name
+                String tagName = mTagMap.get(key);
+
+                // get the value descriptions
+                EventValueDescription[] descriptors = mValueDescriptionMap.get(key);
+
+                String line = null;
+                if (descriptors != null) {
+                    StringBuilder sb = new StringBuilder();
+                    sb.append(String.format("%1$d %2$s", key, tagName)); //$NON-NLS-1$
+                    boolean first = true;
+                    for (EventValueDescription evd : descriptors) {
+                        if (first) {
+                            sb.append(" ("); //$NON-NLS-1$
+                            first = false;
+                        } else {
+                            sb.append(",("); //$NON-NLS-1$
+                        }
+                        sb.append(evd.getName());
+                        sb.append("|"); //$NON-NLS-1$
+                        sb.append(evd.getEventValueType().getValue());
+                        sb.append("|"); //$NON-NLS-1$
+                        sb.append(evd.getValueType().getValue());
+                        sb.append("|)"); //$NON-NLS-1$
+                    }
+                    sb.append("\n"); //$NON-NLS-1$
+
+                    line = sb.toString();
+                } else {
+                    line = String.format("%1$d %2$s\n", key, tagName); //$NON-NLS-1$
+                }
+
+                byte[] buffer = line.getBytes();
+                fos.write(buffer);
+            }
+        } finally {
+            if (fos != null) {
+                fos.close();
+            }
+        }
+    }
+
+
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/log/EventValueDescription.java b/ddmlib/src/main/java/com/android/ddmlib/log/EventValueDescription.java
new file mode 100644
index 0000000..58d147c
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/log/EventValueDescription.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2008 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.ddmlib.log;
+
+import com.android.ddmlib.log.EventContainer.EventValueType;
+
+import java.util.Locale;
+
+
+/**
+ * Describes an {@link EventContainer} value.
+ * <p/>
+ * This is a stand-alone object, not linked to a particular Event. It describes the value, by
+ * name, type ({@link EventValueType}), and (if needed) value unit ({@link ValueType}).
+ * <p/>
+ * The index of the value is not contained within this class, and is instead dependent on the
+ * index of this particular object in the array of {@link EventValueDescription} returned by
+ * {@link EventLogParser#getEventInfoMap()} when queried for a particular event tag.
+ *
+ */
+public final class EventValueDescription {
+
+    /**
+     * Represents the type of a numerical value. This is used to display values of vastly different
+     * type/range in graphs.
+     */
+    public static enum ValueType {
+        NOT_APPLICABLE(0),
+        OBJECTS(1),
+        BYTES(2),
+        MILLISECONDS(3),
+        ALLOCATIONS(4),
+        ID(5),
+        PERCENT(6);
+
+        private int mValue;
+
+        /**
+         * Checks that the {@link EventValueType} is compatible with the {@link ValueType}.
+         * @param type the {@link EventValueType} to check.
+         * @throws InvalidValueTypeException if the types are not compatible.
+         */
+        public void checkType(EventValueType type) throws InvalidValueTypeException {
+            if ((type != EventValueType.INT && type != EventValueType.LONG)
+                    && this != NOT_APPLICABLE) {
+                throw new InvalidValueTypeException(
+                        String.format("%1$s doesn't support type %2$s", type, this));
+            }
+        }
+
+        /**
+         * Returns a {@link ValueType} from an integer value, or <code>null</code> if no match
+         * were found.
+         * @param value the integer value.
+         */
+        public static ValueType getValueType(int value) {
+            for (ValueType type : values()) {
+                if (type.mValue == value) {
+                    return type;
+                }
+            }
+            return null;
+        }
+
+        /**
+         * Returns the integer value of the enum.
+         */
+        public int getValue() {
+            return mValue;
+        }
+
+        @Override
+        public String toString() {
+            return super.toString().toLowerCase(Locale.US);
+        }
+
+        private ValueType(int value) {
+            mValue = value;
+        }
+    }
+
+    private String mName;
+    private EventValueType mEventValueType;
+    private ValueType mValueType;
+
+    /**
+     * Builds a {@link EventValueDescription} with a name and a type.
+     * <p/>
+     * If the type is {@link EventValueType#INT} or {@link EventValueType#LONG}, the
+     * {@link #mValueType} is set to {@link ValueType#BYTES} by default. It set to
+     * {@link ValueType#NOT_APPLICABLE} for all other {@link EventValueType} values.
+     * @param name
+     * @param type
+     */
+    EventValueDescription(String name, EventValueType type) {
+        mName = name;
+        mEventValueType = type;
+        if (mEventValueType == EventValueType.INT || mEventValueType == EventValueType.LONG) {
+            mValueType = ValueType.BYTES;
+        } else {
+            mValueType = ValueType.NOT_APPLICABLE;
+        }
+    }
+
+    /**
+     * Builds a {@link EventValueDescription} with a name and a type, and a {@link ValueType}.
+     * <p/>
+     * @param name
+     * @param type
+     * @param valueType
+     * @throws InvalidValueTypeException if type and valuetype are not compatible.
+     *
+     */
+    EventValueDescription(String name, EventValueType type, ValueType valueType)
+            throws InvalidValueTypeException {
+        mName = name;
+        mEventValueType = type;
+        mValueType = valueType;
+        mValueType.checkType(mEventValueType);
+    }
+
+    /**
+     * @return the Name.
+     */
+    public String getName() {
+        return mName;
+    }
+
+    /**
+     * @return the {@link EventValueType}.
+     */
+    public EventValueType getEventValueType() {
+        return mEventValueType;
+    }
+
+    /**
+     * @return the {@link ValueType}.
+     */
+    public ValueType getValueType() {
+        return mValueType;
+    }
+
+    @Override
+    public String toString() {
+        if (mValueType != ValueType.NOT_APPLICABLE) {
+            return String.format("%1$s (%2$s, %3$s)", mName, mEventValueType.toString(),
+                    mValueType.toString());
+        }
+
+        return String.format("%1$s (%2$s)", mName, mEventValueType.toString());
+    }
+
+    /**
+     * Checks if the value is of the proper type for this receiver.
+     * @param value the value to check.
+     * @return true if the value is of the proper type for this receiver.
+     */
+    public boolean checkForType(Object value) {
+        switch (mEventValueType) {
+            case INT:
+                return value instanceof Integer;
+            case LONG:
+                return value instanceof Long;
+            case STRING:
+                return value instanceof String;
+            case LIST:
+                return value instanceof Object[];
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns an object of a valid type (based on the value returned by
+     * {@link #getEventValueType()}) from a String value.
+     * <p/>
+     * IMPORTANT {@link EventValueType#LIST} and {@link EventValueType#TREE} are not
+     * supported.
+     * @param value the value of the object expressed as a string.
+     * @return an object or null if the conversion could not be done.
+     */
+    public Object getObjectFromString(String value) {
+        switch (mEventValueType) {
+            case INT:
+                try {
+                    return Integer.valueOf(value);
+                } catch (NumberFormatException e) {
+                    return null;
+                }
+            case LONG:
+                try {
+                    return Long.valueOf(value);
+                } catch (NumberFormatException e) {
+                    return null;
+                }
+            case STRING:
+                return value;
+        }
+
+        return null;
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/log/GcEventContainer.java b/ddmlib/src/main/java/com/android/ddmlib/log/GcEventContainer.java
new file mode 100644
index 0000000..859e080
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/log/GcEventContainer.java
@@ -0,0 +1,347 @@
+/*
+ * Copyright (C) 2008 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.ddmlib.log;
+
+import com.android.ddmlib.log.EventValueDescription.ValueType;
+import com.android.ddmlib.log.LogReceiver.LogEntry;
+
+/**
+ * Custom Event Container for the Gc event since this event doesn't simply output data in
+ * int or long format, but encodes several values on 4 longs.
+ * <p/>
+ * The array of {@link EventValueDescription}s parsed from the "event-log-tags" file must
+ * be ignored, and instead, the array returned from {@link #getValueDescriptions()} must be used. 
+ */
+final class GcEventContainer extends EventContainer {
+    
+    public static final int GC_EVENT_TAG = 20001;
+
+    private String processId;
+    private long gcTime;
+    private long bytesFreed;
+    private long objectsFreed;
+    private long actualSize;
+    private long allowedSize;
+    private long softLimit;
+    private long objectsAllocated;
+    private long bytesAllocated;
+    private long zActualSize;
+    private long zAllowedSize;
+    private long zObjectsAllocated;
+    private long zBytesAllocated;
+    private long dlmallocFootprint;
+    private long mallinfoTotalAllocatedSpace;
+    private long externalLimit;
+    private long externalBytesAllocated;
+
+    GcEventContainer(LogEntry entry, int tag, Object data) {
+        super(entry, tag, data);
+        init(data);
+    }
+
+    GcEventContainer(int tag, int pid, int tid, int sec, int nsec, Object data) {
+        super(tag, pid, tid, sec, nsec, data);
+        init(data);
+    }
+
+    /**
+     * @param data
+     */
+    private void init(Object data) {
+        if (data instanceof Object[]) {
+            Object[] values = (Object[])data;
+            for (int i = 0; i < values.length; i++) {
+                if (values[i] instanceof Long) {
+                    parseDvmHeapInfo((Long)values[i], i);
+                }
+            }
+        }
+    }
+    
+    @Override
+    public EventValueType getType() {
+        return EventValueType.LIST;
+    }
+
+    @Override
+    public boolean testValue(int index, Object value, CompareMethod compareMethod)
+            throws InvalidTypeException {
+        // do a quick easy check on the type.
+        if (index == 0) {
+            if (!(value instanceof String)) {
+                throw new InvalidTypeException();
+            }
+        } else if (!(value instanceof Long)) {
+            throw new InvalidTypeException();
+        }
+        
+        switch (compareMethod) {
+            case EQUAL_TO:
+                if (index == 0) {
+                    return processId.equals(value);
+                } else {
+                    return getValueAsLong(index) == (Long) value;
+                }
+            case LESSER_THAN:
+                return getValueAsLong(index) <= (Long) value;
+            case LESSER_THAN_STRICT:
+                return getValueAsLong(index) < (Long) value;
+            case GREATER_THAN:
+                return getValueAsLong(index) >= (Long) value;
+            case GREATER_THAN_STRICT:
+                return getValueAsLong(index) > (Long) value;
+            case BIT_CHECK:
+                return (getValueAsLong(index) & (Long) value) != 0;
+        }
+
+        throw new ArrayIndexOutOfBoundsException();
+    }
+
+    @Override
+    public Object getValue(int valueIndex) {
+        if (valueIndex == 0) {
+            return processId;
+        }
+        
+        try {
+            return getValueAsLong(valueIndex);
+        } catch (InvalidTypeException e) {
+            // this would only happened if valueIndex was 0, which we test above.
+        }
+        
+        return null;
+    }
+
+    @Override
+    public double getValueAsDouble(int valueIndex) throws InvalidTypeException {
+        return (double)getValueAsLong(valueIndex);
+    }
+
+    @Override
+    public String getValueAsString(int valueIndex) {
+        switch (valueIndex) {
+            case 0:
+                return processId;
+            default:
+                try {
+                    return Long.toString(getValueAsLong(valueIndex));
+                } catch (InvalidTypeException e) {
+                    // we shouldn't stop there since we test, in this method first.
+                }
+        }
+
+        throw new ArrayIndexOutOfBoundsException();
+    }
+    
+    /**
+     * Returns a custom array of {@link EventValueDescription} since the actual content of this
+     * event (list of (long, long) does not match the values encoded into those longs.
+     */
+    static EventValueDescription[] getValueDescriptions() {
+        try {
+            return new EventValueDescription[] {
+                    new EventValueDescription("Process Name", EventValueType.STRING),
+                    new EventValueDescription("GC Time", EventValueType.LONG,
+                            ValueType.MILLISECONDS),
+                    new EventValueDescription("Freed Objects", EventValueType.LONG,
+                            ValueType.OBJECTS),
+                    new EventValueDescription("Freed Bytes", EventValueType.LONG, ValueType.BYTES),
+                    new EventValueDescription("Soft Limit", EventValueType.LONG, ValueType.BYTES),
+                    new EventValueDescription("Actual Size (aggregate)", EventValueType.LONG,
+                            ValueType.BYTES),
+                    new EventValueDescription("Allowed Size (aggregate)", EventValueType.LONG,
+                            ValueType.BYTES),
+                    new EventValueDescription("Allocated Objects (aggregate)",
+                            EventValueType.LONG, ValueType.OBJECTS),
+                    new EventValueDescription("Allocated Bytes (aggregate)", EventValueType.LONG,
+                            ValueType.BYTES),
+                    new EventValueDescription("Actual Size", EventValueType.LONG, ValueType.BYTES),
+                    new EventValueDescription("Allowed Size", EventValueType.LONG, ValueType.BYTES),
+                    new EventValueDescription("Allocated Objects", EventValueType.LONG,
+                            ValueType.OBJECTS),
+                    new EventValueDescription("Allocated Bytes", EventValueType.LONG,
+                            ValueType.BYTES),
+                    new EventValueDescription("Actual Size (zygote)", EventValueType.LONG,
+                            ValueType.BYTES),
+                    new EventValueDescription("Allowed Size (zygote)", EventValueType.LONG,
+                            ValueType.BYTES),
+                    new EventValueDescription("Allocated Objects (zygote)", EventValueType.LONG,
+                            ValueType.OBJECTS),
+                    new EventValueDescription("Allocated Bytes (zygote)", EventValueType.LONG,
+                            ValueType.BYTES),
+                    new EventValueDescription("External Allocation Limit", EventValueType.LONG,
+                            ValueType.BYTES),
+                    new EventValueDescription("External Bytes Allocated", EventValueType.LONG,
+                            ValueType.BYTES),
+                    new EventValueDescription("dlmalloc Footprint", EventValueType.LONG,
+                            ValueType.BYTES),
+                    new EventValueDescription("Malloc Info: Total Allocated Space",
+                            EventValueType.LONG, ValueType.BYTES),
+                  };
+        } catch (InvalidValueTypeException e) {
+            // this shouldn't happen since we control manual the EventValueType and the ValueType
+            // values. For development purpose, we assert if this happens.
+            assert false;
+        }
+
+        // this shouldn't happen, but the compiler complains otherwise.
+        return null;
+    }
+
+    private void parseDvmHeapInfo(long data, int index) {
+        switch (index) {
+            case 0:
+                //    [63   ] Must be zero
+                //    [62-24] ASCII process identifier
+                //    [23-12] GC time in ms
+                //    [11- 0] Bytes freed
+                
+                gcTime = float12ToInt((int)((data >> 12) & 0xFFFL));
+                bytesFreed = float12ToInt((int)(data & 0xFFFL));
+                
+                // convert the long into an array, in the proper order so that we can convert the
+                // first 5 char into a string.
+                byte[] dataArray = new byte[8];
+                put64bitsToArray(data, dataArray, 0);
+                
+                // get the name from the string
+                processId = new String(dataArray, 0, 5);
+                break;
+            case 1:
+                //    [63-62] 10
+                //    [61-60] Reserved; must be zero
+                //    [59-48] Objects freed
+                //    [47-36] Actual size (current footprint)
+                //    [35-24] Allowed size (current hard max)
+                //    [23-12] Objects allocated
+                //    [11- 0] Bytes allocated
+                objectsFreed = float12ToInt((int)((data >> 48) & 0xFFFL));
+                actualSize = float12ToInt((int)((data >> 36) & 0xFFFL));
+                allowedSize = float12ToInt((int)((data >> 24) & 0xFFFL));
+                objectsAllocated = float12ToInt((int)((data >> 12) & 0xFFFL));
+                bytesAllocated = float12ToInt((int)(data & 0xFFFL));
+                break;
+            case 2:
+                //    [63-62] 11
+                //    [61-60] Reserved; must be zero
+                //    [59-48] Soft limit (current soft max)
+                //    [47-36] Actual size (current footprint)
+                //    [35-24] Allowed size (current hard max)
+                //    [23-12] Objects allocated
+                //    [11- 0] Bytes allocated
+                softLimit = float12ToInt((int)((data >> 48) & 0xFFFL));
+                zActualSize = float12ToInt((int)((data >> 36) & 0xFFFL));
+                zAllowedSize = float12ToInt((int)((data >> 24) & 0xFFFL));
+                zObjectsAllocated = float12ToInt((int)((data >> 12) & 0xFFFL));
+                zBytesAllocated = float12ToInt((int)(data & 0xFFFL));
+                break;
+            case 3:
+                //    [63-48] Reserved; must be zero
+                //    [47-36] dlmallocFootprint
+                //    [35-24] mallinfo: total allocated space
+                //    [23-12] External byte limit
+                //    [11- 0] External bytes allocated
+                dlmallocFootprint = float12ToInt((int)((data >> 36) & 0xFFFL));
+                mallinfoTotalAllocatedSpace = float12ToInt((int)((data >> 24) & 0xFFFL));
+                externalLimit = float12ToInt((int)((data >> 12) & 0xFFFL));
+                externalBytesAllocated = float12ToInt((int)(data & 0xFFFL));
+                break;
+            default:
+                break;
+        }
+    }
+    
+    /**
+     * Converts a 12 bit float representation into an unsigned int (returned as a long)
+     * @param f12
+     */
+    private static long float12ToInt(int f12) {
+        return (f12 & 0x1FF) << ((f12 >>> 9) * 4);
+    }
+    
+    /**
+     * puts an unsigned value in an array.
+     * @param value The value to put.
+     * @param dest the destination array
+     * @param offset the offset in the array where to put the value.
+     *      Array length must be at least offset + 8
+     */
+    private static void put64bitsToArray(long value, byte[] dest, int offset) {
+        dest[offset + 7] = (byte)(value & 0x00000000000000FFL);
+        dest[offset + 6] = (byte)((value & 0x000000000000FF00L) >> 8);
+        dest[offset + 5] = (byte)((value & 0x0000000000FF0000L) >> 16);
+        dest[offset + 4] = (byte)((value & 0x00000000FF000000L) >> 24);
+        dest[offset + 3] = (byte)((value & 0x000000FF00000000L) >> 32);
+        dest[offset + 2] = (byte)((value & 0x0000FF0000000000L) >> 40);
+        dest[offset + 1] = (byte)((value & 0x00FF000000000000L) >> 48);
+        dest[offset + 0] = (byte)((value & 0xFF00000000000000L) >> 56);
+    }
+    
+    /**
+     * Returns the long value of the <code>valueIndex</code>-th value.
+     * @param valueIndex the index of the value.
+     * @throws InvalidTypeException if index is 0 as it is a string value.
+     */
+    private final long getValueAsLong(int valueIndex) throws InvalidTypeException {
+        switch (valueIndex) {
+            case 0:
+                throw new InvalidTypeException();
+            case 1:
+                return gcTime;
+            case 2:
+                return objectsFreed;
+            case 3:
+                return bytesFreed;
+            case 4:
+                return softLimit;
+            case 5:
+                return actualSize;
+            case 6:
+                return allowedSize;
+            case 7:
+                return objectsAllocated;
+            case 8:
+                return bytesAllocated;
+            case 9:
+                return actualSize - zActualSize;
+            case 10:
+                return allowedSize - zAllowedSize;
+            case 11:
+                return objectsAllocated - zObjectsAllocated;
+            case 12:
+                return bytesAllocated - zBytesAllocated;
+            case 13:
+               return zActualSize;
+            case 14:
+                return zAllowedSize;
+            case 15:
+                return zObjectsAllocated;
+            case 16:
+                return zBytesAllocated;
+            case 17:
+                return externalLimit;
+            case 18:
+                return externalBytesAllocated;
+            case 19:
+                return dlmallocFootprint;
+            case 20:
+                return mallinfoTotalAllocatedSpace;
+        }
+
+        throw new ArrayIndexOutOfBoundsException();
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/log/InvalidTypeException.java b/ddmlib/src/main/java/com/android/ddmlib/log/InvalidTypeException.java
new file mode 100644
index 0000000..016f8aa
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/log/InvalidTypeException.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2008 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.ddmlib.log;
+
+import java.io.Serializable;
+
+/**
+ * Exception thrown when accessing an {@link EventContainer} value with the wrong type.
+ */
+public final class InvalidTypeException extends Exception {
+
+    /**
+     * Needed by {@link Serializable}.
+     */
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Constructs a new exception with the default detail message.
+     * @see java.lang.Exception
+     */
+    public InvalidTypeException() {
+        super("Invalid Type");
+    }
+
+    /**
+     * Constructs a new exception with the specified detail message.
+     * @param message the detail message. The detail message is saved for later retrieval
+     * by the {@link Throwable#getMessage()} method.
+     * @see java.lang.Exception
+     */
+    public InvalidTypeException(String message) {
+        super(message);
+    }
+
+    /**
+     * Constructs a new exception with the specified cause and a detail message of
+     * <code>(cause==null ? null : cause.toString())</code> (which typically contains
+     * the class and detail message of cause).
+     * @param cause the cause (which is saved for later retrieval by the
+     * {@link Throwable#getCause()} method). (A <code>null</code> value is permitted,
+     * and indicates that the cause is nonexistent or unknown.)
+     * @see java.lang.Exception
+     */
+    public InvalidTypeException(Throwable cause) {
+        super(cause);
+    }
+
+    /**
+     * Constructs a new exception with the specified detail message and cause.
+     * @param message the detail message. The detail message is saved for later retrieval
+     * by the {@link Throwable#getMessage()} method.
+     * @param cause the cause (which is saved for later retrieval by the
+     * {@link Throwable#getCause()} method). (A <code>null</code> value is permitted,
+     * and indicates that the cause is nonexistent or unknown.)
+     * @see java.lang.Exception
+     */
+    public InvalidTypeException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/log/InvalidValueTypeException.java b/ddmlib/src/main/java/com/android/ddmlib/log/InvalidValueTypeException.java
new file mode 100644
index 0000000..a3050c8
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/log/InvalidValueTypeException.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2008 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.ddmlib.log;
+
+import com.android.ddmlib.log.EventContainer.EventValueType;
+import com.android.ddmlib.log.EventValueDescription.ValueType;
+
+import java.io.Serializable;
+
+/**
+ * Exception thrown when associating an {@link EventValueType} with an incompatible
+ * {@link ValueType}.
+ */
+public final class InvalidValueTypeException extends Exception {
+
+    /**
+     * Needed by {@link Serializable}.
+     */
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Constructs a new exception with the default detail message.
+     * @see java.lang.Exception
+     */
+    public InvalidValueTypeException() {
+        super("Invalid Type");
+    }
+
+    /**
+     * Constructs a new exception with the specified detail message.
+     * @param message the detail message. The detail message is saved for later retrieval
+     * by the {@link Throwable#getMessage()} method.
+     * @see java.lang.Exception
+     */
+    public InvalidValueTypeException(String message) {
+        super(message);
+    }
+
+    /**
+     * Constructs a new exception with the specified cause and a detail message of
+     * <code>(cause==null ? null : cause.toString())</code> (which typically contains
+     * the class and detail message of cause).
+     * @param cause the cause (which is saved for later retrieval by the
+     * {@link Throwable#getCause()} method). (A <code>null</code> value is permitted,
+     * and indicates that the cause is nonexistent or unknown.)
+     * @see java.lang.Exception
+     */
+    public InvalidValueTypeException(Throwable cause) {
+        super(cause);
+    }
+
+    /**
+     * Constructs a new exception with the specified detail message and cause.
+     * @param message the detail message. The detail message is saved for later retrieval
+     * by the {@link Throwable#getMessage()} method.
+     * @param cause the cause (which is saved for later retrieval by the
+     * {@link Throwable#getCause()} method). (A <code>null</code> value is permitted,
+     * and indicates that the cause is nonexistent or unknown.)
+     * @see java.lang.Exception
+     */
+    public InvalidValueTypeException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/log/LogReceiver.java b/ddmlib/src/main/java/com/android/ddmlib/log/LogReceiver.java
new file mode 100644
index 0000000..195eec6
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/log/LogReceiver.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2008 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.ddmlib.log;
+
+
+import com.android.ddmlib.utils.ArrayHelper;
+
+import java.security.InvalidParameterException;
+
+/**
+ * Receiver able to provide low level parsing for device-side log services.
+ */
+public final class LogReceiver {
+
+    private static final int ENTRY_HEADER_SIZE = 20; // 2*2 + 4*4; see LogEntry.
+
+    /**
+     * Represents a log entry and its raw data.
+     */
+    public static final class LogEntry {
+        /*
+         * See //device/include/utils/logger.h
+         */
+        /** 16bit unsigned: length of the payload. */
+        public int  len; /* This is normally followed by a 16 bit padding */
+        /** pid of the process that generated this {@link LogEntry} */
+        public int   pid;
+        /** tid of the process that generated this {@link LogEntry} */
+        public int   tid;
+        /** Seconds since epoch. */
+        public int   sec;
+        /** nanoseconds. */
+        public int   nsec;
+        /** The entry's raw data. */
+        public byte[] data;
+    }
+
+    /**
+     * Classes which implement this interface provide a method that deals
+     * with {@link LogEntry} objects coming from log service through a {@link LogReceiver}.
+     * <p/>This interface provides two methods.
+     * <ul>
+     * <li>{@link #newEntry(com.android.ddmlib.log.LogReceiver.LogEntry)} provides a
+     * first level of parsing, extracting {@link LogEntry} objects out of the log service output.</li>
+     * <li>{@link #newData(byte[], int, int)} provides a way to receive the raw information
+     * coming directly from the log service.</li>
+     * </ul>
+     */
+    public interface ILogListener {
+        /**
+         * Sent when a new {@link LogEntry} has been parsed by the {@link LogReceiver}.
+         * @param entry the new log entry.
+         */
+        public void newEntry(LogEntry entry);
+        
+        /**
+         * Sent when new raw data is coming from the log service.
+         * @param data the raw data buffer.
+         * @param offset the offset into the buffer signaling the beginning of the new data.
+         * @param length the length of the new data.
+         */
+        public void newData(byte[] data, int offset, int length);
+    }
+
+    /** Current {@link LogEntry} being read, before sending it to the listener. */
+    private LogEntry mCurrentEntry;
+
+    /** Temp buffer to store partial entry headers. */
+    private byte[] mEntryHeaderBuffer = new byte[ENTRY_HEADER_SIZE];
+    /** Offset in the partial header buffer */
+    private int mEntryHeaderOffset = 0;
+    /** Offset in the partial entry data */
+    private int mEntryDataOffset = 0;
+    
+    /** Listener waiting for receive fully read {@link LogEntry} objects */
+    private ILogListener mListener;
+
+    private boolean mIsCancelled = false;
+    
+    /**
+     * Creates a {@link LogReceiver} with an {@link ILogListener}.
+     * <p/>
+     * The {@link ILogListener} will receive new log entries as they are parsed, in the form 
+     * of {@link LogEntry} objects.
+     * @param listener the listener to receive new log entries.
+     */
+    public LogReceiver(ILogListener listener) {
+        mListener = listener;
+    }
+    
+
+    /**
+     * Parses new data coming from the log service.
+     * @param data the data buffer
+     * @param offset the offset into the buffer signaling the beginning of the new data.
+     * @param length the length of the new data.
+     */
+    public void parseNewData(byte[] data, int offset, int length) {
+        // notify the listener of new raw data
+        if (mListener != null) {
+            mListener.newData(data, offset, length);
+        }
+
+        // loop while there is still data to be read and the receiver has not be cancelled.
+        while (length > 0 && !mIsCancelled) {
+            // first check if we have no current entry.
+            if (mCurrentEntry == null) {
+                if (mEntryHeaderOffset + length < ENTRY_HEADER_SIZE) {
+                    // if we don't have enough data to finish the header, save
+                    // the data we have and return
+                    System.arraycopy(data, offset, mEntryHeaderBuffer, mEntryHeaderOffset, length);
+                    mEntryHeaderOffset += length;
+                    return;
+                } else {
+                    // we have enough to fill the header, let's do it.
+                    // did we store some part at the beginning of the header?
+                    if (mEntryHeaderOffset != 0) {
+                        // copy the rest of the entry header into the header buffer
+                        int size = ENTRY_HEADER_SIZE - mEntryHeaderOffset; 
+                        System.arraycopy(data, offset, mEntryHeaderBuffer, mEntryHeaderOffset,
+                                size);
+                        
+                        // create the entry from the header buffer
+                        mCurrentEntry = createEntry(mEntryHeaderBuffer, 0);
+    
+                        // since we used the whole entry header buffer, we reset  the offset
+                        mEntryHeaderOffset = 0;
+                        
+                        // adjust current offset and remaining length to the beginning
+                        // of the entry data
+                        offset += size;
+                        length -= size;
+                    } else {
+                        // create the entry directly from the data array
+                        mCurrentEntry = createEntry(data, offset);
+                        
+                        // adjust current offset and remaining length to the beginning
+                        // of the entry data
+                        offset += ENTRY_HEADER_SIZE;
+                        length -= ENTRY_HEADER_SIZE;
+                    }
+                }
+            }
+            
+            // at this point, we have an entry, and offset/length have been updated to skip
+            // the entry header.
+    
+            // if we have enough data for this entry or more, we'll need to end this entry
+            if (length >= mCurrentEntry.len - mEntryDataOffset) {
+                // compute and save the size of the data that we have to read for this entry,
+                // based on how much we may already have read.
+                int dataSize = mCurrentEntry.len - mEntryDataOffset;  
+    
+                // we only read what we need, and put it in the entry buffer.
+                System.arraycopy(data, offset, mCurrentEntry.data, mEntryDataOffset, dataSize);
+                
+                // notify the listener of a new entry
+                if (mListener != null) {
+                    mListener.newEntry(mCurrentEntry);
+                }
+    
+                // reset some flags: we have read 0 data of the current entry.
+                // and we have no current entry being read.
+                mEntryDataOffset = 0;
+                mCurrentEntry = null;
+                
+                // and update the data buffer info to the end of the current entry / start
+                // of the next one.
+                offset += dataSize;
+                length -= dataSize;
+            } else {
+                // we don't have enough data to fill this entry, so we store what we have
+                // in the entry itself.
+                System.arraycopy(data, offset, mCurrentEntry.data, mEntryDataOffset, length);
+                
+                // save the amount read for the data.
+                mEntryDataOffset += length;
+                return;
+            }
+        }
+    }
+
+    /**
+     * Returns whether this receiver is canceling the remote service.
+     */
+    public boolean isCancelled() {
+        return mIsCancelled;
+    }
+    
+    /**
+     * Cancels the current remote service.
+     */
+    public void cancel() {
+        mIsCancelled = true;
+    }
+    
+    /**
+     * Creates a {@link LogEntry} from the array of bytes. This expects the data buffer size
+     * to be at least <code>offset + {@link #ENTRY_HEADER_SIZE}</code>.
+     * @param data the data buffer the entry is read from.
+     * @param offset the offset of the first byte from the buffer representing the entry.
+     * @return a new {@link LogEntry} or <code>null</code> if some error happened.
+     */
+    private LogEntry createEntry(byte[] data, int offset) {
+        if (data.length < offset + ENTRY_HEADER_SIZE) {
+            throw new InvalidParameterException(
+                    "Buffer not big enough to hold full LoggerEntry header");
+        }
+
+        // create the new entry and fill it.
+        LogEntry entry = new LogEntry();
+        entry.len = ArrayHelper.swapU16bitFromArray(data, offset);
+        
+        // we've read only 16 bits, but since there's also a 16 bit padding,
+        // we can skip right over both.
+        offset += 4;
+        
+        entry.pid = ArrayHelper.swap32bitFromArray(data, offset);
+        offset += 4;
+        entry.tid = ArrayHelper.swap32bitFromArray(data, offset);
+        offset += 4;
+        entry.sec = ArrayHelper.swap32bitFromArray(data, offset);
+        offset += 4;
+        entry.nsec = ArrayHelper.swap32bitFromArray(data, offset);
+        offset += 4;
+        
+        // allocate the data
+        entry.data = new byte[entry.len];
+        
+        return entry;
+    }
+    
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatFilter.java b/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatFilter.java
new file mode 100644
index 0000000..34fdc38
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatFilter.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ddmlib.logcat;
+
+import com.android.annotations.NonNull;
+import com.android.ddmlib.Log.LogLevel;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * A Filter for logcat messages. A filter can be constructed to match
+ * different fields of a logcat message. It can then be queried to see if
+ * a message matches the filter's settings.
+ */
+public final class LogCatFilter {
+    private static final String PID_KEYWORD = "pid:";   //$NON-NLS-1$
+    private static final String APP_KEYWORD = "app:";   //$NON-NLS-1$
+    private static final String TAG_KEYWORD = "tag:";   //$NON-NLS-1$
+    private static final String TEXT_KEYWORD = "text:"; //$NON-NLS-1$
+
+    private final String mName;
+    private final String mTag;
+    private final String mText;
+    private final String mPid;
+    private final String mAppName;
+    private final LogLevel mLogLevel;
+
+    private boolean mCheckPid;
+    private boolean mCheckAppName;
+    private boolean mCheckTag;
+    private boolean mCheckText;
+
+    private Pattern mAppNamePattern;
+    private Pattern mTagPattern;
+    private Pattern mTextPattern;
+
+    /**
+     * Construct a filter with the provided restrictions for the logcat message. All the text
+     * fields accept Java regexes as input, but ignore invalid regexes.
+     * @param name name for the filter
+     * @param tag value for the logcat message's tag field.
+     * @param text value for the logcat message's text field.
+     * @param pid value for the logcat message's pid field.
+     * @param appName value for the logcat message's app name field.
+     * @param logLevel value for the logcat message's log level. Only messages of
+     * higher priority will be accepted by the filter.
+     */
+    public LogCatFilter(@NonNull String name, @NonNull String tag, @NonNull String text,
+            @NonNull String pid, @NonNull String appName, @NonNull LogLevel logLevel) {
+        mName = name.trim();
+        mTag = tag.trim();
+        mText = text.trim();
+        mPid = pid.trim();
+        mAppName = appName.trim();
+        mLogLevel = logLevel;
+
+        mCheckPid = !mPid.isEmpty();
+
+        if (!mAppName.isEmpty()) {
+            try {
+                mAppNamePattern = Pattern.compile(mAppName, getPatternCompileFlags(mAppName));
+                mCheckAppName = true;
+            } catch (PatternSyntaxException e) {
+                mCheckAppName = false;
+            }
+        }
+
+        if (!mTag.isEmpty()) {
+            try {
+                mTagPattern = Pattern.compile(mTag, getPatternCompileFlags(mTag));
+                mCheckTag = true;
+            } catch (PatternSyntaxException e) {
+                mCheckTag = false;
+            }
+        }
+
+        if (!mText.isEmpty()) {
+            try {
+                mTextPattern = Pattern.compile(mText, getPatternCompileFlags(mText));
+                mCheckText = true;
+            } catch (PatternSyntaxException e) {
+                mCheckText = false;
+            }
+        }
+    }
+
+    /**
+     * Obtain the flags to pass to {@link Pattern#compile(String, int)}. This method
+     * tries to figure out whether case sensitive matching should be used. It is based on
+     * the following heuristic: if the regex has an upper case character, then the match
+     * will be case sensitive. Otherwise it will be case insensitive.
+     */
+    private int getPatternCompileFlags(String regex) {
+        for (char c : regex.toCharArray()) {
+            if (Character.isUpperCase(c)) {
+                return 0;
+            }
+        }
+
+        return Pattern.CASE_INSENSITIVE;
+    }
+
+    /**
+     * Construct a list of {@link LogCatFilter} objects by decoding the query.
+     * @param query encoded search string. The query is simply a list of words (can be regexes)
+     * a user would type in a search bar. These words are searched for in the text field of
+     * each collected logcat message. To search in a different field, the word could be prefixed
+     * with a keyword corresponding to the field name. Currently, the following keywords are
+     * supported: "pid:", "tag:" and "text:". Invalid regexes are ignored.
+     * @param minLevel minimum log level to match
+     * @return list of filter settings that fully match the given query
+     */
+    public static List<LogCatFilter> fromString(String query, LogLevel minLevel) {
+        List<LogCatFilter> filterSettings = new ArrayList<LogCatFilter>();
+
+        for (String s : query.trim().split(" ")) {
+            String tag = "";
+            String text = "";
+            String pid = "";
+            String app = "";
+
+            if (s.startsWith(PID_KEYWORD)) {
+                pid = s.substring(PID_KEYWORD.length());
+            } else if (s.startsWith(APP_KEYWORD)) {
+                app = s.substring(APP_KEYWORD.length());
+            } else if (s.startsWith(TAG_KEYWORD)) {
+                tag = s.substring(TAG_KEYWORD.length());
+            } else {
+                if (s.startsWith(TEXT_KEYWORD)) {
+                    text = s.substring(TEXT_KEYWORD.length());
+                } else {
+                    text = s;
+                }
+            }
+            filterSettings.add(new LogCatFilter("livefilter-" + s,
+                    tag, text, pid, app, minLevel));
+        }
+
+        return filterSettings;
+    }
+
+    @NonNull
+    public String getName() {
+        return mName;
+    }
+
+    @NonNull
+    public String getTag() {
+        return mTag;
+    }
+
+    @NonNull
+    public String getText() {
+        return mText;
+    }
+
+    @NonNull
+    public String getPid() {
+        return mPid;
+    }
+
+    @NonNull
+    public String getAppName() {
+        return mAppName;
+    }
+
+    @NonNull
+    public LogLevel getLogLevel() {
+        return mLogLevel;
+    }
+
+    /**
+     * Check whether a given message will make it through this filter.
+     * @param m message to check
+     * @return true if the message matches the filter's conditions.
+     */
+    public boolean matches(LogCatMessage m) {
+        /* filter out messages of a lower priority */
+        if (m.getLogLevel().getPriority() < mLogLevel.getPriority()) {
+            return false;
+        }
+
+        /* if pid filter is enabled, filter out messages whose pid does not match
+         * the filter's pid */
+        if (mCheckPid && !m.getPid().equals(mPid)) {
+            return false;
+        }
+
+        /* if app name filter is enabled, filter out messages not matching the app name */
+        if (mCheckAppName) {
+            Matcher matcher = mAppNamePattern.matcher(m.getAppName());
+            if (!matcher.find()) {
+                return false;
+            }
+        }
+
+        /* if tag filter is enabled, filter out messages not matching the tag */
+        if (mCheckTag) {
+            Matcher matcher = mTagPattern.matcher(m.getTag());
+            if (!matcher.find()) {
+                return false;
+            }
+        }
+
+        if (mCheckText) {
+            Matcher matcher = mTextPattern.matcher(m.getMessage());
+            if (!matcher.find()) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatListener.java b/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatListener.java
new file mode 100644
index 0000000..2050402
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatListener.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2013 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.ddmlib.logcat;
+
+import java.util.List;
+
+public interface LogCatListener {
+    void log(List<LogCatMessage> msgList);
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatMessage.java b/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatMessage.java
new file mode 100644
index 0000000..bca1df0
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatMessage.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib.logcat;
+
+import com.android.annotations.NonNull;
+import com.android.ddmlib.Log.LogLevel;
+
+/**
+ * Model a single log message output from {@code logcat -v long}.
+ * A logcat message has a {@link LogLevel}, the pid (process id) of the process
+ * generating the message, the time at which the message was generated, and
+ * the tag and message itself.
+ */
+public final class LogCatMessage {
+    private final LogLevel mLogLevel;
+    private final String mPid;
+    private final String mTid;
+    private final String mAppName;
+    private final String mTag;
+    private final String mTime;
+    private final String mMessage;
+
+    /**
+     * Construct an immutable log message object.
+     */
+    public LogCatMessage(@NonNull LogLevel logLevel, @NonNull String pid, @NonNull String tid,
+            @NonNull String appName, @NonNull String tag,
+            @NonNull String time, @NonNull String msg) {
+        mLogLevel = logLevel;
+        mPid = pid;
+        mAppName = appName;
+        mTag = tag;
+        mTime = time;
+        mMessage = msg;
+
+        long tidValue;
+        try {
+            // Thread id's may be in hex on some platforms.
+            // Decode and store them in radix 10.
+            tidValue = Long.decode(tid.trim());
+        } catch (NumberFormatException e) {
+            tidValue = -1;
+        }
+
+        mTid = Long.toString(tidValue);
+    }
+
+    @NonNull
+    public LogLevel getLogLevel() {
+        return mLogLevel;
+    }
+
+    @NonNull
+    public String getPid() {
+        return mPid;
+    }
+
+    @NonNull
+    public String getTid() {
+        return mTid;
+    }
+
+    @NonNull
+    public String getAppName() {
+        return mAppName;
+    }
+
+    @NonNull
+    public String getTag() {
+        return mTag;
+    }
+
+    @NonNull
+    public String getTime() {
+        return mTime;
+    }
+
+    @NonNull
+    public String getMessage() {
+        return mMessage;
+    }
+
+    @Override
+    public String toString() {
+        return mTime + ": "
+                + mLogLevel.getPriorityLetter() + "/"
+                + mTag + "("
+                + mPid + "): "
+                + mMessage;
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatMessageParser.java b/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatMessageParser.java
new file mode 100644
index 0000000..0e8b03c
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatMessageParser.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmlib.logcat;
+
+import com.android.annotations.NonNull;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log.LogLevel;
+import com.google.common.primitives.Ints;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Class to parse raw output of {@code adb logcat -v long} to {@link LogCatMessage} objects.
+ */
+public final class LogCatMessageParser {
+    private LogLevel mCurLogLevel = LogLevel.WARN;
+    private String mCurPid = "?";
+    private String mCurTid = "?";
+    private String mCurTag = "?";
+    private String mCurTime = "?:??";
+
+    /**
+     * This pattern is meant to parse the first line of a log message with the option
+     * 'logcat -v long'. The first line represents the date, tag, severity, etc.. while the
+     * following lines are the message (can be several lines).<br>
+     * This first line looks something like:<br>
+     * {@code "[ 00-00 00:00:00.000 <pid>:0x<???> <severity>/<tag>]"}
+     * <br>
+     * Note: severity is one of V, D, I, W, E, A? or F. However, there doesn't seem to be
+     *       a way to actually generate an A (assert) message. Log.wtf is supposed to generate
+     *       a message with severity A, however it generates the undocumented F level. In
+     *       such a case, the parser will change the level from F to A.<br>
+     * Note: the fraction of second value can have any number of digit.<br>
+     * Note: the tag should be trimmed as it may have spaces at the end.
+     */
+    private static final Pattern sLogHeaderPattern = Pattern.compile(
+            "^\\[\\s(\\d\\d-\\d\\d\\s\\d\\d:\\d\\d:\\d\\d\\.\\d+)"
+          + "\\s+(\\d*):\\s*(\\S+)\\s([VDIWEAF])/(.*)\\]$");
+
+    /**
+     * Parse a list of strings into {@link LogCatMessage} objects. This method
+     * maintains state from previous calls regarding the last seen header of
+     * logcat messages.
+     * @param lines list of raw strings obtained from logcat -v long
+     * @param device device from which these log messages have been received
+     * @return list of LogMessage objects parsed from the input
+     */
+    @NonNull
+    public List<LogCatMessage> processLogLines(String[] lines, IDevice device) {
+        List<LogCatMessage> messages = new ArrayList<LogCatMessage>(lines.length);
+
+        for (String line : lines) {
+            if (line.isEmpty()) {
+                continue;
+            }
+
+            Matcher matcher = sLogHeaderPattern.matcher(line);
+            if (matcher.matches()) {
+                mCurTime = matcher.group(1);
+                mCurPid = matcher.group(2);
+                mCurTid = matcher.group(3);
+                mCurLogLevel = LogLevel.getByLetterString(matcher.group(4));
+                mCurTag = matcher.group(5).trim();
+
+                /* LogLevel doesn't support messages with severity "F". Log.wtf() is supposed
+                 * to generate "A", but generates "F". */
+                if (mCurLogLevel == null && matcher.group(4).equals("F")) {
+                    mCurLogLevel = LogLevel.ASSERT;
+                }
+            } else {
+                String pkgName = ""; //$NON-NLS-1$
+                Integer pid = Ints.tryParse(mCurPid);
+                if (pid != null && device != null) {
+                    pkgName = device.getClientName(pid);
+                }
+                LogCatMessage m = new LogCatMessage(mCurLogLevel, mCurPid, mCurTid,
+                        pkgName, mCurTag, mCurTime, line);
+                messages.add(m);
+            }
+        }
+
+        return messages;
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatReceiverTask.java b/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatReceiverTask.java
new file mode 100644
index 0000000..b5fd36e
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/logcat/LogCatReceiverTask.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2013 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.ddmlib.logcat;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.concurrency.GuardedBy;
+import com.android.ddmlib.AdbCommandRejectedException;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.IShellOutputReceiver;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmlib.MultiLineReceiver;
+import com.android.ddmlib.ShellCommandUnresponsiveException;
+import com.android.ddmlib.TimeoutException;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class LogCatReceiverTask implements Runnable {
+    private static final String LOGCAT_COMMAND = "logcat -v long"; //$NON-NLS-1$
+    private static final int DEVICE_POLL_INTERVAL_MSEC = 1000;
+
+    private static final LogCatMessage sDeviceDisconnectedMsg =
+            errorMessage("Device disconnected: 1");
+    private static final LogCatMessage sConnectionTimeoutMsg =
+            errorMessage("LogCat Connection timed out");
+    private static final LogCatMessage sConnectionErrorMsg =
+            errorMessage("LogCat Connection error");
+
+    private final IDevice mDevice;
+    private final LogCatOutputReceiver mReceiver;
+    private final LogCatMessageParser mParser;
+    private final AtomicBoolean mCancelled;
+
+    @GuardedBy("this")
+    private final Set<LogCatListener> mListeners = new HashSet<LogCatListener>();
+
+    public LogCatReceiverTask(@NonNull IDevice device) {
+        mDevice = device;
+
+        mReceiver = new LogCatOutputReceiver();
+        mParser = new LogCatMessageParser();
+        mCancelled = new AtomicBoolean();
+    }
+
+    @Override
+    public void run() {
+        // wait while device comes online
+        while (!mDevice.isOnline()) {
+            try {
+                Thread.sleep(DEVICE_POLL_INTERVAL_MSEC);
+            } catch (InterruptedException e) {
+                return;
+            }
+        }
+
+        try {
+            mDevice.executeShellCommand(LOGCAT_COMMAND, mReceiver, 0);
+        } catch (TimeoutException e) {
+            notifyListeners(Collections.singletonList(sConnectionTimeoutMsg));
+        } catch (AdbCommandRejectedException ignored) {
+            // will not be thrown as long as the shell supports logcat
+        } catch (ShellCommandUnresponsiveException ignored) {
+            // this will not be thrown since the last argument is 0
+        } catch (IOException e) {
+            notifyListeners(Collections.singletonList(sConnectionErrorMsg));
+        }
+
+        notifyListeners(Collections.singletonList(sDeviceDisconnectedMsg));
+    }
+
+    public void stop() {
+        mCancelled.set(true);
+    }
+
+    private class LogCatOutputReceiver extends MultiLineReceiver {
+        public LogCatOutputReceiver() {
+            setTrimLine(false);
+        }
+
+        /** Implements {@link IShellOutputReceiver#isCancelled() }. */
+        @Override
+        public boolean isCancelled() {
+            return mCancelled.get();
+        }
+
+        @Override
+        public void processNewLines(String[] lines) {
+            if (!mCancelled.get()) {
+                processLogLines(lines);
+            }
+        }
+
+        private void processLogLines(String[] lines) {
+            List<LogCatMessage> newMessages = mParser.processLogLines(lines, mDevice);
+            if (!newMessages.isEmpty()) {
+                notifyListeners(newMessages);
+            }
+        }
+    }
+
+    public synchronized void addLogCatListener(LogCatListener l) {
+        mListeners.add(l);
+    }
+
+    public synchronized void removeLogCatListener(LogCatListener l) {
+        mListeners.remove(l);
+    }
+
+    private synchronized void notifyListeners(List<LogCatMessage> messages) {
+        for (LogCatListener l: mListeners) {
+            l.log(messages);
+        }
+    }
+
+    private static LogCatMessage errorMessage(String msg) {
+        return new LogCatMessage(LogLevel.ERROR, "", "", "", "", "", msg);
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/testrunner/IRemoteAndroidTestRunner.java b/ddmlib/src/main/java/com/android/ddmlib/testrunner/IRemoteAndroidTestRunner.java
new file mode 100644
index 0000000..7d3d6bf
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/testrunner/IRemoteAndroidTestRunner.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2010 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.ddmlib.testrunner;
+
+import com.android.ddmlib.AdbCommandRejectedException;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.ShellCommandUnresponsiveException;
+import com.android.ddmlib.TimeoutException;
+
+import java.io.IOException;
+import java.util.Collection;
+
+/**
+ * Interface for running a Android test command remotely and reporting result to a listener.
+ */
+public interface IRemoteAndroidTestRunner {
+
+    public static enum TestSize {
+        /** Run tests annotated with SmallTest */
+        SMALL("small"),
+        /** Run tests annotated with MediumTest */
+        MEDIUM("medium"),
+        /** Run tests annotated with LargeTest */
+        LARGE("large");
+
+        private String mRunnerValue;
+
+        /**
+         * Create a {@link TestSize}.
+         *
+         * @param runnerValue the {@link String} value that represents the size that is passed to
+         * device. Defined on device in android.test.InstrumentationTestRunner.
+         */
+        TestSize(String runnerValue) {
+            mRunnerValue = runnerValue;
+        }
+
+        String getRunnerValue() {
+            return mRunnerValue;
+        }
+
+        /**
+         * Return the {@link TestSize} corresponding to the given Android platform defined value.
+         *
+         * @throws IllegalArgumentException if {@link TestSize} cannot be found.
+         */
+        public static TestSize getTestSize(String value) {
+            // build the error message in the success case too, to avoid two for loops
+            StringBuilder msgBuilder = new StringBuilder("Unknown TestSize ");
+            msgBuilder.append(value);
+            msgBuilder.append(", Must be one of ");
+            for (TestSize size : values()) {
+                if (size.getRunnerValue().equals(value)) {
+                    return size;
+                }
+                msgBuilder.append(size.getRunnerValue());
+                msgBuilder.append(", ");
+            }
+            throw new IllegalArgumentException(msgBuilder.toString());
+        }
+    }
+
+    /**
+     * Returns the application package name.
+     */
+    public String getPackageName();
+
+    /**
+     * Returns the runnerName.
+     */
+    public String getRunnerName();
+
+    /**
+     * Sets to run only tests in this class
+     * Must be called before 'run'.
+     *
+     * @param className fully qualified class name (eg x.y.z)
+     */
+    public void setClassName(String className);
+
+    /**
+     * Sets to run only tests in the provided classes
+     * Must be called before 'run'.
+     * <p>
+     * If providing more than one class, requires a InstrumentationTestRunner that supports
+     * the multiple class argument syntax.
+     *
+     * @param classNames array of fully qualified class names (eg x.y.z)
+     */
+    public void setClassNames(String[] classNames);
+
+    /**
+     * Sets to run only specified test method
+     * Must be called before 'run'.
+     *
+     * @param className fully qualified class name (eg x.y.z)
+     * @param testName method name
+     */
+    public void setMethodName(String className, String testName);
+
+    /**
+     * Sets to run all tests in specified package
+     * Must be called before 'run'.
+     *
+     * @param packageName fully qualified package name (eg x.y.z)
+     */
+    public void setTestPackageName(String packageName);
+
+    /**
+     * Sets to run only tests of given size.
+     * Must be called before 'run'.
+     *
+     * @param size the {@link TestSize} to run.
+     */
+    public void setTestSize(TestSize size);
+
+    /**
+     * Adds a argument to include in instrumentation command.
+     * <p/>
+     * Must be called before 'run'. If an argument with given name has already been provided, it's
+     * value will be overridden.
+     *
+     * @param name the name of the instrumentation bundle argument
+     * @param value the value of the argument
+     */
+    public void addInstrumentationArg(String name, String value);
+
+    /**
+     * Removes a previously added argument.
+     *
+     * @param name the name of the instrumentation bundle argument to remove
+     */
+    public void removeInstrumentationArg(String name);
+
+    /**
+     * Adds a boolean argument to include in instrumentation command.
+     * <p/>
+     * @see RemoteAndroidTestRunner#addInstrumentationArg
+     *
+     * @param name the name of the instrumentation bundle argument
+     * @param value the value of the argument
+     */
+    public void addBooleanArg(String name, boolean value);
+
+    /**
+     * Sets this test run to log only mode - skips test execution.
+     */
+    public void setLogOnly(boolean logOnly);
+
+    /**
+     * Sets this debug mode of this test run. If true, the Android test runner will wait for a
+     * debugger to attach before proceeding with test execution.
+     */
+    public void setDebug(boolean debug);
+
+    /**
+     * Sets this code coverage mode of this test run.
+     */
+    public void setCoverage(boolean coverage);
+
+    /**
+     * Sets the maximum time allowed between output of the shell command running the tests on
+     * the devices.
+     * <p/>
+     * This allows setting a timeout in case the tests can become stuck and never finish. This is
+     * different from the normal timeout on the connection.
+     * <p/>
+     * By default no timeout will be specified.
+     *
+     * @see IDevice#executeShellCommand(String, com.android.ddmlib.IShellOutputReceiver, int)
+     */
+    public void setMaxtimeToOutputResponse(int maxTimeToOutputResponse);
+
+    /**
+     * Set a custom run name to be reported to the {@link ITestRunListener} on {@link #run}
+     * <p/>
+     * If unspecified, will use package name
+     *
+     * @param runName
+     */
+    public void setRunName(String runName);
+
+    /**
+     * Execute this test run.
+     * <p/>
+     * Convenience method for {@link #run(Collection)}.
+     *
+     * @param listeners listens for test results
+     * @throws TimeoutException in case of a timeout on the connection.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     * @throws ShellCommandUnresponsiveException if the device did not output any test result for
+     * a period longer than the max time to output.
+     * @throws IOException if connection to device was lost.
+     *
+     * @see #setMaxtimeToOutputResponse(int)
+     */
+    public void run(ITestRunListener... listeners)
+            throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException,
+            IOException;
+
+    /**
+     * Execute this test run.
+     *
+     * @param listeners collection of listeners for test results
+     * @throws TimeoutException in case of a timeout on the connection.
+     * @throws AdbCommandRejectedException if adb rejects the command
+     * @throws ShellCommandUnresponsiveException if the device did not output any test result for
+     * a period longer than the max time to output.
+     * @throws IOException if connection to device was lost.
+     *
+     * @see #setMaxtimeToOutputResponse(int)
+     */
+    public void run(Collection<ITestRunListener> listeners)
+            throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException,
+            IOException;
+
+    /**
+     * Requests cancellation of this test run.
+     */
+    public void cancel();
+
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/testrunner/ITestRunListener.java b/ddmlib/src/main/java/com/android/ddmlib/testrunner/ITestRunListener.java
new file mode 100644
index 0000000..7e20c9f
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/testrunner/ITestRunListener.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2008 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.ddmlib.testrunner;
+
+import java.util.Map;
+
+/**
+ * Receives event notifications during instrumentation test runs.
+ * <p/>
+ * Patterned after junit.runner.TestRunListener.
+ * <p/>
+ * The sequence of calls will be:
+ * <ul>
+ * <li> testRunStarted
+ * <li> testStarted
+ * <li> [testFailed]
+ * <li> testEnded
+ * <li> ....
+ * <li> [testRunFailed]
+ * <li> testRunEnded
+ * </ul>
+ */
+public interface ITestRunListener {
+
+    /**
+     *  Types of test failures.
+     */
+    enum TestFailure {
+        /** Test failed due to unanticipated uncaught exception. */
+        ERROR,
+        /** Test failed due to a false assertion. */
+        FAILURE
+    }
+
+    /**
+     * Reports the start of a test run.
+     *
+     * @param runName the test run name
+     * @param testCount total number of tests in test run
+     */
+    public void testRunStarted(String runName, int testCount);
+
+    /**
+     * Reports the start of an individual test case.
+     *
+     * @param test identifies the test
+     */
+    public void testStarted(TestIdentifier test);
+
+    /**
+     * Reports the failure of a individual test case.
+     * <p/>
+     * Will be called between testStarted and testEnded.
+     *
+     * @param status failure type
+     * @param test identifies the test
+     * @param trace stack trace of failure
+     */
+    public void testFailed(TestFailure status, TestIdentifier test, String trace);
+
+    /**
+     * Reports the execution end of an individual test case.
+     * <p/>
+     * If {@link #testFailed} was not invoked, this test passed.  Also returns any key/value
+     * metrics which may have been emitted during the test case's execution.
+     *
+     * @param test identifies the test
+     * @param testMetrics a {@link Map} of the metrics emitted
+     */
+    public void testEnded(TestIdentifier test, Map<String, String> testMetrics);
+
+    /**
+     * Reports test run failed to complete due to a fatal error.
+     *
+     * @param errorMessage {@link String} describing reason for run failure.
+     */
+    public void testRunFailed(String errorMessage);
+
+    /**
+     * Reports test run stopped before completion due to a user request.
+     * <p/>
+     * TODO: currently unused, consider removing
+     *
+     * @param elapsedTime device reported elapsed time, in milliseconds
+     */
+    public void testRunStopped(long elapsedTime);
+
+    /**
+     * Reports end of test run.
+     *
+     * @param elapsedTime device reported elapsed time, in milliseconds
+     * @param runMetrics key-value pairs reported at the end of a test run
+     */
+    public void testRunEnded(long elapsedTime, Map<String, String> runMetrics);
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/testrunner/InstrumentationResultParser.java b/ddmlib/src/main/java/com/android/ddmlib/testrunner/InstrumentationResultParser.java
new file mode 100644
index 0000000..b254553
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/testrunner/InstrumentationResultParser.java
@@ -0,0 +1,609 @@
+/*
+ * Copyright (C) 2008 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.ddmlib.testrunner;
+
+import com.android.ddmlib.IShellOutputReceiver;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.MultiLineReceiver;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Parses the 'raw output mode' results of an instrumentation test run from shell and informs a
+ * ITestRunListener of the results.
+ *
+ * <p>Expects the following output:
+ *
+ * <p>If fatal error occurred when attempted to run the tests:
+ * <pre>
+ * INSTRUMENTATION_STATUS: Error=error Message
+ * INSTRUMENTATION_FAILED:
+ * </pre>
+ * <p>or
+ * <pre>
+ * INSTRUMENTATION_RESULT: shortMsg=error Message
+ * </pre>
+ *
+ * <p>Otherwise, expect a series of test results, each one containing a set of status key/value
+ * pairs, delimited by a start(1)/pass(0)/fail(-2)/error(-1) status code result. At end of test
+ * run, expects that the elapsed test time in seconds will be displayed
+ *
+ * <p>For example:
+ * <pre>
+ * INSTRUMENTATION_STATUS_CODE: 1
+ * INSTRUMENTATION_STATUS: class=com.foo.FooTest
+ * INSTRUMENTATION_STATUS: test=testFoo
+ * INSTRUMENTATION_STATUS: numtests=2
+ * INSTRUMENTATION_STATUS: stack=com.foo.FooTest#testFoo:312
+ *    com.foo.X
+ * INSTRUMENTATION_STATUS_CODE: -2
+ * ...
+ *
+ * Time: X
+ * </pre>
+ * <p>Note that the "value" portion of the key-value pair may wrap over several text lines
+ */
+public class InstrumentationResultParser extends MultiLineReceiver {
+
+    /** Relevant test status keys. */
+    private static class StatusKeys {
+        private static final String TEST = "test";
+        private static final String CLASS = "class";
+        private static final String STACK = "stack";
+        private static final String NUMTESTS = "numtests";
+        private static final String ERROR = "Error";
+        private static final String SHORTMSG = "shortMsg";
+    }
+
+    /** The set of expected status keys. Used to filter which keys should be stored as metrics */
+    private static final Set<String> KNOWN_KEYS = new HashSet<String>();
+    static {
+        KNOWN_KEYS.add(StatusKeys.TEST);
+        KNOWN_KEYS.add(StatusKeys.CLASS);
+        KNOWN_KEYS.add(StatusKeys.STACK);
+        KNOWN_KEYS.add(StatusKeys.NUMTESTS);
+        KNOWN_KEYS.add(StatusKeys.ERROR);
+        KNOWN_KEYS.add(StatusKeys.SHORTMSG);
+        // unused, but regularly occurring status keys.
+        KNOWN_KEYS.add("stream");
+        KNOWN_KEYS.add("id");
+        KNOWN_KEYS.add("current");
+    }
+
+    /** Test result status codes. */
+    private static class StatusCodes {
+        private static final int FAILURE = -2;
+        private static final int START = 1;
+        private static final int ERROR = -1;
+        private static final int OK = 0;
+        private static final int IN_PROGRESS = 2;
+    }
+
+    /** Prefixes used to identify output. */
+    private static class Prefixes {
+        private static final String STATUS = "INSTRUMENTATION_STATUS: ";
+        private static final String STATUS_CODE = "INSTRUMENTATION_STATUS_CODE: ";
+        private static final String STATUS_FAILED = "INSTRUMENTATION_FAILED: ";
+        private static final String CODE = "INSTRUMENTATION_CODE: ";
+        private static final String RESULT = "INSTRUMENTATION_RESULT: ";
+        private static final String TIME_REPORT = "Time: ";
+    }
+
+    private final Collection<ITestRunListener> mTestListeners;
+
+    /**
+     * Test result data
+     */
+    private static class TestResult {
+        private Integer mCode = null;
+        private String mTestName = null;
+        private String mTestClass = null;
+        private String mStackTrace = null;
+        private Integer mNumTests = null;
+
+        /** Returns true if all expected values have been parsed */
+        boolean isComplete() {
+            return mCode != null && mTestName != null && mTestClass != null;
+        }
+
+        /** Provides a more user readable string for TestResult, if possible */
+        @Override
+        public String toString() {
+            StringBuilder output = new StringBuilder();
+            if (mTestClass != null ) {
+                output.append(mTestClass);
+                output.append('#');
+            }
+            if (mTestName != null) {
+                output.append(mTestName);
+            }
+            if (output.length() > 0) {
+                return output.toString();
+            }
+            return "unknown result";
+        }
+    }
+
+    /** the name to provide to {@link ITestRunListener#testRunStarted(String, int)} */
+    private final String mTestRunName;
+
+    /** Stores the status values for the test result currently being parsed */
+    private TestResult mCurrentTestResult = null;
+
+    /** Stores the status values for the test result last parsed */
+    private TestResult mLastTestResult = null;
+
+    /** Stores the current "key" portion of the status key-value being parsed. */
+    private String mCurrentKey = null;
+
+    /** Stores the current "value" portion of the status key-value being parsed. */
+    private StringBuilder mCurrentValue = null;
+
+    /** True if start of test has already been reported to listener. */
+    private boolean mTestStartReported = false;
+
+    /** True if the completion of the test run has been detected. */
+    private boolean mTestRunFinished = false;
+
+    /** True if test run failure has already been reported to listener. */
+    private boolean mTestRunFailReported = false;
+
+    /** The elapsed time of the test run, in milliseconds. */
+    private long mTestTime = 0;
+
+    /** True if current test run has been canceled by user. */
+    private boolean mIsCancelled = false;
+
+    /** The number of tests currently run  */
+    private int mNumTestsRun = 0;
+
+    /** The number of tests expected to run  */
+    private int mNumTestsExpected = 0;
+
+    /** True if the parser is parsing a line beginning with "INSTRUMENTATION_RESULT" */
+    private boolean mInInstrumentationResultKey = false;
+
+    /**
+     * Stores key-value pairs under INSTRUMENTATION_RESULT header, these are printed at the
+     * end of a test run, if applicable
+     */
+    private Map<String, String> mInstrumentationResultBundle = new HashMap<String, String>();
+
+    /**
+     * Stores key-value pairs of metrics emitted during the execution of each test case.  Note that
+     * standard keys that are stored in the TestResults class are filtered out of this Map.
+     */
+    private Map<String, String> mTestMetrics = new HashMap<String, String>();
+
+    private static final String LOG_TAG = "InstrumentationResultParser";
+
+    /** Error message supplied when no parseable test results are received from test run. */
+    static final String NO_TEST_RESULTS_MSG = "No test results";
+
+    /** Error message supplied when a test start bundle is parsed, but not the test end bundle. */
+    static final String INCOMPLETE_TEST_ERR_MSG_PREFIX = "Test failed to run to completion";
+    static final String INCOMPLETE_TEST_ERR_MSG_POSTFIX = "Check device logcat for details";
+
+    /** Error message supplied when the test run is incomplete. */
+    static final String INCOMPLETE_RUN_ERR_MSG_PREFIX = "Test run failed to complete";
+
+    /**
+     * Creates the InstrumentationResultParser.
+     *
+     * @param runName the test run name to provide to
+     *            {@link ITestRunListener#testRunStarted(String, int)}
+     * @param listeners informed of test results as the tests are executing
+     */
+    public InstrumentationResultParser(String runName, Collection<ITestRunListener> listeners) {
+        mTestRunName = runName;
+        mTestListeners = new ArrayList<ITestRunListener>(listeners);
+    }
+
+    /**
+     * Creates the InstrumentationResultParser for a single listener.
+     *
+     * @param runName the test run name to provide to
+     *            {@link ITestRunListener#testRunStarted(String, int)}
+     * @param listener informed of test results as the tests are executing
+     */
+    public InstrumentationResultParser(String runName, ITestRunListener listener) {
+        this(runName, Collections.singletonList(listener));
+    }
+
+    /**
+     * Processes the instrumentation test output from shell.
+     *
+     * @see MultiLineReceiver#processNewLines
+     */
+    @Override
+    public void processNewLines(String[] lines) {
+        for (String line : lines) {
+            parse(line);
+            // in verbose mode, dump all adb output to log
+            Log.v(LOG_TAG, line);
+        }
+    }
+
+    /**
+     * Parse an individual output line. Expects a line that is one of:
+     * <ul>
+     * <li>
+     * The start of a new status line (starts with Prefixes.STATUS or Prefixes.STATUS_CODE),
+     * and thus there is a new key=value pair to parse, and the previous key-value pair is
+     * finished.
+     * </li>
+     * <li>
+     * A continuation of the previous status (the "value" portion of the key has wrapped
+     * to the next line).
+     * </li>
+     * <li> A line reporting a fatal error in the test run (Prefixes.STATUS_FAILED) </li>
+     * <li> A line reporting the total elapsed time of the test run. (Prefixes.TIME_REPORT) </li>
+     * </ul>
+     *
+     * @param line  Text output line
+     */
+    private void parse(String line) {
+        if (line.startsWith(Prefixes.STATUS_CODE)) {
+            // Previous status key-value has been collected. Store it.
+            submitCurrentKeyValue();
+            mInInstrumentationResultKey = false;
+            parseStatusCode(line);
+        } else if (line.startsWith(Prefixes.STATUS)) {
+            // Previous status key-value has been collected. Store it.
+            submitCurrentKeyValue();
+            mInInstrumentationResultKey = false;
+            parseKey(line, Prefixes.STATUS.length());
+        } else if (line.startsWith(Prefixes.RESULT)) {
+            // Previous status key-value has been collected. Store it.
+            submitCurrentKeyValue();
+            mInInstrumentationResultKey = true;
+            parseKey(line, Prefixes.RESULT.length());
+        } else if (line.startsWith(Prefixes.STATUS_FAILED) ||
+                   line.startsWith(Prefixes.CODE)) {
+            // Previous status key-value has been collected. Store it.
+            submitCurrentKeyValue();
+            mInInstrumentationResultKey = false;
+            // these codes signal the end of the instrumentation run
+            mTestRunFinished = true;
+            // just ignore the remaining data on this line
+        } else if (line.startsWith(Prefixes.TIME_REPORT)) {
+            parseTime(line);
+        } else {
+            if (mCurrentValue != null) {
+                // this is a value that has wrapped to next line.
+                mCurrentValue.append("\r\n");
+                mCurrentValue.append(line);
+            } else if (!line.trim().isEmpty()) {
+                Log.d(LOG_TAG, "unrecognized line " + line);
+            }
+        }
+    }
+
+    /**
+     * Stores the currently parsed key-value pair in the appropriate place.
+     */
+    private void submitCurrentKeyValue() {
+        if (mCurrentKey != null && mCurrentValue != null) {
+            String statusValue = mCurrentValue.toString();
+            if (mInInstrumentationResultKey) {
+                if (!KNOWN_KEYS.contains(mCurrentKey)) {
+                    mInstrumentationResultBundle.put(mCurrentKey, statusValue);
+                } else if (mCurrentKey.equals(StatusKeys.SHORTMSG)) {
+                    // test run must have failed
+                    handleTestRunFailed(String.format("Instrumentation run failed due to '%1$s'",
+                            statusValue));
+                }
+            } else {
+                TestResult testInfo = getCurrentTestInfo();
+
+                if (mCurrentKey.equals(StatusKeys.CLASS)) {
+                    testInfo.mTestClass = statusValue.trim();
+                } else if (mCurrentKey.equals(StatusKeys.TEST)) {
+                    testInfo.mTestName = statusValue.trim();
+                } else if (mCurrentKey.equals(StatusKeys.NUMTESTS)) {
+                    try {
+                        testInfo.mNumTests = Integer.parseInt(statusValue);
+                    } catch (NumberFormatException e) {
+                        Log.w(LOG_TAG, "Unexpected integer number of tests, received "
+                                + statusValue);
+                    }
+                } else if (mCurrentKey.equals(StatusKeys.ERROR)) {
+                    // test run must have failed
+                    handleTestRunFailed(statusValue);
+                } else if (mCurrentKey.equals(StatusKeys.STACK)) {
+                    testInfo.mStackTrace = statusValue;
+                } else if (!KNOWN_KEYS.contains(mCurrentKey)) {
+                    // Not one of the recognized key/value pairs, so dump it in mTestMetrics
+                    mTestMetrics.put(mCurrentKey, statusValue);
+                }
+            }
+
+            mCurrentKey = null;
+            mCurrentValue = null;
+        }
+    }
+
+    /**
+     * A utility method to return the test metrics from the current test case execution and get
+     * ready for the next one.
+     */
+    private Map<String, String> getAndResetTestMetrics() {
+        Map<String, String> retVal = mTestMetrics;
+        mTestMetrics = new HashMap<String, String>();
+        return retVal;
+    }
+
+    private TestResult getCurrentTestInfo() {
+        if (mCurrentTestResult == null) {
+            mCurrentTestResult = new TestResult();
+        }
+        return mCurrentTestResult;
+    }
+
+    private void clearCurrentTestInfo() {
+        mLastTestResult = mCurrentTestResult;
+        mCurrentTestResult = null;
+    }
+
+    /**
+     * Parses the key from the current line.
+     * Expects format of "key=value".
+     *
+     * @param line full line of text to parse
+     * @param keyStartPos the starting position of the key in the given line
+     */
+    private void parseKey(String line, int keyStartPos) {
+        int endKeyPos = line.indexOf('=', keyStartPos);
+        if (endKeyPos != -1) {
+            mCurrentKey = line.substring(keyStartPos, endKeyPos).trim();
+            parseValue(line, endKeyPos + 1);
+        }
+    }
+
+    /**
+     * Parses the start of a key=value pair.
+     *
+     * @param line - full line of text to parse
+     * @param valueStartPos - the starting position of the value in the given line
+     */
+    private void parseValue(String line, int valueStartPos) {
+        mCurrentValue = new StringBuilder();
+        mCurrentValue.append(line.substring(valueStartPos));
+    }
+
+    /**
+     * Parses out a status code result.
+     */
+    private void parseStatusCode(String line) {
+        String value = line.substring(Prefixes.STATUS_CODE.length()).trim();
+        TestResult testInfo = getCurrentTestInfo();
+        testInfo.mCode = StatusCodes.ERROR;
+        try {
+            testInfo.mCode = Integer.parseInt(value);
+        } catch (NumberFormatException e) {
+            Log.w(LOG_TAG, "Expected integer status code, received: " + value);
+            testInfo.mCode = StatusCodes.ERROR;
+        }
+        if (testInfo.mCode != StatusCodes.IN_PROGRESS) {
+            // this means we're done with current test result bundle
+            reportResult(testInfo);
+            clearCurrentTestInfo();
+        }
+    }
+
+    /**
+     * Returns true if test run canceled.
+     *
+     * @see IShellOutputReceiver#isCancelled()
+     */
+    @Override
+    public boolean isCancelled() {
+        return mIsCancelled;
+    }
+
+    /**
+     * Requests cancellation of test run.
+     */
+    public void cancel() {
+        mIsCancelled = true;
+    }
+
+    /**
+     * Reports a test result to the test run listener. Must be called when a individual test
+     * result has been fully parsed.
+     *
+     * @param statusMap key-value status pairs of test result
+     */
+    private void reportResult(TestResult testInfo) {
+        if (!testInfo.isComplete()) {
+            Log.w(LOG_TAG, "invalid instrumentation status bundle " + testInfo.toString());
+            return;
+        }
+        reportTestRunStarted(testInfo);
+        TestIdentifier testId = new TestIdentifier(testInfo.mTestClass, testInfo.mTestName);
+        Map<String, String> metrics;
+
+        switch (testInfo.mCode) {
+            case StatusCodes.START:
+                for (ITestRunListener listener : mTestListeners) {
+                    listener.testStarted(testId);
+                }
+                break;
+            case StatusCodes.FAILURE:
+                metrics = getAndResetTestMetrics();
+                for (ITestRunListener listener : mTestListeners) {
+                    listener.testFailed(ITestRunListener.TestFailure.FAILURE, testId,
+                        getTrace(testInfo));
+
+                    listener.testEnded(testId, metrics);
+                }
+                mNumTestsRun++;
+                break;
+            case StatusCodes.ERROR:
+                metrics = getAndResetTestMetrics();
+                for (ITestRunListener listener : mTestListeners) {
+                    listener.testFailed(ITestRunListener.TestFailure.ERROR, testId,
+                        getTrace(testInfo));
+                    listener.testEnded(testId, metrics);
+                }
+                mNumTestsRun++;
+                break;
+            case StatusCodes.OK:
+                metrics = getAndResetTestMetrics();
+                for (ITestRunListener listener : mTestListeners) {
+                    listener.testEnded(testId, metrics);
+                }
+                mNumTestsRun++;
+                break;
+            default:
+                metrics = getAndResetTestMetrics();
+                Log.e(LOG_TAG, "Unknown status code received: " + testInfo.mCode);
+                for (ITestRunListener listener : mTestListeners) {
+                    listener.testEnded(testId, metrics);
+                }
+                mNumTestsRun++;
+            break;
+        }
+
+    }
+
+    /**
+     * Reports the start of a test run, and the total test count, if it has not been previously
+     * reported.
+     *
+     * @param testInfo current test status values
+     */
+    private void reportTestRunStarted(TestResult testInfo) {
+        // if start test run not reported yet
+        if (!mTestStartReported && testInfo.mNumTests != null) {
+            for (ITestRunListener listener : mTestListeners) {
+                listener.testRunStarted(mTestRunName, testInfo.mNumTests);
+            }
+            mNumTestsExpected = testInfo.mNumTests;
+            mTestStartReported = true;
+        }
+    }
+
+    /**
+     * Returns the stack trace of the current failed test, from the provided testInfo.
+     */
+    private String getTrace(TestResult testInfo) {
+        if (testInfo.mStackTrace != null) {
+            return testInfo.mStackTrace;
+        } else {
+            Log.e(LOG_TAG, "Could not find stack trace for failed test ");
+            return new Throwable("Unknown failure").toString();
+        }
+    }
+
+    /**
+     * Parses out and store the elapsed time.
+     */
+    private void parseTime(String line) {
+        final Pattern timePattern = Pattern.compile(String.format("%s\\s*([\\d\\.]+)",
+                Prefixes.TIME_REPORT));
+        Matcher timeMatcher = timePattern.matcher(line);
+        if (timeMatcher.find()) {
+            String timeString = timeMatcher.group(1);
+            try {
+                float timeSeconds = Float.parseFloat(timeString);
+                mTestTime = (long) (timeSeconds * 1000);
+            } catch (NumberFormatException e) {
+                Log.w(LOG_TAG, String.format("Unexpected time format %1$s", line));
+            }
+        } else {
+            Log.w(LOG_TAG, String.format("Unexpected time format %1$s", line));
+        }
+    }
+
+    /**
+     * Inform the parser of a instrumentation run failure. Should be called when the adb command
+     * used to run the test fails.
+     */
+    public void handleTestRunFailed(String errorMsg) {
+        errorMsg = (errorMsg == null ? "Unknown error" : errorMsg);
+        Log.i(LOG_TAG, String.format("test run failed: '%1$s'", errorMsg));
+        if (mLastTestResult != null &&
+            mLastTestResult.isComplete() &&
+            StatusCodes.START == mLastTestResult.mCode) {
+
+            // received test start msg, but not test complete
+            // assume test caused this, report as test failure
+            TestIdentifier testId = new TestIdentifier(mLastTestResult.mTestClass,
+                    mLastTestResult.mTestName);
+            for (ITestRunListener listener : mTestListeners) {
+                listener.testFailed(ITestRunListener.TestFailure.ERROR, testId,
+                    String.format("%1$s. Reason: '%2$s'. %3$s", INCOMPLETE_TEST_ERR_MSG_PREFIX,
+                            errorMsg, INCOMPLETE_TEST_ERR_MSG_POSTFIX));
+                listener.testEnded(testId, getAndResetTestMetrics());
+            }
+        }
+        for (ITestRunListener listener : mTestListeners) {
+            if (!mTestStartReported) {
+                // test run wasn't started - must have crashed before it started
+                listener.testRunStarted(mTestRunName, 0);
+            }
+            listener.testRunFailed(errorMsg);
+            listener.testRunEnded(mTestTime, mInstrumentationResultBundle);
+        }
+        mTestStartReported = true;
+        mTestRunFailReported = true;
+    }
+
+    /**
+     * Called by parent when adb session is complete.
+     */
+    @Override
+    public void done() {
+        super.done();
+        if (!mTestRunFailReported) {
+            handleOutputDone();
+        }
+    }
+
+    /**
+     * Handles the end of the adb session when a test run failure has not been reported yet
+     */
+    private void handleOutputDone() {
+        if (!mTestStartReported && !mTestRunFinished) {
+            // no results
+            handleTestRunFailed(NO_TEST_RESULTS_MSG);
+        } else if (mNumTestsExpected > mNumTestsRun) {
+            final String message =
+                String.format("%1$s. Expected %2$d tests, received %3$d",
+                        INCOMPLETE_RUN_ERR_MSG_PREFIX, mNumTestsExpected, mNumTestsRun);
+            handleTestRunFailed(message);
+        } else {
+            for (ITestRunListener listener : mTestListeners) {
+                if (!mTestStartReported) {
+                    // test run wasn't started, but it finished successfully. Must be a run with
+                    // no tests
+                    listener.testRunStarted(mTestRunName, 0);
+                }
+                listener.testRunEnded(mTestTime, mInstrumentationResultBundle);
+            }
+        }
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java b/ddmlib/src/main/java/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java
new file mode 100644
index 0000000..9897fcd
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2008 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.ddmlib.testrunner;
+
+
+import com.android.ddmlib.AdbCommandRejectedException;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.ShellCommandUnresponsiveException;
+import com.android.ddmlib.TimeoutException;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Runs a Android test command remotely and reports results.
+ */
+public class RemoteAndroidTestRunner implements IRemoteAndroidTestRunner  {
+
+    private final String mPackageName;
+    private final String mRunnerName;
+    private IDevice mRemoteDevice;
+    // default to no timeout
+    private int mMaxTimeToOutputResponse = 0;
+    private String mRunName = null;
+
+    /** map of name-value instrumentation argument pairs */
+    private Map<String, String> mArgMap;
+    private InstrumentationResultParser mParser;
+
+    private static final String LOG_TAG = "RemoteAndroidTest";
+    private static final String DEFAULT_RUNNER_NAME = "android.test.InstrumentationTestRunner";
+
+    private static final char CLASS_SEPARATOR = ',';
+    private static final char METHOD_SEPARATOR = '#';
+    private static final char RUNNER_SEPARATOR = '/';
+
+    // defined instrumentation argument names
+    private static final String CLASS_ARG_NAME = "class";
+    private static final String LOG_ARG_NAME = "log";
+    private static final String DEBUG_ARG_NAME = "debug";
+    private static final String COVERAGE_ARG_NAME = "coverage";
+    private static final String PACKAGE_ARG_NAME = "package";
+    private static final String SIZE_ARG_NAME = "size";
+
+    /**
+     * Creates a remote Android test runner.
+     *
+     * @param packageName the Android application package that contains the tests to run
+     * @param runnerName the instrumentation test runner to execute. If null, will use default
+     *   runner
+     * @param remoteDevice the Android device to execute tests on
+     */
+    public RemoteAndroidTestRunner(String packageName,
+                                   String runnerName,
+                                   IDevice remoteDevice) {
+
+        mPackageName = packageName;
+        mRunnerName = runnerName;
+        mRemoteDevice = remoteDevice;
+        mArgMap = new Hashtable<String, String>();
+    }
+
+    /**
+     * Alternate constructor. Uses default instrumentation runner.
+     *
+     * @param packageName the Android application package that contains the tests to run
+     * @param remoteDevice the Android device to execute tests on
+     */
+    public RemoteAndroidTestRunner(String packageName,
+                                   IDevice remoteDevice) {
+        this(packageName, null, remoteDevice);
+    }
+
+    @Override
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    @Override
+    public String getRunnerName() {
+        if (mRunnerName == null) {
+            return DEFAULT_RUNNER_NAME;
+        }
+        return mRunnerName;
+    }
+
+    /**
+     * Returns the complete instrumentation component path.
+     */
+    private String getRunnerPath() {
+        return getPackageName() + RUNNER_SEPARATOR + getRunnerName();
+    }
+
+    @Override
+    public void setClassName(String className) {
+        addInstrumentationArg(CLASS_ARG_NAME, className);
+    }
+
+    @Override
+    public void setClassNames(String[] classNames) {
+        StringBuilder classArgBuilder = new StringBuilder();
+
+        for (int i = 0; i < classNames.length; i++) {
+            if (i != 0) {
+                classArgBuilder.append(CLASS_SEPARATOR);
+            }
+            classArgBuilder.append(classNames[i]);
+        }
+        setClassName(classArgBuilder.toString());
+    }
+
+    @Override
+    public void setMethodName(String className, String testName) {
+        setClassName(className + METHOD_SEPARATOR + testName);
+    }
+
+    @Override
+    public void setTestPackageName(String packageName) {
+        addInstrumentationArg(PACKAGE_ARG_NAME, packageName);
+    }
+
+    @Override
+    public void addInstrumentationArg(String name, String value) {
+        if (name == null || value == null) {
+            throw new IllegalArgumentException("name or value arguments cannot be null");
+        }
+        mArgMap.put(name, value);
+    }
+
+    @Override
+    public void removeInstrumentationArg(String name) {
+        if (name == null) {
+            throw new IllegalArgumentException("name argument cannot be null");
+        }
+        mArgMap.remove(name);
+    }
+
+    @Override
+    public void addBooleanArg(String name, boolean value) {
+        addInstrumentationArg(name, Boolean.toString(value));
+    }
+
+    @Override
+    public void setLogOnly(boolean logOnly) {
+        addBooleanArg(LOG_ARG_NAME, logOnly);
+    }
+
+    @Override
+    public void setDebug(boolean debug) {
+        addBooleanArg(DEBUG_ARG_NAME, debug);
+    }
+
+    @Override
+    public void setCoverage(boolean coverage) {
+        addBooleanArg(COVERAGE_ARG_NAME, coverage);
+    }
+
+    @Override
+    public void setTestSize(TestSize size) {
+        addInstrumentationArg(SIZE_ARG_NAME, size.getRunnerValue());
+    }
+
+    @Override
+    public void setMaxtimeToOutputResponse(int maxTimeToOutputResponse) {
+        mMaxTimeToOutputResponse = maxTimeToOutputResponse;
+    }
+
+    @Override
+    public void setRunName(String runName) {
+        mRunName = runName;
+    }
+
+    @Override
+    public void run(ITestRunListener... listeners)
+            throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException,
+            IOException {
+        run(Arrays.asList(listeners));
+    }
+
+    @Override
+    public void run(Collection<ITestRunListener> listeners)
+            throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException,
+            IOException {
+        final String runCaseCommandStr = String.format("am instrument -w -r %1$s %2$s",
+            getArgsCommand(), getRunnerPath());
+        Log.i(LOG_TAG, String.format("Running %1$s on %2$s", runCaseCommandStr,
+                mRemoteDevice.getSerialNumber()));
+        String runName = mRunName == null ? mPackageName : mRunName;
+        mParser = new InstrumentationResultParser(runName, listeners);
+
+        try {
+            mRemoteDevice.executeShellCommand(runCaseCommandStr, mParser, mMaxTimeToOutputResponse);
+        } catch (IOException e) {
+            Log.w(LOG_TAG, String.format("IOException %1$s when running tests %2$s on %3$s",
+                    e.toString(), getPackageName(), mRemoteDevice.getSerialNumber()));
+            // rely on parser to communicate results to listeners
+            mParser.handleTestRunFailed(e.toString());
+            throw e;
+        } catch (ShellCommandUnresponsiveException e) {
+            Log.w(LOG_TAG, String.format(
+                    "ShellCommandUnresponsiveException %1$s when running tests %2$s on %3$s",
+                    e.toString(), getPackageName(), mRemoteDevice.getSerialNumber()));
+            mParser.handleTestRunFailed(String.format(
+                    "Failed to receive adb shell test output within %1$d ms. " +
+                    "Test may have timed out, or adb connection to device became unresponsive",
+                    mMaxTimeToOutputResponse));
+            throw e;
+        } catch (TimeoutException e) {
+            Log.w(LOG_TAG, String.format(
+                    "TimeoutException when running tests %1$s on %2$s", getPackageName(),
+                    mRemoteDevice.getSerialNumber()));
+            mParser.handleTestRunFailed(e.toString());
+            throw e;
+        } catch (AdbCommandRejectedException e) {
+            Log.w(LOG_TAG, String.format(
+                    "AdbCommandRejectedException %1$s when running tests %2$s on %3$s",
+                    e.toString(), getPackageName(), mRemoteDevice.getSerialNumber()));
+            mParser.handleTestRunFailed(e.toString());
+            throw e;
+        }
+    }
+
+    @Override
+    public void cancel() {
+        if (mParser != null) {
+            mParser.cancel();
+        }
+    }
+
+    /**
+     * Returns the full instrumentation command line syntax for the provided instrumentation
+     * arguments.
+     * Returns an empty string if no arguments were specified.
+     */
+    private String getArgsCommand() {
+        StringBuilder commandBuilder = new StringBuilder();
+        for (Entry<String, String> argPair : mArgMap.entrySet()) {
+            final String argCmd = String.format(" -e %1$s %2$s", argPair.getKey(),
+                    argPair.getValue());
+            commandBuilder.append(argCmd);
+        }
+        return commandBuilder.toString();
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/testrunner/TestIdentifier.java b/ddmlib/src/main/java/com/android/ddmlib/testrunner/TestIdentifier.java
new file mode 100644
index 0000000..7de5736
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/testrunner/TestIdentifier.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2008 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.ddmlib.testrunner;
+
+/**
+ * Identifies a parsed instrumentation test.
+ */
+public class TestIdentifier {
+
+    private final String mClassName;
+    private final String mTestName;
+
+    /**
+     * Creates a test identifier.
+     *
+     * @param className fully qualified class name of the test. Cannot be null.
+     * @param testName name of the test. Cannot be null.
+     */
+    public TestIdentifier(String className, String testName) {
+        if (className == null || testName == null) {
+            throw new IllegalArgumentException("className and testName must " +
+                    "be non-null");
+        }
+        mClassName = className;
+        mTestName = testName;
+    }
+
+    /**
+     * Returns the fully qualified class name of the test.
+     */
+    public String getClassName() {
+        return mClassName;
+    }
+
+    /**
+     * Returns the name of the test.
+     */
+    public String getTestName() {
+        return mTestName;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((mClassName == null) ? 0 : mClassName.hashCode());
+        result = prime * result + ((mTestName == null) ? 0 : mTestName.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        TestIdentifier other = (TestIdentifier) obj;
+        if (mClassName == null) {
+            if (other.mClassName != null)
+                return false;
+        } else if (!mClassName.equals(other.mClassName))
+            return false;
+        if (mTestName == null) {
+            if (other.mTestName != null)
+                return false;
+        } else if (!mTestName.equals(other.mTestName))
+            return false;
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("%s#%s", getClassName(), getTestName());
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/testrunner/TestResult.java b/ddmlib/src/main/java/com/android/ddmlib/testrunner/TestResult.java
new file mode 100644
index 0000000..1056a84
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/testrunner/TestResult.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2010 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.ddmlib.testrunner;
+
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * Container for a result of a single test.
+ */
+public class TestResult {
+
+    public enum TestStatus {
+        /** Test error */
+        ERROR,
+        /** Test failed. */
+        FAILURE,
+        /** Test passed */
+        PASSED,
+        /** Test started but not ended */
+        INCOMPLETE
+    }
+
+    private TestStatus mStatus;
+    private String mStackTrace;
+    private Map<String, String> mMetrics;
+    // the start and end time of the test, measured via {@link System#currentTimeMillis()}
+    private long mStartTime = 0;
+    private long mEndTime = 0;
+
+    public TestResult() {
+        mStatus = TestStatus.INCOMPLETE;
+        mStartTime = System.currentTimeMillis();
+    }
+
+    /**
+     * Get the {@link TestStatus} result of the test.
+     */
+    public TestStatus getStatus() {
+        return mStatus;
+    }
+
+    /**
+     * Get the associated {@link String} stack trace. Should be <code>null</code> if
+     * {@link #getStatus()} is {@link TestStatus.PASSED}.
+     */
+    public String getStackTrace() {
+        return mStackTrace;
+    }
+
+    /**
+     * Get the associated test metrics.
+     */
+    public Map<String, String> getMetrics() {
+        return mMetrics;
+    }
+
+    /**
+     * Set the test metrics, overriding any previous values.
+     */
+    public void setMetrics(Map<String, String> metrics) {
+        mMetrics = metrics;
+    }
+
+    /**
+     * Return the {@link System#currentTimeMillis()} time that the
+     * {@link ITestInvocationListener#testStarted(TestIdentifier)} event was received.
+     */
+    public long getStartTime() {
+        return mStartTime;
+    }
+
+    /**
+     * Return the {@link System#currentTimeMillis()} time that the
+     * {@link ITestInvocationListener#testEnded(TestIdentifier)} event was received.
+     */
+    public long getEndTime() {
+        return mEndTime;
+    }
+
+    /**
+     * Set the {@link TestStatus}.
+     */
+    public TestResult setStatus(TestStatus status) {
+       mStatus = status;
+       return this;
+    }
+
+    /**
+     * Set the stack trace.
+     */
+    public void setStackTrace(String trace) {
+        mStackTrace = trace;
+    }
+
+    /**
+     * Sets the end time
+     */
+    public void setEndTime(long currentTimeMillis) {
+        mEndTime = currentTimeMillis;
+    }
+
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(new Object[] {mMetrics, mStackTrace, mStatus});
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        TestResult other = (TestResult) obj;
+        return equal(mMetrics, other.mMetrics) &&
+               equal(mStackTrace, other.mStackTrace) &&
+               equal(mStatus, other.mStatus);
+    }
+
+    private static boolean equal(Object a, Object b) {
+        return a == b || (a != null && a.equals(b));
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/testrunner/TestRunResult.java b/ddmlib/src/main/java/com/android/ddmlib/testrunner/TestRunResult.java
new file mode 100644
index 0000000..b194275
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/testrunner/TestRunResult.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright (C) 2010 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.ddmlib.testrunner;
+
+import com.android.ddmlib.Log;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Holds results from a single test run.
+ * <p/>
+ * Maintains an accurate count of tests during execution, and tracks incomplete tests.
+ */
+public class TestRunResult {
+    private static final String LOG_TAG = TestRunResult.class.getSimpleName();
+    private final String mTestRunName;
+    // Uses a synchronized map to make thread safe.
+    // Uses a LinkedHashMap to have predictable iteration order
+    private Map<TestIdentifier, TestResult> mTestResults =
+        Collections.synchronizedMap(new LinkedHashMap<TestIdentifier, TestResult>());
+    private Map<String, String> mRunMetrics = new HashMap<String, String>();
+    private boolean mIsRunComplete = false;
+    private long mElapsedTime = 0;
+    private int mNumFailedTests = 0;
+    private int mNumErrorTests = 0;
+    private int mNumPassedTests = 0;
+    private int mNumInCompleteTests = 0;
+    private String mRunFailureError = null;
+
+    /**
+     * Create a {@link TestRunResult}.
+     *
+     * @param runName
+     */
+    public TestRunResult(String runName) {
+        mTestRunName = runName;
+    }
+
+    /**
+     * Create an empty{@link TestRunResult}.
+     */
+    public TestRunResult() {
+        this("not started");
+    }
+
+    /**
+     * @return the test run name
+     */
+    public String getName() {
+        return mTestRunName;
+    }
+
+    /**
+     * Gets a map of the test results.
+     * @return
+     */
+    public Map<TestIdentifier, TestResult> getTestResults() {
+        return mTestResults;
+    }
+
+    /**
+     * Adds test run metrics.
+     * <p/>
+     * @param runMetrics the run metrics
+     * @param aggregateMetrics if <code>true</code>, attempt to add given metrics values to any
+     * currently stored values. If <code>false</code>, replace any currently stored metrics with
+     * the same key.
+     */
+    public void addMetrics(Map<String, String> runMetrics, boolean aggregateMetrics) {
+        if (aggregateMetrics) {
+            for (Map.Entry<String, String> entry : runMetrics.entrySet()) {
+                String existingValue = mRunMetrics.get(entry.getKey());
+                String combinedValue = combineValues(existingValue, entry.getValue());
+                mRunMetrics.put(entry.getKey(), combinedValue);
+            }
+        } else {
+            mRunMetrics.putAll(runMetrics);
+        }
+    }
+
+    /**
+     * Combine old and new metrics value
+     *
+     * @param existingValue
+     * @param value
+     * @return
+     */
+    private String combineValues(String existingValue, String newValue) {
+        if (existingValue != null) {
+            try {
+                Long existingLong = Long.parseLong(existingValue);
+                Long newLong = Long.parseLong(newValue);
+                return Long.toString(existingLong + newLong);
+            } catch (NumberFormatException e) {
+                // not a long, skip to next
+            }
+            try {
+               Double existingDouble = Double.parseDouble(existingValue);
+               Double newDouble = Double.parseDouble(newValue);
+               return Double.toString(existingDouble + newDouble);
+            } catch (NumberFormatException e) {
+                // not a double either, fall through
+            }
+        }
+        // default to overriding existingValue
+        return newValue;
+    }
+
+    /**
+     * @return a {@link Map} of the test test run metrics.
+     */
+    public Map<String, String> getRunMetrics() {
+        return mRunMetrics;
+    }
+
+    /**
+     * Gets the set of completed tests.
+     */
+    public Set<TestIdentifier> getCompletedTests() {
+        Set<TestIdentifier> completedTests = new LinkedHashSet<TestIdentifier>();
+        for (Map.Entry<TestIdentifier, TestResult> testEntry : getTestResults().entrySet()) {
+            if (!testEntry.getValue().getStatus().equals(TestStatus.INCOMPLETE)) {
+                completedTests.add(testEntry.getKey());
+            }
+        }
+        return completedTests;
+    }
+
+    /**
+     * @return <code>true</code> if test run failed.
+     */
+    public boolean isRunFailure() {
+        return mRunFailureError != null;
+    }
+
+    /**
+     * @return <code>true</code> if test run finished.
+     */
+    public boolean isRunComplete() {
+        return mIsRunComplete;
+    }
+
+    void setRunComplete(boolean runComplete) {
+        mIsRunComplete = runComplete;
+    }
+
+    void addElapsedTime(long elapsedTime) {
+        mElapsedTime+= elapsedTime;
+    }
+
+    void setRunFailureError(String errorMessage) {
+        mRunFailureError  = errorMessage;
+    }
+
+    /**
+     * Gets the number of passed tests for this run.
+     */
+    public int getNumPassedTests() {
+        return mNumPassedTests;
+    }
+
+    /**
+     * Gets the number of tests in this run.
+     */
+    public int getNumTests() {
+        return mTestResults.size();
+    }
+
+    /**
+     * Gets the number of complete tests in this run ie with status != incomplete.
+     */
+    public int getNumCompleteTests() {
+        return getNumTests() - getNumIncompleteTests();
+    }
+
+    /**
+     * Gets the number of failed tests in this run.
+     */
+    public int getNumFailedTests() {
+        return mNumFailedTests;
+    }
+
+    /**
+     * Gets the number of error tests in this run.
+     */
+    public int getNumErrorTests() {
+        return mNumErrorTests;
+    }
+
+    /**
+     * Gets the number of incomplete tests in this run.
+     */
+    public int getNumIncompleteTests() {
+        return mNumInCompleteTests;
+    }
+
+    /**
+     * @return <code>true</code> if test run had any failed or error tests.
+     */
+    public boolean hasFailedTests() {
+        return getNumErrorTests() > 0 || getNumFailedTests() > 0;
+    }
+
+    /**
+     * @return
+     */
+    public long getElapsedTime() {
+        return mElapsedTime;
+    }
+
+    /**
+     * Return the run failure error message, <code>null</code> if run did not fail.
+     */
+    public String getRunFailureMessage() {
+        return mRunFailureError;
+    }
+
+    /**
+     * Report the start of a test.
+     * @param test
+     */
+    void reportTestStarted(TestIdentifier test) {
+        TestResult result = mTestResults.get(test);
+
+        if (result != null) {
+            Log.d(LOG_TAG, String.format("Replacing result for %s", test));
+            switch (result.getStatus()) {
+                case ERROR:
+                    mNumErrorTests--;
+                    break;
+                case FAILURE:
+                    mNumFailedTests--;
+                    break;
+                case PASSED:
+                    mNumPassedTests--;
+                    break;
+                case INCOMPLETE:
+                    // ignore
+                    break;
+            }
+        } else {
+            mNumInCompleteTests++;
+        }
+        mTestResults.put(test, new TestResult());
+    }
+
+    /**
+     * Report a test failure.
+     *
+     * @param test
+     * @param status
+     * @param trace
+     */
+    void reportTestFailure(TestIdentifier test, TestStatus status, String trace) {
+        TestResult result = mTestResults.get(test);
+        if (result == null) {
+            Log.d(LOG_TAG, String.format("Received test failure for %s without testStarted", test));
+            result = new TestResult();
+            mTestResults.put(test, result);
+        } else if (result.getStatus().equals(TestStatus.PASSED)) {
+            // this should never happen...
+            Log.d(LOG_TAG, String.format("Replacing passed result for %s", test));
+            mNumPassedTests--;
+        }
+
+        result.setStackTrace(trace);
+        switch (status) {
+            case ERROR:
+                mNumErrorTests++;
+                result.setStatus(TestStatus.ERROR);
+                break;
+            case FAILURE:
+                result.setStatus(TestStatus.FAILURE);
+                mNumFailedTests++;
+                break;
+        }
+    }
+
+    /**
+     * Report the end of the test
+     *
+     * @param test
+     * @param testMetrics
+     * @return <code>true</code> if test was recorded as passed, false otherwise
+     */
+    boolean reportTestEnded(TestIdentifier test, Map<String, String> testMetrics) {
+        TestResult result = mTestResults.get(test);
+        if (result == null) {
+            Log.d(LOG_TAG, String.format("Received test ended for %s without testStarted", test));
+            result = new TestResult();
+            mTestResults.put(test, result);
+        } else {
+            mNumInCompleteTests--;
+        }
+
+        result.setEndTime(System.currentTimeMillis());
+        result.setMetrics(testMetrics);
+        if (result.getStatus().equals(TestStatus.INCOMPLETE)) {
+            result.setStatus(TestStatus.PASSED);
+            mNumPassedTests++;
+            return true;
+        }
+        return false;
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/testrunner/XmlTestRunListener.java b/ddmlib/src/main/java/com/android/ddmlib/testrunner/XmlTestRunListener.java
new file mode 100644
index 0000000..18ce841
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/testrunner/XmlTestRunListener.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2009 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.ddmlib.testrunner;
+
+import com.android.ddmlib.Log;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
+
+import org.kxml2.io.KXmlSerializer;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+
+/**
+ * Writes JUnit results to an XML files in a format consistent with
+ * Ant's XMLJUnitResultFormatter.
+ * <p/>
+ * Creates a separate XML file per test run.
+ * <p/>
+ */
+public class XmlTestRunListener implements ITestRunListener {
+
+    private static final String LOG_TAG = "XmlResultReporter";
+
+    private static final String TEST_RESULT_FILE_SUFFIX = ".xml";
+    private static final String TEST_RESULT_FILE_PREFIX = "test_result_";
+
+    private static final String TESTSUITE = "testsuite";
+    private static final String TESTCASE = "testcase";
+    private static final String ERROR = "error";
+    private static final String FAILURE = "failure";
+    private static final String ATTR_NAME = "name";
+    private static final String ATTR_TIME = "time";
+    private static final String ATTR_ERRORS = "errors";
+    private static final String ATTR_FAILURES = "failures";
+    private static final String ATTR_TESTS = "tests";
+    //private static final String ATTR_TYPE = "type";
+    //private static final String ATTR_MESSAGE = "message";
+    private static final String PROPERTIES = "properties";
+    private static final String ATTR_CLASSNAME = "classname";
+    private static final String TIMESTAMP = "timestamp";
+    private static final String HOSTNAME = "hostname";
+
+    /** the XML namespace */
+    private static final String ns = null;
+
+    private String mHostName = "localhost";
+
+    private File mReportDir = new File(System.getProperty("java.io.tmpdir"));
+
+    private String mReportPath = "";
+
+    private TestRunResult mRunResult = new TestRunResult();
+
+    /**
+     * Sets the report file to use.
+     */
+    public void setReportDir(File file) {
+        mReportDir = file;
+    }
+
+    public void setHostName(String hostName) {
+        mHostName = hostName;
+    }
+
+    /**
+     * Returns the {@link TestRunResult}
+     * @return the test run results.
+     */
+    public TestRunResult getRunResult() {
+        return mRunResult;
+    }
+
+    @Override
+    public void testRunStarted(String runName, int numTests) {
+        mRunResult = new TestRunResult(runName);
+    }
+
+    @Override
+    public void testStarted(TestIdentifier test) {
+       mRunResult.reportTestStarted(test);
+    }
+
+    @Override
+    public void testFailed(TestFailure status, TestIdentifier test, String trace) {
+        if (status.equals(TestFailure.ERROR)) {
+            mRunResult.reportTestFailure(test, TestStatus.ERROR, trace);
+        } else {
+            mRunResult.reportTestFailure(test, TestStatus.FAILURE, trace);
+        }
+        Log.d(LOG_TAG, String.format("%s %s: %s", test, status, trace));
+    }
+
+    @Override
+    public void testEnded(TestIdentifier test, Map<String, String> testMetrics) {
+        mRunResult.reportTestEnded(test, testMetrics);
+    }
+
+    @Override
+    public void testRunFailed(String errorMessage) {
+        mRunResult.setRunFailureError(errorMessage);
+    }
+
+    @Override
+    public void testRunStopped(long arg0) {
+        // ignore
+    }
+
+    @Override
+    public void testRunEnded(long elapsedTime, Map<String, String> runMetrics) {
+        mRunResult.setRunComplete(true);
+        generateDocument(mReportDir, elapsedTime);
+    }
+
+    /**
+     * Creates a report file and populates it with the report data from the completed tests.
+     */
+    private void generateDocument(File reportDir, long elapsedTime) {
+        String timestamp = getTimestamp();
+
+        OutputStream stream = null;
+        try {
+            stream = createOutputResultStream(reportDir);
+            KXmlSerializer serializer = new KXmlSerializer();
+            serializer.setOutput(stream, "UTF-8");
+            serializer.startDocument("UTF-8", null);
+            serializer.setFeature(
+                    "http://xmlpull.org/v1/doc/features.html#indent-output", true);
+            // TODO: insert build info
+            printTestResults(serializer, timestamp, elapsedTime);
+            serializer.endDocument();
+            String msg = String.format("XML test result file generated at %s. Total tests %d, " +
+                    "Failed %d, Error %d", getAbsoluteReportPath(), mRunResult.getNumTests(),
+                    mRunResult.getNumFailedTests(), mRunResult.getNumErrorTests());
+            Log.logAndDisplay(LogLevel.INFO, LOG_TAG, msg);
+        } catch (IOException e) {
+            Log.e(LOG_TAG, "Failed to generate report data");
+            // TODO: consider throwing exception
+        } finally {
+            if (stream != null) {
+                try {
+                    stream.close();
+                } catch (IOException ignored) {
+                }
+            }
+        }
+    }
+
+    private String getAbsoluteReportPath() {
+        return mReportPath ;
+    }
+
+    /**
+     * Return the current timestamp as a {@link String}.
+     */
+    String getTimestamp() {
+        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss",
+                Locale.getDefault());
+        TimeZone gmt = TimeZone.getTimeZone("UTC");
+        dateFormat.setTimeZone(gmt);
+        dateFormat.setLenient(true);
+        String timestamp = dateFormat.format(new Date());
+        return timestamp;
+    }
+
+    /**
+     * Creates a {@link File} where the report will be created.
+     * @param reportDir the root directory of the report.
+     * @return a file
+     * @throws IOException
+     */
+    protected File getResultFile(File reportDir) throws IOException {
+        File reportFile = File.createTempFile(TEST_RESULT_FILE_PREFIX, TEST_RESULT_FILE_SUFFIX,
+                reportDir);
+        Log.i(LOG_TAG, String.format("Created xml report file at %s",
+                reportFile.getAbsolutePath()));
+
+        return reportFile;
+    }
+
+    /**
+     * Creates the output stream to use for test results. Exposed for mocking.
+     */
+    OutputStream createOutputResultStream(File reportDir) throws IOException {
+        File reportFile = getResultFile(reportDir);
+        mReportPath = reportFile.getAbsolutePath();
+        return new BufferedOutputStream(new FileOutputStream(reportFile));
+    }
+
+    protected String getTestSuiteName() {
+        return mRunResult.getName();
+    }
+
+    void printTestResults(KXmlSerializer serializer, String timestamp, long elapsedTime)
+            throws IOException {
+        serializer.startTag(ns, TESTSUITE);
+        String name = getTestSuiteName();
+        if (name != null) {
+            serializer.attribute(ns, ATTR_NAME, name);
+        }
+        serializer.attribute(ns, ATTR_TESTS, Integer.toString(mRunResult.getNumTests()));
+        serializer.attribute(ns, ATTR_FAILURES, Integer.toString(mRunResult.getNumFailedTests()));
+        serializer.attribute(ns, ATTR_ERRORS, Integer.toString(mRunResult.getNumErrorTests()));
+        serializer.attribute(ns, ATTR_TIME, Double.toString((double) elapsedTime / 1000.f));
+        serializer.attribute(ns, TIMESTAMP, timestamp);
+        serializer.attribute(ns, HOSTNAME, mHostName);
+
+        serializer.startTag(ns, PROPERTIES);
+        setPropertiesAttributes(serializer, ns);
+        serializer.endTag(ns, PROPERTIES);
+
+        Map<TestIdentifier, TestResult> testResults = mRunResult.getTestResults();
+        for (Map.Entry<TestIdentifier, TestResult> testEntry : testResults.entrySet()) {
+            print(serializer, testEntry.getKey(), testEntry.getValue());
+        }
+
+        serializer.endTag(ns, TESTSUITE);
+    }
+
+    /**
+     * Sets the attributes on properties.
+     * @param serializer the serializer
+     * @param namespace the namespace
+     * @throws IOException
+     */
+    protected void setPropertiesAttributes(KXmlSerializer serializer, String namespace)
+            throws IOException {
+    }
+
+    protected String getTestName(TestIdentifier testId) {
+        return testId.getTestName();
+    }
+
+    void print(KXmlSerializer serializer, TestIdentifier testId, TestResult testResult)
+            throws IOException {
+
+        serializer.startTag(ns, TESTCASE);
+        serializer.attribute(ns, ATTR_NAME, getTestName(testId));
+        serializer.attribute(ns, ATTR_CLASSNAME, testId.getClassName());
+        long elapsedTimeMs = testResult.getEndTime() - testResult.getStartTime();
+        serializer.attribute(ns, ATTR_TIME, Double.toString((double) elapsedTimeMs / 1000.f));
+
+        if (!TestStatus.PASSED.equals(testResult.getStatus())) {
+            String result = testResult.getStatus().equals(TestStatus.FAILURE) ? FAILURE : ERROR;
+            serializer.startTag(ns, result);
+            // TODO: get message of stack trace ?
+//            String msg = testResult.getStackTrace();
+//            if (msg != null && msg.length() > 0) {
+//                serializer.attribute(ns, ATTR_MESSAGE, msg);
+//            }
+           // TODO: get class name of stackTrace exception
+            //serializer.attribute(ns, ATTR_TYPE, testId.getClassName());
+            String stackText = sanitize(testResult.getStackTrace());
+            serializer.text(stackText);
+            serializer.endTag(ns, result);
+        }
+
+        serializer.endTag(ns, TESTCASE);
+     }
+
+    /**
+     * Returns the text in a format that is safe for use in an XML document.
+     */
+    private String sanitize(String text) {
+        return text.replace("\0", "<\\0>");
+    }
+}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/utils/ArrayHelper.java b/ddmlib/src/main/java/com/android/ddmlib/utils/ArrayHelper.java
new file mode 100644
index 0000000..8167e5d
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/utils/ArrayHelper.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2007 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.ddmlib.utils;
+
+/**
+ * Utility class providing array to int/long conversion for data received from devices through adb. 
+ */
+public final class ArrayHelper {
+
+    /**
+     * Swaps an unsigned value around, and puts the result in an array that can be sent to a device.
+     * @param value The value to swap.
+     * @param dest the destination array
+     * @param offset the offset in the array where to put the swapped value.
+     *      Array length must be at least offset + 4
+     */
+    public static void swap32bitsToArray(int value, byte[] dest, int offset) {
+        dest[offset] = (byte)(value & 0x000000FF);
+        dest[offset + 1] = (byte)((value & 0x0000FF00) >> 8);
+        dest[offset + 2] = (byte)((value & 0x00FF0000) >> 16);
+        dest[offset + 3] = (byte)((value & 0xFF000000) >> 24);
+    }
+
+    /**
+     * Reads a signed 32 bit integer from an array coming from a device.
+     * @param value the array containing the int
+     * @param offset the offset in the array at which the int starts
+     * @return the integer read from the array
+     */
+    public static int swap32bitFromArray(byte[] value, int offset) {
+        int v = 0;
+        v |= ((int)value[offset]) & 0x000000FF;
+        v |= (((int)value[offset + 1]) & 0x000000FF) << 8;
+        v |= (((int)value[offset + 2]) & 0x000000FF) << 16;
+        v |= (((int)value[offset + 3]) & 0x000000FF) << 24;
+
+        return v;
+    }
+    
+    /**
+     * Reads an unsigned 16 bit integer from an array coming from a device,
+     * and returns it as an 'int'
+     * @param value the array containing the 16 bit int (2 byte).
+     * @param offset the offset in the array at which the int starts
+     *      Array length must be at least offset + 2
+     * @return the integer read from the array.
+     */
+    public static int swapU16bitFromArray(byte[] value, int offset) {
+        int v = 0;
+        v |= ((int)value[offset]) & 0x000000FF;
+        v |= (((int)value[offset + 1]) & 0x000000FF) << 8;
+
+        return v;
+    }
+    
+    /**
+     * Reads a signed 64 bit integer from an array coming from a device.
+     * @param value the array containing the int
+     * @param offset the offset in the array at which the int starts
+     *      Array length must be at least offset + 8
+     * @return the integer read from the array
+     */
+    public static long swap64bitFromArray(byte[] value, int offset) {
+        long v = 0;
+        v |= ((long)value[offset]) & 0x00000000000000FFL;
+        v |= (((long)value[offset + 1]) & 0x00000000000000FFL) << 8;
+        v |= (((long)value[offset + 2]) & 0x00000000000000FFL) << 16;
+        v |= (((long)value[offset + 3]) & 0x00000000000000FFL) << 24;
+        v |= (((long)value[offset + 4]) & 0x00000000000000FFL) << 32;
+        v |= (((long)value[offset + 5]) & 0x00000000000000FFL) << 40;
+        v |= (((long)value[offset + 6]) & 0x00000000000000FFL) << 48;
+        v |= (((long)value[offset + 7]) & 0x00000000000000FFL) << 56;
+
+        return v;
+    }
+}
diff --git a/ddms/app/.classpath b/ddms/app/.classpath
new file mode 100644
index 0000000..6c323f6
--- /dev/null
+++ b/ddms/app/.classpath
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src/main/java"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+	<classpathentry combineaccessrules="false" kind="src" path="/ddmlib"/>
+	<classpathentry combineaccessrules="false" kind="src" path="/ddmuilib"/>
+	<classpathentry combineaccessrules="false" kind="src" path="/sdkstats"/>
+	<classpathentry combineaccessrules="false" kind="src" path="/common"/>
+	<classpathentry combineaccessrules="false" kind="src" path="/swtmenubar"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/ddms/app/.project b/ddms/app/.project
new file mode 100644
index 0000000..ffb19d7
--- /dev/null
+++ b/ddms/app/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>ddms</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>
diff --git a/ddms/app/.settings/org.eclipse.jdt.core.prefs b/ddms/app/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..9dbff07
--- /dev/null
+++ b/ddms/app/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,98 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
+org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault
+org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled
+org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
+org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
+org.eclipse.jdt.core.compiler.problem.deadCode=warning
+org.eclipse.jdt.core.compiler.problem.deprecation=warning
+org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
+org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
+org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
+org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning
+org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled
+org.eclipse.jdt.core.compiler.problem.fieldHiding=warning
+org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
+org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=error
+org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
+org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled
+org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
+org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning
+org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning
+org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
+org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
+org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
+org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
+org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
+org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
+org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
+org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
+org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
+org.eclipse.jdt.core.compiler.problem.nullReference=error
+org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning
+org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore
+org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
+org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning
+org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
+org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
+org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
+org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled
+org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
+org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
+org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
+org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled
+org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning
+org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error
+org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning
+org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.unusedImport=warning
+org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
+org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
+org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning
+org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled
+org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
+org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
+org.eclipse.jdt.core.compiler.source=1.6
diff --git a/ddms/app/NOTICE b/ddms/app/NOTICE
new file mode 100644
index 0000000..c5b1efa
--- /dev/null
+++ b/ddms/app/NOTICE
@@ -0,0 +1,190 @@
+
+   Copyright (c) 2005-2008, 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.
+
+   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.
+
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
diff --git a/ddms/app/README b/ddms/app/README
new file mode 100644
index 0000000..0d9bbc4
--- /dev/null
+++ b/ddms/app/README
@@ -0,0 +1,75 @@
+Using the Eclipse project DDMS
+------------------------------
+
+DDMS requires some external libraries to compile.
+If you build DDMS using the makefile, you have nothing to configure.
+However if you want to develop on DDMS using Eclipse, you need to
+perform the following configuration.
+
+
+-------
+1- Projects required in Eclipse
+-------
+
+To run DDMS from Eclipse, you need to import the following 5 projects:
+
+  - sdk/androidpprefs:      project AndroidPrefs
+  - sdk/sdkstats:           project SdkStatsService
+  - sdk/ddms/app:           project Ddms
+  - sdk/ddms/libs/ddmlib:   project Ddmlib
+  - sdk/ddms/libs/ddmuilib: project Ddmuilib
+
+
+-------
+2- DDMS requires some SWT and OSGI JARs to compile.
+-------
+
+SWT is available in the tree under prebuild/<platform>/swt
+
+Because the build path cannot contain relative path that are not inside
+the project directory, the .classpath file references a user library
+called ANDROID_SWT.
+SWT depends on OSGI, so we'll also create an ANDROID_OSGI library for that.
+
+In order to compile the project:
+- Open Preferences > Java > Build Path > User Libraries
+
+- Create a new user library named ANDROID_SWT
+- Add the following 4 JAR files:
+
+  - prebuilt/<platform>/swt/swt.jar
+  - prebuilt/common/eclipse/org.eclipse.core.commands_3.*.jar
+  - prebuilt/common/eclipse/org.eclipse.equinox.common_3.*.jar
+  - prebuilt/common/eclipse/org.eclipse.jface_3.*.jar
+
+- Create a new user library named ANDROID_OSGI
+- Add the following JAR file:
+
+  - prebuilt/common/eclipse/org.eclipse.osgi_3.*.jar
+
+
+-------
+3- DDMS also requires the compiled SwtMenuBar library.
+-------
+
+Build the swtmenubar library:
+$ cd $TOP (top of Android tree)
+$ . build/envsetup.sh && lunch sdk-eng
+$ sdk/eclipse/scripts/create_sdkman_symlinks.sh
+
+Define a classpath variable in Eclipse:
+- Open Preferences > Java > Build Path > Classpath Variables
+- Create a new classpath variable named ANDROID_OUT_FRAMEWORK
+- Set its folder value to <Android tree>/out/host/<platform>/framework
+- Create a new classpath variable named ANDROID_SRC
+- Set its folder value to <Android tree>
+
+You might need to clean the ddms project (Project > Clean...) after
+you add the new classpath variable, otherwise previous errors might not
+go away automatically.
+
+The ANDROID_SRC part should be optional. It allows you to have access to
+the SwtMenuBar generic parts from the Java editor.
+
+--
+EOF
diff --git a/ddms/app/etc/ddms b/ddms/app/etc/ddms
new file mode 100755
index 0000000..79b93f9
--- /dev/null
+++ b/ddms/app/etc/ddms
@@ -0,0 +1,111 @@
+#!/bin/bash
+# Copyright 2005-2007, 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.
+
+# Set up prog to be the path of this script, including following symlinks,
+# and set up progdir to be the fully-qualified pathname of its directory.
+prog="$0"
+while [ -h "${prog}" ]; do
+    newProg=`/bin/ls -ld "${prog}"`
+    newProg=`expr "${newProg}" : ".* -> \(.*\)$"`
+    if expr "x${newProg}" : 'x/' >/dev/null; then
+        prog="${newProg}"
+    else
+        progdir=`dirname "${prog}"`
+        prog="${progdir}/${newProg}"
+    fi
+done
+oldwd=`pwd`
+progdir=`dirname "${prog}"`
+cd "${progdir}"
+progdir=`pwd`
+prog="${progdir}"/`basename "${prog}"`
+cd "${oldwd}"
+
+jarfile=ddms.jar
+frameworkdir="$progdir"
+libdir="$progdir"
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+    frameworkdir=`dirname "$progdir"`/tools/lib
+    libdir=`dirname "$progdir"`/tools/lib
+fi
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+    frameworkdir=`dirname "$progdir"`/framework
+    libdir=`dirname "$progdir"`/lib
+fi
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+    echo `basename "$prog"`": can't find $jarfile"
+    exit 1
+fi
+
+
+# Check args.
+if [ debug = "$1" ]; then
+    # add this in for debugging
+    java_debug=-agentlib:jdwp=transport=dt_socket,server=y,address=8050,suspend=y
+    shift 1
+else
+    java_debug=
+fi
+
+javaCmd="java"
+
+# Mac OS X needs an additional arg, or you get an "illegal thread" complaint.
+if [ `uname` = "Darwin" ]; then
+    os_opts="-XstartOnFirstThread"
+else
+    os_opts=
+fi
+
+if [ `uname` = "Linux" ]; then
+    export GDK_NATIVE_WINDOWS=true
+fi
+
+jarpath="$frameworkdir/$jarfile:$frameworkdir/swtmenubar.jar"
+
+# Figure out the path to the swt.jar for the current architecture.
+# if ANDROID_SWT is defined, then just use this.
+# else, if running in the Android source tree, then look for the correct swt folder in prebuilt
+# else, look for the correct swt folder in the SDK under tools/lib/
+swtpath=""
+if [ -n "$ANDROID_SWT" ]; then
+    swtpath="$ANDROID_SWT"
+else
+    vmarch=`${javaCmd} -jar "${frameworkdir}"/archquery.jar`
+    if [ -n "$ANDROID_BUILD_TOP" ]; then
+        osname=`uname -s | tr A-Z a-z`
+        swtpath="${ANDROID_BUILD_TOP}/prebuilts/tools/${osname}-${vmarch}/swt"
+    else
+        swtpath="${frameworkdir}/${vmarch}"
+    fi
+fi
+
+if [ ! -d "$swtpath" ]; then
+    echo "SWT folder '${swtpath}' does not exist."
+    echo "Please export ANDROID_SWT to point to the folder containing swt.jar for your platform."
+    exit 1
+fi
+
+if [ -x $progdir/monitor ]; then
+    echo "The standalone version of DDMS is deprecated."
+    echo "Please use Android Device Monitor (tools/monitor) instead."
+fi
+exec "$javaCmd" \
+    -Xmx256M $os_opts $java_debug \
+    -Dcom.android.ddms.bindir="$progdir" \
+    -classpath "$jarpath:$swtpath/swt.jar" \
+    com.android.ddms.Main "$@"
diff --git a/ddms/app/etc/ddms.bat b/ddms/app/etc/ddms.bat
new file mode 100755
index 0000000..d710ea6
--- /dev/null
+++ b/ddms/app/etc/ddms.bat
@@ -0,0 +1,74 @@
+ at echo off
+rem Copyright (C) 2007 The Android Open Source Project
+rem
+rem Licensed under the Apache License, Version 2.0 (the "License");
+rem you may not use this file except in compliance with the License.
+rem You may obtain a copy of the License at
+rem
+rem      http://www.apache.org/licenses/LICENSE-2.0
+rem
+rem Unless required by applicable law or agreed to in writing, software
+rem distributed under the License is distributed on an "AS IS" BASIS,
+rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+rem See the License for the specific language governing permissions and
+rem limitations under the License.
+
+rem don't modify the caller's environment
+setlocal
+
+rem Set up prog to be the path of this script, including following symlinks,
+rem and set up progdir to be the fully-qualified pathname of its directory.
+set prog=%~f0
+
+rem Change current directory and drive to where the script is, to avoid
+rem issues with directories containing whitespaces.
+cd /d %~dp0
+
+rem Get the CWD as a full path with short names only (without spaces)
+for %%i in ("%cd%") do set prog_dir=%%~fsi
+
+rem Check we have a valid Java.exe in the path.
+set java_exe=
+call lib\find_java.bat
+if not defined java_exe goto :EOF
+
+set jarfile=ddms.jar
+set frameworkdir=
+
+if exist %frameworkdir%%jarfile% goto JarFileOk
+    set frameworkdir=lib\
+
+if exist %frameworkdir%%jarfile% goto JarFileOk
+    set frameworkdir=..\framework\
+
+:JarFileOk
+
+if debug NEQ "%1" goto NoDebug
+    set java_debug=-agentlib:jdwp=transport=dt_socket,server=y,address=8050,suspend=y
+    shift 1
+:NoDebug
+
+set jarpath=%frameworkdir%%jarfile%;%frameworkdir%swtmenubar.jar
+
+if not defined ANDROID_SWT goto QueryArch
+    set swt_path=%ANDROID_SWT%
+    goto SwtDone
+
+:QueryArch
+
+    for /f %%a in ('%java_exe% -jar %frameworkdir%archquery.jar') do set swt_path=%frameworkdir%%%a
+
+:SwtDone
+
+if exist %swt_path% goto SetPath
+    echo SWT folder '%swt_path%' does not exist.
+    echo Please set ANDROID_SWT to point to the folder containing swt.jar for your platform.
+    exit /B
+
+:SetPath
+set javaextdirs=%swt_path%;%frameworkdir%
+
+echo The standalone version of DDMS is deprecated.
+echo Please use Android Device Monitor (monitor.bat) instead.
+call %java_exe% %java_debug% -Dcom.android.ddms.bindir=%prog_dir% -classpath "%jarpath%;%swt_path%\swt.jar" com.android.ddms.Main %*
+
diff --git a/ddms/app/src/main/java/com/android/ddms/AboutDialog.java b/ddms/app/src/main/java/com/android/ddms/AboutDialog.java
new file mode 100644
index 0000000..b3ddff7
--- /dev/null
+++ b/ddms/app/src/main/java/com/android/ddms/AboutDialog.java
@@ -0,0 +1,158 @@
+/* //device/tools/ddms/src/com/android/ddms/AboutDialog.java
+**
+** Copyright 2007, 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.ddms;
+
+import com.android.ddmlib.Log;
+import com.android.ddmuilib.ImageLoader;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+import java.io.InputStream;
+
+/**
+ * Our "about" box.
+ */
+public class AboutDialog extends Dialog {
+
+    private Image logoImage;
+
+    /**
+     * Create with default style.
+     */
+    public AboutDialog(Shell parent) {
+        this(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL);
+    }
+
+    /**
+     * Create with app-defined style.
+     */
+    public AboutDialog(Shell parent, int style) {
+        super(parent, style);
+    }
+
+    /**
+     * Prepare and display the dialog.
+     */
+    public void open() {
+        Shell parent = getParent();
+        Shell shell = new Shell(parent, getStyle());
+        shell.setText("About...");
+
+        logoImage = loadImage(shell, "ddms-128.png"); //$NON-NLS-1$
+        createContents(shell);
+        shell.pack();
+
+        shell.open();
+        Display display = parent.getDisplay();
+        while (!shell.isDisposed()) {
+            if (!display.readAndDispatch())
+                display.sleep();
+        }
+
+        logoImage.dispose();
+    }
+
+    /*
+     * Load an image file from a resource.
+     *
+     * This depends on Display, so I'm not sure what the rules are for
+     * loading once and caching in a static class field.
+     */
+    private Image loadImage(Shell shell, String fileName) {
+        InputStream imageStream;
+        String pathName = "/images/" + fileName;  //$NON-NLS-1$
+
+        imageStream = this.getClass().getResourceAsStream(pathName);
+        if (imageStream == null) {
+            //throw new NullPointerException("couldn't find " + pathName);
+            Log.w("ddms", "Couldn't load " + pathName);
+            Display display = shell.getDisplay();
+            return ImageLoader.createPlaceHolderArt(display, 100, 50,
+                    display.getSystemColor(SWT.COLOR_BLUE));
+        }
+
+        Image img = new Image(shell.getDisplay(), imageStream);
+        if (img == null)
+            throw new NullPointerException("couldn't load " + pathName);
+        return img;
+    }
+
+    /*
+     * Create the about box contents.
+     */
+    private void createContents(final Shell shell) {
+        GridLayout layout;
+        GridData data;
+        Label label;
+
+        shell.setLayout(new GridLayout(2, false));
+
+        // Fancy logo
+        Label logo = new Label(shell, SWT.BORDER);
+        logo.setImage(logoImage);
+
+        // Text Area
+        Composite textArea = new Composite(shell, SWT.NONE);
+        layout = new GridLayout(1, true);
+        textArea.setLayout(layout);
+
+        // Text lines
+        label = new Label(textArea, SWT.NONE);
+        if (Main.sRevision != null && Main.sRevision.length() > 0) {
+            label.setText("Dalvik Debug Monitor Revision " + Main.sRevision);
+        } else {
+            label.setText("Dalvik Debug Monitor");
+        }
+        label = new Label(textArea, SWT.NONE);
+        // TODO: update with new year date (search this to find other occurrences to update)
+        label.setText("Copyright 2007-2012, The Android Open Source Project");
+        label = new Label(textArea, SWT.NONE);
+        label.setText("All Rights Reserved.");
+
+        // blank spot in grid
+        label = new Label(shell, SWT.NONE);
+
+        // "OK" button
+        Button ok = new Button(shell, SWT.PUSH);
+        ok.setText("OK");
+        data = new GridData(GridData.HORIZONTAL_ALIGN_END);
+        data.widthHint = 80;
+        ok.setLayoutData(data);
+        ok.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                shell.close();
+            }
+        });
+
+        shell.pack();
+
+        shell.setDefaultButton(ok);
+    }
+}
diff --git a/ddms/app/src/main/java/com/android/ddms/DebugPortProvider.java b/ddms/app/src/main/java/com/android/ddms/DebugPortProvider.java
new file mode 100644
index 0000000..2dcd5d4
--- /dev/null
+++ b/ddms/app/src/main/java/com/android/ddms/DebugPortProvider.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2007 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.ddms;
+
+import com.android.ddmlib.DebugPortManager.IDebugPortProvider;
+import com.android.ddmlib.IDevice;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * DDMS implementation of the IDebugPortProvider interface.
+ * This class handles saving/loading the list of static debug port from
+ * the preference store and provides the port number to the Device Monitor.
+ */
+public class DebugPortProvider implements IDebugPortProvider {
+
+    private static DebugPortProvider sThis  = new DebugPortProvider();
+
+    /** Preference name for the static port list. */
+    public static final String PREFS_STATIC_PORT_LIST = "android.staticPortList"; //$NON-NLS-1$
+
+    /**
+     * Mapping device serial numbers to maps. The embedded maps are mapping application names to
+     * debugger ports.
+     */
+    private Map<String, Map<String, Integer>> mMap;
+
+    public static DebugPortProvider getInstance() {
+        return sThis;
+    }
+
+    private DebugPortProvider() {
+        computePortList();
+    }
+
+    /**
+         * Returns a static debug port for the specified application running on the
+         * specified {@link IDevice}.
+         * @param device The device the application is running on.
+         * @param appName The application name, as defined in the
+         *  AndroidManifest.xml package attribute.
+         * @return The static debug port or {@link #NO_STATIC_PORT} if there is none setup.
+     *
+     * @see IDebugPortProvider#getPort(IDevice, String)
+     */
+    @Override
+    public int getPort(IDevice device, String appName) {
+        if (mMap != null) {
+            Map<String, Integer> deviceMap = mMap.get(device.getSerialNumber());
+            if (deviceMap != null) {
+                Integer i = deviceMap.get(appName);
+                if (i != null) {
+                    return i.intValue();
+                }
+            }
+        }
+        return IDebugPortProvider.NO_STATIC_PORT;
+    }
+
+    /**
+     * Returns the map of Static debugger ports. The map links device serial numbers to
+     * a map linking application name to debugger ports.
+     */
+    public Map<String, Map<String, Integer>> getPortList() {
+        return mMap;
+    }
+
+    /**
+     * Create the map member from the values contained in the Preference Store.
+     */
+    private void computePortList() {
+        mMap = new HashMap<String, Map<String, Integer>>();
+
+        // get the prefs store
+        IPreferenceStore store = PrefsDialog.getStore();
+        String value = store.getString(PREFS_STATIC_PORT_LIST);
+
+        if (value != null && value.length() > 0) {
+            // format is
+            // port1|port2|port3|...
+            // where port# is
+            // appPackageName:appPortNumber:device-serial-number
+            String[] portSegments = value.split("\\|");  //$NON-NLS-1$
+            for (String seg : portSegments) {
+                String[] entry = seg.split(":");  //$NON-NLS-1$
+
+                // backward compatibility support. if we have only 2 entry, we default
+                // to the first emulator.
+                String deviceName = null;
+                if (entry.length == 3) {
+                    deviceName = entry[2];
+                } else {
+                    deviceName = IDevice.FIRST_EMULATOR_SN;
+                }
+
+                // get the device map
+                Map<String, Integer> deviceMap = mMap.get(deviceName);
+                if (deviceMap == null) {
+                    deviceMap = new HashMap<String, Integer>();
+                    mMap.put(deviceName, deviceMap);
+                }
+
+                deviceMap.put(entry[0], Integer.valueOf(entry[1]));
+            }
+        }
+    }
+
+    /**
+     * Sets new [device, app, port] values.
+     * The values are also sync'ed in the preference store.
+     * @param map The map containing the new values.
+     */
+    public void setPortList(Map<String, Map<String,Integer>> map) {
+        // update the member map.
+        mMap.clear();
+        mMap.putAll(map);
+
+        // create the value to store in the preference store.
+        // see format definition in getPortList
+        StringBuilder sb = new StringBuilder();
+
+        Set<String> deviceKeys = map.keySet();
+        for (String deviceKey : deviceKeys) {
+            Map<String, Integer> deviceMap = map.get(deviceKey);
+            if (deviceMap != null) {
+                Set<String> appKeys = deviceMap.keySet();
+
+                for (String appKey : appKeys) {
+                    Integer port = deviceMap.get(appKey);
+                    if (port != null) {
+                        sb.append(appKey).append(':').append(port.intValue()).append(':').
+                            append(deviceKey).append('|');
+                    }
+                }
+            }
+        }
+
+        String value = sb.toString();
+
+        // get the prefs store.
+        IPreferenceStore store = PrefsDialog.getStore();
+
+        // and give it the new value.
+        store.setValue(PREFS_STATIC_PORT_LIST, value);
+    }
+}
diff --git a/ddms/app/src/main/java/com/android/ddms/DeviceCommandDialog.java b/ddms/app/src/main/java/com/android/ddms/DeviceCommandDialog.java
new file mode 100644
index 0000000..6775cbb
--- /dev/null
+++ b/ddms/app/src/main/java/com/android/ddms/DeviceCommandDialog.java
@@ -0,0 +1,441 @@
+/* //device/tools/ddms/src/com/android/ddms/DeviceCommandDialog.java
+**
+** Copyright 2007, 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.ddms;
+
+import com.android.ddmlib.AdbCommandRejectedException;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.IShellOutputReceiver;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.ShellCommandUnresponsiveException;
+import com.android.ddmlib.TimeoutException;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+import java.io.BufferedOutputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+
+/**
+ * Execute a command on an ADB-attached device and save the output.
+ *
+ * There are several ways to do this.  One is to run a single command
+ * and show the output.  Another is to have several possible commands and
+ * let the user click a button next to the one (or ones) they want.  This
+ * currently uses the simple 1:1 form.
+ */
+public class DeviceCommandDialog extends Dialog {
+
+    public static final int DEVICE_STATE = 0;
+    public static final int APP_STATE = 1;
+    public static final int RADIO_STATE = 2;
+    public static final int LOGCAT = 3;
+
+    private String mCommand;
+    private String mFileName;
+
+    private Label mStatusLabel;
+    private Button mCancelDone;
+    private Button mSave;
+    private Text mText;
+    private Font mFont = null;
+    private boolean mCancel;
+    private boolean mFinished;
+
+
+    /**
+     * Create with default style.
+     */
+    public DeviceCommandDialog(String command, String fileName, Shell parent) {
+        // don't want a close button, but it seems hard to get rid of on GTK
+        // keep it on all platforms for consistency
+        this(command, fileName, parent,
+            SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL | SWT.RESIZE);
+    }
+
+    /**
+     * Create with app-defined style.
+     */
+    public DeviceCommandDialog(String command, String fileName, Shell parent,
+        int style)
+    {
+        super(parent, style);
+        mCommand = command;
+        mFileName = fileName;
+    }
+
+    /**
+     * Prepare and display the dialog.
+     * @param currentDevice
+     */
+    public void open(IDevice currentDevice) {
+        Shell parent = getParent();
+        Shell shell = new Shell(parent, getStyle());
+        shell.setText("Remote Command");
+
+        mFinished = false;
+        mFont = findFont(shell.getDisplay());
+        createContents(shell);
+
+        // Getting weird layout behavior under Linux when Text is added --
+        // looks like text widget has min width of 400 when FILL_HORIZONTAL
+        // is used, and layout gets tweaked to force this.  (Might be even
+        // more with the scroll bars in place -- it wigged out when the
+        // file save dialog was invoked.)
+        shell.setMinimumSize(500, 200);
+        shell.setSize(800, 600);
+        shell.open();
+
+        executeCommand(shell, currentDevice);
+
+        Display display = parent.getDisplay();
+        while (!shell.isDisposed()) {
+            if (!display.readAndDispatch())
+                display.sleep();
+        }
+
+        if (mFont != null)
+            mFont.dispose();
+    }
+
+    /*
+     * Create a text widget to show the output and some buttons to
+     * manage things.
+     */
+    private void createContents(final Shell shell) {
+        GridData data;
+
+        shell.setLayout(new GridLayout(2, true));
+
+        shell.addListener(SWT.Close, new Listener() {
+            @Override
+            public void handleEvent(Event event) {
+                if (!mFinished) {
+                    Log.d("ddms", "NOT closing - cancelling command");
+                    event.doit = false;
+                    mCancel = true;
+                }
+            }
+        });
+
+        mStatusLabel = new Label(shell, SWT.NONE);
+        mStatusLabel.setText("Executing '" + shortCommandString() + "'");
+        data = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING);
+        data.horizontalSpan = 2;
+        mStatusLabel.setLayoutData(data);
+
+        mText = new Text(shell, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
+        mText.setEditable(false);
+        mText.setFont(mFont);
+        data = new GridData(GridData.FILL_BOTH);
+        data.horizontalSpan = 2;
+        mText.setLayoutData(data);
+
+        // "save" button
+        mSave = new Button(shell, SWT.PUSH);
+        mSave.setText("Save");
+        data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
+        data.widthHint = 80;
+        mSave.setLayoutData(data);
+        mSave.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                saveText(shell);
+            }
+        });
+        mSave.setEnabled(false);
+
+        // "cancel/done" button
+        mCancelDone = new Button(shell, SWT.PUSH);
+        mCancelDone.setText("Cancel");
+        data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
+        data.widthHint = 80;
+        mCancelDone.setLayoutData(data);
+        mCancelDone.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                if (!mFinished)
+                    mCancel = true;
+                else
+                    shell.close();
+            }
+        });
+    }
+
+    /*
+     * Figure out what font to use.
+     *
+     * Returns "null" if we can't figure it out, which SWT understands to
+     * mean "use default system font".
+     */
+    private Font findFont(Display display) {
+        String fontStr = PrefsDialog.getStore().getString("textOutputFont");
+        if (fontStr != null) {
+            FontData fdat = new FontData(fontStr);
+            if (fdat != null)
+                return new Font(display, fdat);
+        }
+        return null;
+    }
+
+
+    /*
+     * Callback class for command execution.
+     */
+    class Gatherer extends Thread implements IShellOutputReceiver {
+        public static final int RESULT_UNKNOWN = 0;
+        public static final int RESULT_SUCCESS = 1;
+        public static final int RESULT_FAILURE = 2;
+        public static final int RESULT_CANCELLED = 3;
+
+        private Shell mShell;
+        private String mCommand;
+        private Text mText;
+        private int mResult;
+        private IDevice mDevice;
+
+        /**
+         * Constructor; pass in the text widget that will receive the output.
+         * @param device
+         */
+        public Gatherer(Shell shell, IDevice device, String command, Text text) {
+            mShell = shell;
+            mDevice = device;
+            mCommand = command;
+            mText = text;
+            mResult = RESULT_UNKNOWN;
+
+            // this is in outer class
+            mCancel = false;
+        }
+
+        /**
+         * Thread entry point.
+         */
+        @Override
+        public void run() {
+
+            if (mDevice == null) {
+                Log.w("ddms", "Cannot execute command: no device selected.");
+                mResult = RESULT_FAILURE;
+            } else {
+                try {
+                    mDevice.executeShellCommand(mCommand, this);
+                    if (mCancel)
+                        mResult = RESULT_CANCELLED;
+                    else
+                        mResult = RESULT_SUCCESS;
+                }
+                catch (IOException ioe) {
+                    Log.w("ddms", "Remote exec failed: " + ioe.getMessage());
+                    mResult = RESULT_FAILURE;
+                } catch (TimeoutException e) {
+                    Log.w("ddms", "Remote exec failed: " + e.getMessage());
+                    mResult = RESULT_FAILURE;
+                } catch (AdbCommandRejectedException e) {
+                    Log.w("ddms", "Remote exec failed: " + e.getMessage());
+                    mResult = RESULT_FAILURE;
+                } catch (ShellCommandUnresponsiveException e) {
+                    Log.w("ddms", "Remote exec failed: " + e.getMessage());
+                    mResult = RESULT_FAILURE;
+                }
+            }
+
+            mShell.getDisplay().asyncExec(new Runnable() {
+                @Override
+                public void run() {
+                    updateForResult(mResult);
+                }
+            });
+        }
+
+        /**
+         * Called by executeRemoteCommand().
+         */
+        @Override
+        public void addOutput(byte[] data, int offset, int length) {
+
+            Log.v("ddms", "received " + length + " bytes");
+            try {
+                final String text;
+                text = new String(data, offset, length, "ISO-8859-1");
+
+                // add to text widget; must do in UI thread
+                mText.getDisplay().asyncExec(new Runnable() {
+                    @Override
+                    public void run() {
+                        mText.append(text);
+                    }
+                });
+            }
+            catch (UnsupportedEncodingException uee) {
+                uee.printStackTrace();      // not expected
+            }
+        }
+
+        @Override
+        public void flush() {
+            // nothing to flush.
+        }
+
+        /**
+         * Called by executeRemoteCommand().
+         */
+        @Override
+        public boolean isCancelled() {
+            return mCancel;
+        }
+    };
+
+    /*
+     * Execute a remote command, add the output to the text widget, and
+     * update controls.
+     *
+     * We have to run the command in a thread so that the UI continues
+     * to work.
+     */
+    private void executeCommand(Shell shell, IDevice device) {
+        Gatherer gath = new Gatherer(shell, device, commandString(), mText);
+        gath.start();
+    }
+
+    /*
+     * Update the controls after the remote operation completes.  This
+     * must be called from the UI thread.
+     */
+    private void updateForResult(int result) {
+        if (result == Gatherer.RESULT_SUCCESS) {
+            mStatusLabel.setText("Successfully executed '"
+                + shortCommandString() + "'");
+            mSave.setEnabled(true);
+        } else if (result == Gatherer.RESULT_CANCELLED) {
+            mStatusLabel.setText("Execution cancelled; partial results below");
+            mSave.setEnabled(true);     // save partial
+        } else if (result == Gatherer.RESULT_FAILURE) {
+            mStatusLabel.setText("Failed");
+        }
+        mStatusLabel.pack();
+        mCancelDone.setText("Done");
+        mFinished = true;
+    }
+
+    /*
+     * Allow the user to save the contents of the text dialog.
+     */
+    private void saveText(Shell shell) {
+        FileDialog dlg = new FileDialog(shell, SWT.SAVE);
+        String fileName;
+
+        dlg.setText("Save output...");
+        dlg.setFileName(defaultFileName());
+        dlg.setFilterPath(PrefsDialog.getStore().getString("lastTextSaveDir"));
+        dlg.setFilterNames(new String[] {
+            "Text Files (*.txt)"
+        });
+        dlg.setFilterExtensions(new String[] {
+            "*.txt"
+        });
+
+        fileName = dlg.open();
+        if (fileName != null) {
+            PrefsDialog.getStore().setValue("lastTextSaveDir",
+                                            dlg.getFilterPath());
+
+            Log.d("ddms", "Saving output to " + fileName);
+
+            /*
+             * Convert to 8-bit characters.
+             */
+            String text = mText.getText();
+            byte[] ascii;
+            try {
+                ascii = text.getBytes("ISO-8859-1");
+            }
+            catch (UnsupportedEncodingException uee) {
+                uee.printStackTrace();
+                ascii = new byte[0];
+            }
+
+            /*
+             * Output data, converting CRLF to LF.
+             */
+            try {
+                int length = ascii.length;
+
+                FileOutputStream outFile = new FileOutputStream(fileName);
+                BufferedOutputStream out = new BufferedOutputStream(outFile);
+                for (int i = 0; i < length; i++) {
+                    if (i < length-1 &&
+                        ascii[i] == 0x0d && ascii[i+1] == 0x0a)
+                    {
+                        continue;
+                    }
+                    out.write(ascii[i]);
+                }
+                out.close();        // flush buffer, close file
+            }
+            catch (IOException ioe) {
+                Log.w("ddms", "Unable to save " + fileName + ": " + ioe);
+            }
+        }
+    }
+
+
+    /*
+     * Return the shell command we're going to use.
+     */
+    private String commandString() {
+        return mCommand;
+
+    }
+
+    /*
+     * Return a default filename for the "save" command.
+     */
+    private String defaultFileName() {
+        return mFileName;
+    }
+
+    /*
+     * Like commandString(), but length-limited.
+     */
+    private String shortCommandString() {
+        String str = commandString();
+        if (str.length() > 50)
+            return str.substring(0, 50) + "...";
+        else
+            return str;
+    }
+}
+
diff --git a/ddms/app/src/main/java/com/android/ddms/DropdownSelectionListener.java b/ddms/app/src/main/java/com/android/ddms/DropdownSelectionListener.java
new file mode 100644
index 0000000..04d921c
--- /dev/null
+++ b/ddms/app/src/main/java/com/android/ddms/DropdownSelectionListener.java
@@ -0,0 +1,80 @@
+/* //device/tools/ddms/src/com/android/ddms/DropdownSelectionListener.java
+**
+** Copyright 2007, 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.ddms;
+
+import com.android.ddmlib.Log;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
+import org.eclipse.swt.widgets.ToolItem;
+
+/**
+ * Helper class for drop-down menus in toolbars.
+ */
+public class DropdownSelectionListener extends SelectionAdapter {
+    private Menu mMenu;
+    private ToolItem mDropdown;
+
+    /**
+     * Basic constructor.  Creates an empty Menu to hold items.
+     */
+    public DropdownSelectionListener(ToolItem item) {
+        mDropdown = item;
+        mMenu = new Menu(item.getParent().getShell(), SWT.POP_UP);
+    }
+
+    /**
+     * Add an item to the dropdown menu.
+     */
+    public void add(String label) {
+        MenuItem item = new MenuItem(mMenu, SWT.NONE);
+        item.setText(label);
+        item.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                // update the dropdown's text to match the selection
+                MenuItem sel = (MenuItem) e.widget;
+                mDropdown.setText(sel.getText());
+            }
+        });
+    }
+
+    /**
+     * Invoked when dropdown or neighboring arrow is clicked.
+     */
+    @Override
+    public void widgetSelected(SelectionEvent e) {
+        if (e.detail == SWT.ARROW) {
+            // arrow clicked, show menu
+            ToolItem item = (ToolItem) e.widget;
+            Rectangle rect = item.getBounds();
+            Point pt = item.getParent().toDisplay(new Point(rect.x, rect.y));
+            mMenu.setLocation(pt.x, pt.y + rect.height);
+            mMenu.setVisible(true);
+        } else {
+            // button clicked
+            Log.d("ddms", mDropdown.getText() + " Pressed");
+        }
+    }
+}
+
diff --git a/ddms/app/src/main/java/com/android/ddms/Main.java b/ddms/app/src/main/java/com/android/ddms/Main.java
new file mode 100644
index 0000000..bfdb78b
--- /dev/null
+++ b/ddms/app/src/main/java/com/android/ddms/Main.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2007 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.ddms;
+
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.DebugPortManager;
+import com.android.ddmlib.Log;
+import com.android.sdkstats.SdkStatsService;
+
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.lang.management.ManagementFactory;
+import java.lang.management.RuntimeMXBean;
+import java.util.Properties;
+
+
+/**
+ * Start the UI and network.
+ */
+public class Main {
+
+    public static String sRevision;
+
+    public Main() {
+    }
+
+    /*
+     * If a thread bails with an uncaught exception, bring the whole
+     * thing down.
+     */
+    private static class UncaughtHandler implements Thread.UncaughtExceptionHandler {
+        @Override
+        public void uncaughtException(Thread t, Throwable e) {
+            Log.e("ddms", "shutting down due to uncaught exception");
+            Log.e("ddms", e);
+            System.exit(1);
+        }
+    }
+
+    /**
+     * Parse args, start threads.
+     */
+    public static void main(String[] args) {
+        // In order to have the AWT/SWT bridge work on Leopard, we do this little hack.
+        if (isMac()) {
+            RuntimeMXBean rt = ManagementFactory.getRuntimeMXBean();
+            System.setProperty(
+                    "JAVA_STARTED_ON_FIRST_THREAD_" + (rt.getName().split("@"))[0], //$NON-NLS-1$
+                    "1"); //$NON-NLS-1$
+        }
+
+        Thread.setDefaultUncaughtExceptionHandler(new UncaughtHandler());
+
+        // load prefs and init the default values
+        PrefsDialog.init();
+
+        Log.d("ddms", "Initializing");
+
+        // Create an initial shell display with the correct app name.
+        Display.setAppName(UIThread.APP_NAME);
+        Shell shell = new Shell(Display.getDefault());
+
+        // if this is the first time using ddms or adt, open up the stats service
+        // opt out dialog, and request user for permissions.
+        SdkStatsService stats = new SdkStatsService();
+        stats.checkUserPermissionForPing(shell);
+
+        // the "ping" argument means to check in with the server and exit
+        // the application name and version number must also be supplied
+        if (args.length >= 3 && args[0].equals("ping")) {
+            stats.ping(args);
+            return;
+        } else if (args.length > 0) {
+            Log.e("ddms", "Unknown argument: " + args[0]);
+            System.exit(1);
+        }
+
+        // get the ddms parent folder location
+        String ddmsParentLocation = System.getProperty("com.android.ddms.bindir"); //$NON-NLS-1$
+
+        if (ddmsParentLocation == null) {
+            // Tip: for debugging DDMS in eclipse, set this env var to the SDK/tools
+            // directory path.
+            ddmsParentLocation = System.getenv("com.android.ddms.bindir"); //$NON-NLS-1$
+        }
+
+        // we're past the point where ddms can be called just to send a ping, so we can
+        // ping for ddms itself.
+        ping(stats, ddmsParentLocation);
+        stats = null;
+
+        DebugPortManager.setProvider(DebugPortProvider.getInstance());
+
+        // create the three main threads
+        UIThread ui = UIThread.getInstance();
+
+        try {
+            ui.runUI(ddmsParentLocation);
+        } finally {
+            PrefsDialog.save();
+
+            AndroidDebugBridge.terminate();
+        }
+
+        Log.d("ddms", "Bye");
+
+        // this is kinda bad, but on MacOS the shutdown doesn't seem to finish because of
+        // a thread called AWT-Shutdown. This will help while I track this down.
+        System.exit(0);
+    }
+
+    /** Return true iff we're running on a Mac */
+    static boolean isMac() {
+        // TODO: Replace usages of this method with
+        // org.eclipse.jface.util.Util#isMac() when we switch to Eclipse 3.5
+        // (ddms is currently built with SWT 3.4.2 from ANDROID_SWT)
+        return System.getProperty("os.name").startsWith("Mac OS"); //$NON-NLS-1$ //$NON-NLS-2$
+    }
+
+    private static void ping(SdkStatsService stats, String ddmsParentLocation) {
+        Properties p = new Properties();
+        try{
+            File sourceProp;
+            if (ddmsParentLocation != null && ddmsParentLocation.length() > 0) {
+                sourceProp = new File(ddmsParentLocation, "source.properties"); //$NON-NLS-1$
+            } else {
+                sourceProp = new File("source.properties"); //$NON-NLS-1$
+            }
+            FileInputStream fis = null;
+            try {
+                fis = new FileInputStream(sourceProp);
+                p.load(fis);
+            } finally {
+                if (fis != null) {
+                    try {
+                        fis.close();
+                    } catch (IOException ignore) {
+                    }
+                }
+            }
+
+            sRevision = p.getProperty("Pkg.Revision"); //$NON-NLS-1$
+            if (sRevision != null && sRevision.length() > 0) {
+                stats.ping("ddms", sRevision);  //$NON-NLS-1$
+            }
+        } catch (FileNotFoundException e) {
+            // couldn't find the file? don't ping.
+        } catch (IOException e) {
+            // couldn't find the file? don't ping.
+        }
+    }
+}
diff --git a/ddms/app/src/main/java/com/android/ddms/PrefsDialog.java b/ddms/app/src/main/java/com/android/ddms/PrefsDialog.java
new file mode 100644
index 0000000..acadeb8
--- /dev/null
+++ b/ddms/app/src/main/java/com/android/ddms/PrefsDialog.java
@@ -0,0 +1,610 @@
+/*
+ * Copyright (C) 2007 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.ddms;
+
+import com.android.ddmlib.DdmConstants;
+import com.android.ddmlib.DdmPreferences;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmuilib.DdmUiPreferences;
+import com.android.ddmuilib.PortFieldEditor;
+import com.android.ddmuilib.logcat.LogCatMessageList;
+import com.android.ddmuilib.logcat.LogCatPanel;
+import com.android.sdkstats.DdmsPreferenceStore;
+import com.android.sdkstats.SdkStatsPermissionDialog;
+
+import org.eclipse.jface.preference.BooleanFieldEditor;
+import org.eclipse.jface.preference.DirectoryFieldEditor;
+import org.eclipse.jface.preference.FieldEditorPreferencePage;
+import org.eclipse.jface.preference.FontFieldEditor;
+import org.eclipse.jface.preference.IntegerFieldEditor;
+import org.eclipse.jface.preference.PreferenceDialog;
+import org.eclipse.jface.preference.PreferenceManager;
+import org.eclipse.jface.preference.PreferenceNode;
+import org.eclipse.jface.preference.PreferencePage;
+import org.eclipse.jface.preference.PreferenceStore;
+import org.eclipse.jface.preference.RadioGroupFieldEditor;
+import org.eclipse.jface.preference.StringFieldEditor;
+import org.eclipse.jface.util.IPropertyChangeListener;
+import org.eclipse.jface.util.PropertyChangeEvent;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Link;
+import org.eclipse.swt.widgets.Shell;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Preferences dialog.
+ */
+public final class PrefsDialog {
+
+    // public const values for storage
+    public final static String SHELL_X = "shellX"; //$NON-NLS-1$
+    public final static String SHELL_Y = "shellY"; //$NON-NLS-1$
+    public final static String SHELL_WIDTH = "shellWidth"; //$NON-NLS-1$
+    public final static String SHELL_HEIGHT = "shellHeight"; //$NON-NLS-1$
+    public final static String EXPLORER_SHELL_X = "explorerShellX"; //$NON-NLS-1$
+    public final static String EXPLORER_SHELL_Y = "explorerShellY"; //$NON-NLS-1$
+    public final static String EXPLORER_SHELL_WIDTH = "explorerShellWidth"; //$NON-NLS-1$
+    public final static String EXPLORER_SHELL_HEIGHT = "explorerShellHeight"; //$NON-NLS-1$
+    public final static String SHOW_NATIVE_HEAP = "native"; //$NON-NLS-1$
+
+    public final static String LOGCAT_COLUMN_MODE = "ddmsLogColumnMode"; //$NON-NLS-1$
+    public final static String LOGCAT_FONT = "ddmsLogFont"; //$NON-NLS-1$
+
+    public final static String LOGCAT_COLUMN_MODE_AUTO = "auto"; //$NON-NLS-1$
+    public final static String LOGCAT_COLUMN_MODE_MANUAL = "manual"; //$NON-NLS-1$
+
+    private final static String PREFS_DEBUG_PORT_BASE = "adbDebugBasePort"; //$NON-NLS-1$
+    private final static String PREFS_SELECTED_DEBUG_PORT = "debugSelectedPort"; //$NON-NLS-1$
+    private final static String PREFS_DEFAULT_THREAD_UPDATE = "defaultThreadUpdateEnabled"; //$NON-NLS-1$
+    private final static String PREFS_DEFAULT_HEAP_UPDATE = "defaultHeapUpdateEnabled"; //$NON-NLS-1$
+    private final static String PREFS_THREAD_REFRESH_INTERVAL = "threadStatusInterval"; //$NON-NLS-1$
+    private final static String PREFS_LOG_LEVEL = "ddmsLogLevel"; //$NON-NLS-1$
+    private final static String PREFS_TIMEOUT = "timeOut"; //$NON-NLS-1$
+    private final static String PREFS_PROFILER_BUFFER_SIZE_MB = "profilerBufferSizeMb"; //$NON-NLS-1$
+    private final static String PREFS_USE_ADBHOST = "useAdbHost"; //$NON-NLS-1$
+    private final static String PREFS_ADBHOST_VALUE = "adbHostValue"; //$NON-NLS-1$
+
+    // Preference store.
+    private static DdmsPreferenceStore mStore = new DdmsPreferenceStore();
+
+    /**
+     * Private constructor -- do not instantiate.
+     */
+    private PrefsDialog() {}
+
+    /**
+     * Return the PreferenceStore that holds our values.
+     *
+     * @deprecated Callers should use {@link DdmsPreferenceStore} directly.
+     */
+    @Deprecated
+    public static PreferenceStore getStore() {
+        return mStore.getPreferenceStore();
+    }
+
+    /**
+     * Save the prefs to the config file.
+     *
+     * @deprecated Callers should use {@link DdmsPreferenceStore} directly.
+     */
+    @Deprecated
+    public static void save() {
+        try {
+            mStore.getPreferenceStore().save();
+        }
+        catch (IOException ioe) {
+            Log.w("ddms", "Failed saving prefs file: " + ioe.getMessage());
+        }
+    }
+
+    /**
+     * Do some one-time prep.
+     *
+     * The original plan was to let the individual classes define their
+     * own defaults, which we would get and then override with the config
+     * file.  However, PreferencesStore.load() doesn't trigger the "changed"
+     * events, which means we have to pull the loaded config values out by
+     * hand.
+     *
+     * So, we set the defaults, load the values from the config file, and
+     * then run through and manually export the values.  Then we duplicate
+     * the second part later on for the "changed" events.
+     */
+    public static void init() {
+        PreferenceStore prefStore = mStore.getPreferenceStore();
+
+        if (prefStore == null) {
+            // we have a serious issue here...
+            Log.e("ddms",
+                    "failed to access both the user HOME directory and the system wide temp folder. Quitting.");
+            System.exit(1);
+        }
+
+        // configure default values
+        setDefaults(System.getProperty("user.home")); //$NON-NLS-1$
+
+        // listen for changes
+        prefStore.addPropertyChangeListener(new ChangeListener());
+
+        // Now we initialize the value of the preference, from the values in the store.
+
+        // First the ddm lib.
+        DdmPreferences.setDebugPortBase(prefStore.getInt(PREFS_DEBUG_PORT_BASE));
+        DdmPreferences.setSelectedDebugPort(prefStore.getInt(PREFS_SELECTED_DEBUG_PORT));
+        DdmPreferences.setLogLevel(prefStore.getString(PREFS_LOG_LEVEL));
+        DdmPreferences.setInitialThreadUpdate(prefStore.getBoolean(PREFS_DEFAULT_THREAD_UPDATE));
+        DdmPreferences.setInitialHeapUpdate(prefStore.getBoolean(PREFS_DEFAULT_HEAP_UPDATE));
+        DdmPreferences.setTimeOut(prefStore.getInt(PREFS_TIMEOUT));
+        DdmPreferences.setProfilerBufferSizeMb(prefStore.getInt(PREFS_PROFILER_BUFFER_SIZE_MB));
+        DdmPreferences.setUseAdbHost(prefStore.getBoolean(PREFS_USE_ADBHOST));
+        DdmPreferences.setAdbHostValue(prefStore.getString(PREFS_ADBHOST_VALUE));
+
+        // some static values
+        String out = System.getenv("ANDROID_PRODUCT_OUT"); //$NON-NLS-1$
+        DdmUiPreferences.setSymbolsLocation(out + File.separator + "symbols"); //$NON-NLS-1$
+        DdmUiPreferences.setAddr2LineLocation("arm-linux-androideabi-addr2line"); //$NON-NLS-1$
+
+        String traceview = System.getProperty("com.android.ddms.bindir");  //$NON-NLS-1$
+        if (traceview != null && traceview.length() != 0) {
+            traceview += File.separator + DdmConstants.FN_TRACEVIEW;
+        } else {
+            traceview = DdmConstants.FN_TRACEVIEW;
+        }
+        DdmUiPreferences.setTraceviewLocation(traceview);
+
+        // Now the ddmui lib
+        DdmUiPreferences.setStore(prefStore);
+        DdmUiPreferences.setThreadRefreshInterval(prefStore.getInt(PREFS_THREAD_REFRESH_INTERVAL));
+    }
+
+    /*
+     * Set default values for all preferences.  These are either defined
+     * statically or are based on the values set by the class initializers
+     * in other classes.
+     *
+     * The other threads (e.g. VMWatcherThread) haven't been created yet,
+     * so we want to use static values rather than reading fields from
+     * class.getInstance().
+     */
+    private static void setDefaults(String homeDir) {
+        PreferenceStore prefStore = mStore.getPreferenceStore();
+
+        prefStore.setDefault(PREFS_DEBUG_PORT_BASE, DdmPreferences.DEFAULT_DEBUG_PORT_BASE);
+
+        prefStore.setDefault(PREFS_SELECTED_DEBUG_PORT,
+                DdmPreferences.DEFAULT_SELECTED_DEBUG_PORT);
+
+        prefStore.setDefault(PREFS_USE_ADBHOST, DdmPreferences.DEFAULT_USE_ADBHOST);
+        prefStore.setDefault(PREFS_ADBHOST_VALUE, DdmPreferences.DEFAULT_ADBHOST_VALUE);
+
+        prefStore.setDefault(PREFS_DEFAULT_THREAD_UPDATE, true);
+        prefStore.setDefault(PREFS_DEFAULT_HEAP_UPDATE, false);
+        prefStore.setDefault(PREFS_THREAD_REFRESH_INTERVAL,
+            DdmUiPreferences.DEFAULT_THREAD_REFRESH_INTERVAL);
+
+        prefStore.setDefault("textSaveDir", homeDir); //$NON-NLS-1$
+        prefStore.setDefault("imageSaveDir", homeDir); //$NON-NLS-1$
+
+        prefStore.setDefault(PREFS_LOG_LEVEL, "info"); //$NON-NLS-1$
+
+        prefStore.setDefault(PREFS_TIMEOUT, DdmPreferences.DEFAULT_TIMEOUT);
+        prefStore.setDefault(PREFS_PROFILER_BUFFER_SIZE_MB,
+                DdmPreferences.DEFAULT_PROFILER_BUFFER_SIZE_MB);
+
+        // choose a default font for the text output
+        FontData fdat = new FontData("Courier", 10, SWT.NORMAL); //$NON-NLS-1$
+        prefStore.setDefault("textOutputFont", fdat.toString()); //$NON-NLS-1$
+
+        // layout information.
+        prefStore.setDefault(SHELL_X, 100);
+        prefStore.setDefault(SHELL_Y, 100);
+        prefStore.setDefault(SHELL_WIDTH, 800);
+        prefStore.setDefault(SHELL_HEIGHT, 600);
+
+        prefStore.setDefault(EXPLORER_SHELL_X, 50);
+        prefStore.setDefault(EXPLORER_SHELL_Y, 50);
+
+        prefStore.setDefault(SHOW_NATIVE_HEAP, false);
+    }
+
+
+    /*
+     * Create a "listener" to take action when preferences change.  These are
+     * required for ongoing activities that don't check prefs on each use.
+     *
+     * This is only invoked when something explicitly changes the value of
+     * a preference (e.g. not when the prefs file is loaded).
+     */
+    private static class ChangeListener implements IPropertyChangeListener {
+        @Override
+        public void propertyChange(PropertyChangeEvent event) {
+            String changed = event.getProperty();
+            PreferenceStore prefStore = mStore.getPreferenceStore();
+
+            if (changed.equals(PREFS_DEBUG_PORT_BASE)) {
+                DdmPreferences.setDebugPortBase(prefStore.getInt(PREFS_DEBUG_PORT_BASE));
+            } else if (changed.equals(PREFS_SELECTED_DEBUG_PORT)) {
+                DdmPreferences.setSelectedDebugPort(prefStore.getInt(PREFS_SELECTED_DEBUG_PORT));
+            } else if (changed.equals(PREFS_LOG_LEVEL)) {
+                DdmPreferences.setLogLevel((String)event.getNewValue());
+            } else if (changed.equals("textSaveDir")) {
+                prefStore.setValue("lastTextSaveDir",
+                    (String) event.getNewValue());
+            } else if (changed.equals("imageSaveDir")) {
+                prefStore.setValue("lastImageSaveDir",
+                    (String) event.getNewValue());
+            } else if (changed.equals(PREFS_TIMEOUT)) {
+                DdmPreferences.setTimeOut(prefStore.getInt(PREFS_TIMEOUT));
+            } else if (changed.equals(PREFS_PROFILER_BUFFER_SIZE_MB)) {
+                DdmPreferences.setProfilerBufferSizeMb(
+                        prefStore.getInt(PREFS_PROFILER_BUFFER_SIZE_MB));
+            } else if (changed.equals(PREFS_USE_ADBHOST)) {
+                DdmPreferences.setUseAdbHost(prefStore.getBoolean(PREFS_USE_ADBHOST));
+            } else if (changed.equals(PREFS_ADBHOST_VALUE)) {
+                DdmPreferences.setAdbHostValue(prefStore.getString(PREFS_ADBHOST_VALUE));
+            } else {
+                Log.v("ddms", "Preference change: " + event.getProperty()
+                    + ": '" + event.getOldValue()
+                    + "' --> '" + event.getNewValue() + "'");
+            }
+        }
+    }
+
+
+    /**
+     * Create and display the dialog.
+     */
+    public static void run(Shell shell) {
+        PreferenceStore prefStore = mStore.getPreferenceStore();
+        assert prefStore != null;
+
+        PreferenceManager prefMgr = new PreferenceManager();
+
+        PreferenceNode node, subNode;
+
+        // this didn't work -- got NPE, possibly from class lookup:
+        //PreferenceNode app = new PreferenceNode("app", "Application", null,
+        //    AppPrefs.class.getName());
+
+        node = new PreferenceNode("debugger", new DebuggerPrefs());
+        prefMgr.addToRoot(node);
+
+        subNode = new PreferenceNode("panel", new PanelPrefs());
+        //prefMgr.addTo(node.getId(), subNode);
+        prefMgr.addToRoot(subNode);
+
+        node = new PreferenceNode("LogCat", new LogCatPrefs());
+        prefMgr.addToRoot(node);
+
+        node = new PreferenceNode("misc", new MiscPrefs());
+        prefMgr.addToRoot(node);
+
+        node = new PreferenceNode("stats", new UsageStatsPrefs());
+        prefMgr.addToRoot(node);
+
+        PreferenceDialog dlg = new PreferenceDialog(shell, prefMgr);
+        dlg.setPreferenceStore(prefStore);
+
+        // run it
+        try {
+            dlg.open();
+        } catch (Throwable t) {
+            Log.e("ddms", t);
+        }
+
+        // save prefs
+        try {
+            prefStore.save();
+        }
+        catch (IOException ioe) {
+        }
+
+        // discard the stuff we created
+        //prefMgr.dispose();
+        //dlg.dispose();
+    }
+
+    /**
+     * "Debugger" prefs page.
+     */
+    private static class DebuggerPrefs extends FieldEditorPreferencePage {
+
+        private BooleanFieldEditor mUseAdbHost;
+        private StringFieldEditor mAdbHostValue;
+
+        /**
+         * Basic constructor.
+         */
+        public DebuggerPrefs() {
+            super(GRID);        // use "grid" layout so edit boxes line up
+            setTitle("Debugger");
+        }
+
+         /**
+         * Create field editors.
+         */
+        @Override
+        protected void createFieldEditors() {
+            IntegerFieldEditor ife;
+
+            ife = new PortFieldEditor(PREFS_DEBUG_PORT_BASE,
+                "Starting value for local port:", getFieldEditorParent());
+            addField(ife);
+
+            ife = new PortFieldEditor(PREFS_SELECTED_DEBUG_PORT,
+                "Port of Selected VM:", getFieldEditorParent());
+            addField(ife);
+
+            mUseAdbHost = new BooleanFieldEditor(PREFS_USE_ADBHOST,
+                    "Use ADBHOST", getFieldEditorParent());
+            addField(mUseAdbHost);
+
+            mAdbHostValue = new StringFieldEditor(PREFS_ADBHOST_VALUE,
+                    "ADBHOST value:", getFieldEditorParent());
+            mAdbHostValue.setEnabled(getPreferenceStore()
+                    .getBoolean(PREFS_USE_ADBHOST), getFieldEditorParent());
+            addField(mAdbHostValue);
+        }
+
+        @Override
+        public void propertyChange(PropertyChangeEvent event) {
+            // TODO Auto-generated method stub
+            if (event.getSource().equals(mUseAdbHost)) {
+                mAdbHostValue.setEnabled(mUseAdbHost.getBooleanValue(), getFieldEditorParent());
+            }
+        }
+    }
+
+    /**
+     * "Panel" prefs page.
+     */
+    private static class PanelPrefs extends FieldEditorPreferencePage {
+
+        /**
+         * Basic constructor.
+         */
+        public PanelPrefs() {
+            super(FLAT);        // use "flat" layout
+            setTitle("Info Panels");
+        }
+
+        /**
+         * Create field editors.
+         */
+        @Override
+        protected void createFieldEditors() {
+            BooleanFieldEditor bfe;
+            IntegerFieldEditor ife;
+
+            bfe = new BooleanFieldEditor(PREFS_DEFAULT_THREAD_UPDATE,
+                "Thread updates enabled by default", getFieldEditorParent());
+            addField(bfe);
+
+            bfe = new BooleanFieldEditor(PREFS_DEFAULT_HEAP_UPDATE,
+                "Heap updates enabled by default", getFieldEditorParent());
+            addField(bfe);
+
+            ife = new IntegerFieldEditor(PREFS_THREAD_REFRESH_INTERVAL,
+                "Thread status interval (seconds):", getFieldEditorParent());
+            ife.setValidRange(1, 60);
+            addField(ife);
+        }
+    }
+
+    /**
+     * "logcat" prefs page.
+     */
+    private static class LogCatPrefs extends FieldEditorPreferencePage {
+
+        /**
+         * Basic constructor.
+         */
+        public LogCatPrefs() {
+            super(FLAT);        // use "flat" layout
+            setTitle("Logcat");
+        }
+
+        /**
+         * Create field editors.
+         */
+        @Override
+        protected void createFieldEditors() {
+            if (UIThread.useOldLogCatView()) {
+                RadioGroupFieldEditor rgfe;
+
+                rgfe = new RadioGroupFieldEditor(PrefsDialog.LOGCAT_COLUMN_MODE,
+                    "Message Column Resizing Mode", 1, new String[][] {
+                        { "Manual", PrefsDialog.LOGCAT_COLUMN_MODE_MANUAL },
+                        { "Automatic", PrefsDialog.LOGCAT_COLUMN_MODE_AUTO },
+                        },
+                    getFieldEditorParent(), true);
+                addField(rgfe);
+
+                FontFieldEditor ffe = new FontFieldEditor(PrefsDialog.LOGCAT_FONT,
+                        "Text output font:",
+                        getFieldEditorParent());
+                addField(ffe);
+            } else {
+                FontFieldEditor ffe = new FontFieldEditor(LogCatPanel.LOGCAT_VIEW_FONT_PREFKEY,
+                        "Text output font:",
+                        getFieldEditorParent());
+                addField(ffe);
+
+                IntegerFieldEditor maxMessages = new IntegerFieldEditor(
+                        LogCatMessageList.MAX_MESSAGES_PREFKEY,
+                        "Maximum number of logcat messages to buffer",
+                        getFieldEditorParent());
+                addField(maxMessages);
+
+                BooleanFieldEditor autoScrollLock = new BooleanFieldEditor(
+                        LogCatPanel.AUTO_SCROLL_LOCK_PREFKEY,
+                        "Automatically enable/disable scroll lock based on the scrollbar position",
+                        getFieldEditorParent());
+                addField(autoScrollLock);
+            }
+        }
+    }
+
+    /**
+     * "misc" prefs page.
+     */
+    private static class MiscPrefs extends FieldEditorPreferencePage {
+
+        /**
+         * Basic constructor.
+         */
+        public MiscPrefs() {
+            super(FLAT);        // use "flat" layout
+            setTitle("Misc");
+        }
+
+        /**
+         * Create field editors.
+         */
+        @Override
+        protected void createFieldEditors() {
+            DirectoryFieldEditor dfe;
+            FontFieldEditor ffe;
+
+            IntegerFieldEditor ife = new IntegerFieldEditor(PREFS_TIMEOUT,
+                    "ADB connection time out (ms):", getFieldEditorParent());
+            addField(ife);
+
+            ife = new IntegerFieldEditor(PREFS_PROFILER_BUFFER_SIZE_MB,
+                    "Profiler buffer size (MB):", getFieldEditorParent());
+            addField(ife);
+
+            dfe = new DirectoryFieldEditor("textSaveDir",
+                "Default text save dir:", getFieldEditorParent());
+            addField(dfe);
+
+            dfe = new DirectoryFieldEditor("imageSaveDir",
+                "Default image save dir:", getFieldEditorParent());
+            addField(dfe);
+
+            ffe = new FontFieldEditor("textOutputFont", "Text output font:",
+                getFieldEditorParent());
+            addField(ffe);
+
+            RadioGroupFieldEditor rgfe;
+
+            rgfe = new RadioGroupFieldEditor(PREFS_LOG_LEVEL,
+                "Logging Level", 1, new String[][] {
+                    { "Verbose", LogLevel.VERBOSE.getStringValue() },
+                    { "Debug", LogLevel.DEBUG.getStringValue() },
+                    { "Info", LogLevel.INFO.getStringValue() },
+                    { "Warning", LogLevel.WARN.getStringValue() },
+                    { "Error", LogLevel.ERROR.getStringValue() },
+                    { "Assert", LogLevel.ASSERT.getStringValue() },
+                    },
+                getFieldEditorParent(), true);
+            addField(rgfe);
+        }
+    }
+
+    /**
+     * "Device" prefs page.
+     */
+    private static class UsageStatsPrefs extends PreferencePage {
+
+        private BooleanFieldEditor mOptInCheckbox;
+        private Composite mTop;
+
+        /**
+         * Basic constructor.
+         */
+        public UsageStatsPrefs() {
+            setTitle("Usage Stats");
+        }
+
+        @Override
+        protected Control createContents(Composite parent) {
+            mTop = new Composite(parent, SWT.NONE);
+            mTop.setLayout(new GridLayout(1, false));
+            mTop.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+            Label text = new Label(mTop, SWT.WRAP);
+            text.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+            text.setText(SdkStatsPermissionDialog.BODY_TEXT);
+
+            Link privacyPolicyLink = new Link(mTop, SWT.WRAP);
+            privacyPolicyLink.setText(SdkStatsPermissionDialog.PRIVACY_POLICY_LINK_TEXT);
+            privacyPolicyLink.addSelectionListener(new SelectionAdapter() {
+                @Override
+                public void widgetSelected(SelectionEvent event) {
+                    SdkStatsPermissionDialog.openUrl(event.text);
+                }
+            });
+
+            mOptInCheckbox = new BooleanFieldEditor(DdmsPreferenceStore.PING_OPT_IN,
+                    SdkStatsPermissionDialog.CHECKBOX_TEXT, mTop);
+            mOptInCheckbox.setPage(this);
+            mOptInCheckbox.setPreferenceStore(getPreferenceStore());
+            mOptInCheckbox.load();
+
+            return null;
+        }
+
+        @Override
+        protected Point doComputeSize() {
+            if (mTop != null) {
+                return mTop.computeSize(450, SWT.DEFAULT, true);
+            }
+
+            return super.doComputeSize();
+        }
+
+        @Override
+        protected void performDefaults() {
+            if (mOptInCheckbox != null) {
+                mOptInCheckbox.loadDefault();
+            }
+            super.performDefaults();
+        }
+
+        @Override
+        public void performApply() {
+            if (mOptInCheckbox != null) {
+                mOptInCheckbox.store();
+            }
+            super.performApply();
+        }
+
+        @Override
+        public boolean performOk() {
+            if (mOptInCheckbox != null) {
+                mOptInCheckbox.store();
+            }
+            return super.performOk();
+        }
+    }
+
+}
+
+
diff --git a/ddms/app/src/main/java/com/android/ddms/StaticPortConfigDialog.java b/ddms/app/src/main/java/com/android/ddms/StaticPortConfigDialog.java
new file mode 100644
index 0000000..9a8ada3
--- /dev/null
+++ b/ddms/app/src/main/java/com/android/ddms/StaticPortConfigDialog.java
@@ -0,0 +1,395 @@
+/*
+ * Copyright (C) 2007 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.ddms;
+
+import com.android.ddmuilib.TableHelper;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableItem;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Dialog to configure the static debug ports.
+ *
+ */
+public class StaticPortConfigDialog extends Dialog {
+
+    /** Preference name for the 0th column width */
+    private static final String PREFS_DEVICE_COL = "spcd.deviceColumn"; //$NON-NLS-1$
+
+    /** Preference name for the 1st column width */
+    private static final String PREFS_APP_COL = "spcd.AppColumn"; //$NON-NLS-1$
+
+    /** Preference name for the 2nd column width */
+    private static final String PREFS_PORT_COL = "spcd.PortColumn"; //$NON-NLS-1$
+
+    private static final int COL_DEVICE = 0;
+    private static final int COL_APPLICATION = 1;
+    private static final int COL_PORT = 2;
+
+
+    private static final int DLG_WIDTH = 500;
+    private static final int DLG_HEIGHT = 300;
+
+    private Shell mShell;
+    private Shell mParent;
+
+    private Table mPortTable;
+
+    /**
+     * Array containing the list of already used static port to avoid
+     * duplication.
+     */
+    private ArrayList<Integer> mPorts = new ArrayList<Integer>();
+
+    /**
+     * Basic constructor.
+     * @param parent
+     */
+    public StaticPortConfigDialog(Shell parent) {
+        super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL);
+    }
+
+    /**
+     * Open and display the dialog. This method returns only when the
+     * user closes the dialog somehow.
+     *
+     */
+    public void open() {
+        createUI();
+
+        if (mParent == null || mShell == null) {
+            return;
+        }
+
+        updateFromStore();
+
+        // Set the dialog size.
+        mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT);
+        Rectangle r = mParent.getBounds();
+        // get the center new top left.
+        int cx = r.x + r.width/2;
+        int x = cx - DLG_WIDTH / 2;
+        int cy = r.y + r.height/2;
+        int y = cy - DLG_HEIGHT / 2;
+        mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT);
+
+        mShell.pack();
+
+        // actually open the dialog
+        mShell.open();
+
+        // event loop until the dialog is closed.
+        Display display = mParent.getDisplay();
+        while (!mShell.isDisposed()) {
+            if (!display.readAndDispatch())
+                display.sleep();
+        }
+    }
+
+    /**
+     * Creates the dialog ui.
+     */
+    private void createUI() {
+        mParent = getParent();
+        mShell = new Shell(mParent, getStyle());
+        mShell.setText("Static Port Configuration");
+
+        mShell.setLayout(new GridLayout(1, true));
+
+        mShell.addListener(SWT.Close, new Listener() {
+            @Override
+            public void handleEvent(Event event) {
+                event.doit = true;
+            }
+        });
+
+        // center part with the list on the left and the buttons
+        // on the right.
+        Composite main = new Composite(mShell, SWT.NONE);
+        main.setLayoutData(new GridData(GridData.FILL_BOTH));
+        main.setLayout(new GridLayout(2, false));
+
+        // left part: list view
+        mPortTable = new Table(main, SWT.SINGLE | SWT.FULL_SELECTION);
+        mPortTable.setLayoutData(new GridData(GridData.FILL_BOTH));
+        mPortTable.setHeaderVisible(true);
+        mPortTable.setLinesVisible(true);
+
+        TableHelper.createTableColumn(mPortTable, "Device Serial Number",
+                SWT.LEFT, "emulator-5554", //$NON-NLS-1$
+                PREFS_DEVICE_COL, PrefsDialog.getStore());
+
+        TableHelper.createTableColumn(mPortTable, "Application Package",
+                SWT.LEFT, "com.android.samples.phone", //$NON-NLS-1$
+                PREFS_APP_COL, PrefsDialog.getStore());
+
+        TableHelper.createTableColumn(mPortTable, "Debug Port",
+                SWT.RIGHT, "Debug Port", //$NON-NLS-1$
+                PREFS_PORT_COL, PrefsDialog.getStore());
+
+        // right part: buttons
+        Composite buttons = new Composite(main, SWT.NONE);
+        buttons.setLayoutData(new GridData(GridData.FILL_VERTICAL));
+        buttons.setLayout(new GridLayout(1, true));
+
+        Button newButton = new Button(buttons, SWT.NONE);
+        newButton.setText("New...");
+        newButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                StaticPortEditDialog dlg = new StaticPortEditDialog(mShell,
+                        mPorts);
+                if (dlg.open()) {
+                    // get the text
+                    String device = dlg.getDeviceSN();
+                    String app = dlg.getAppName();
+                    int port = dlg.getPortNumber();
+
+                    // add it to the list
+                    addEntry(device, app, port);
+                }
+            }
+        });
+
+        final Button editButton = new Button(buttons, SWT.NONE);
+        editButton.setText("Edit...");
+        editButton.setEnabled(false);
+        editButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                int index = mPortTable.getSelectionIndex();
+                String oldDeviceName = getDeviceName(index);
+                String oldAppName = getAppName(index);
+                String oldPortNumber = getPortNumber(index);
+                StaticPortEditDialog dlg = new StaticPortEditDialog(mShell,
+                        mPorts, oldDeviceName, oldAppName, oldPortNumber);
+                if (dlg.open()) {
+                    // get the text
+                    String deviceName = dlg.getDeviceSN();
+                    String app = dlg.getAppName();
+                    int port = dlg.getPortNumber();
+
+                    // add it to the list
+                    replaceEntry(index, deviceName, app, port);
+                }
+            }
+        });
+
+        final Button deleteButton = new Button(buttons, SWT.NONE);
+        deleteButton.setText("Delete");
+        deleteButton.setEnabled(false);
+        deleteButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                int index = mPortTable.getSelectionIndex();
+                removeEntry(index);
+            }
+        });
+
+        // bottom part with the ok/cancel
+        Composite bottomComp = new Composite(mShell, SWT.NONE);
+        bottomComp.setLayoutData(new GridData(
+                GridData.HORIZONTAL_ALIGN_CENTER));
+        bottomComp.setLayout(new GridLayout(2, true));
+
+        Button okButton = new Button(bottomComp, SWT.NONE);
+        okButton.setText("OK");
+        okButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                updateStore();
+                mShell.close();
+            }
+        });
+
+        Button cancelButton = new Button(bottomComp, SWT.NONE);
+        cancelButton.setText("Cancel");
+        cancelButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                mShell.close();
+            }
+        });
+
+        mPortTable.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                // get the selection index
+                int index = mPortTable.getSelectionIndex();
+
+                boolean enabled = index != -1;
+                editButton.setEnabled(enabled);
+                deleteButton.setEnabled(enabled);
+            }
+        });
+
+        mShell.pack();
+
+    }
+
+    /**
+     * Add a new entry in the list.
+     * @param deviceName the serial number of the device
+     * @param appName java package for the application
+     * @param portNumber port number
+     */
+    private void addEntry(String deviceName, String appName, int portNumber) {
+        // create a new item for the table
+        TableItem item = new TableItem(mPortTable, SWT.NONE);
+
+        item.setText(COL_DEVICE, deviceName);
+        item.setText(COL_APPLICATION, appName);
+        item.setText(COL_PORT, Integer.toString(portNumber));
+
+        // add the port to the list of port number used.
+        mPorts.add(portNumber);
+    }
+
+    /**
+     * Remove an entry from the list.
+     * @param index The index of the entry to be removed
+     */
+    private void removeEntry(int index) {
+        // remove from the ui
+        mPortTable.remove(index);
+
+        // and from the port list.
+        mPorts.remove(index);
+    }
+
+    /**
+     * Replace an entry in the list with new values.
+     * @param index The index of the item to be replaced
+     * @param deviceName the serial number of the device
+     * @param appName The new java package for the application
+     * @param portNumber The new port number.
+     */
+    private void replaceEntry(int index, String deviceName, String appName, int portNumber) {
+        // get the table item by index
+        TableItem item = mPortTable.getItem(index);
+
+        // set its new value
+        item.setText(COL_DEVICE, deviceName);
+        item.setText(COL_APPLICATION, appName);
+        item.setText(COL_PORT, Integer.toString(portNumber));
+
+        // and replace the port number in the port list.
+        mPorts.set(index, portNumber);
+    }
+
+
+    /**
+     * Returns the device name for a specific index
+     * @param index The index
+     * @return the java package name of the application
+     */
+    private String getDeviceName(int index) {
+        TableItem item = mPortTable.getItem(index);
+        return item.getText(COL_DEVICE);
+    }
+
+    /**
+     * Returns the application name for a specific index
+     * @param index The index
+     * @return the java package name of the application
+     */
+    private String getAppName(int index) {
+        TableItem item = mPortTable.getItem(index);
+        return item.getText(COL_APPLICATION);
+    }
+
+    /**
+     * Returns the port number for a specific index
+     * @param index The index
+     * @return the port number
+     */
+    private String getPortNumber(int index) {
+        TableItem item = mPortTable.getItem(index);
+        return item.getText(COL_PORT);
+    }
+
+    /**
+     * Updates the ui from the value in the preference store.
+     */
+    private void updateFromStore() {
+        // get the map from the debug port manager
+        DebugPortProvider provider = DebugPortProvider.getInstance();
+        Map<String, Map<String, Integer>> map = provider.getPortList();
+
+        // we're going to loop on the keys and fill the table.
+        Set<String> deviceKeys = map.keySet();
+
+        for (String deviceKey : deviceKeys) {
+            Map<String, Integer> deviceMap = map.get(deviceKey);
+            if (deviceMap != null) {
+                Set<String> appKeys = deviceMap.keySet();
+
+                for (String appKey : appKeys) {
+                    Integer port = deviceMap.get(appKey);
+                    if (port != null) {
+                        addEntry(deviceKey, appKey, port);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Update the store from the content of the ui.
+     */
+    private void updateStore() {
+        // create a new Map object and fill it.
+        HashMap<String, Map<String, Integer>> map = new HashMap<String, Map<String, Integer>>();
+
+        int count = mPortTable.getItemCount();
+
+        for (int i = 0 ; i < count ; i++) {
+            TableItem item = mPortTable.getItem(i);
+            String deviceName = item.getText(COL_DEVICE);
+
+            Map<String, Integer> deviceMap = map.get(deviceName);
+            if (deviceMap == null) {
+                deviceMap = new HashMap<String, Integer>();
+                map.put(deviceName, deviceMap);
+            }
+
+            deviceMap.put(item.getText(COL_APPLICATION), Integer.valueOf(item.getText(COL_PORT)));
+        }
+
+        // set it in the store through the debug port manager.
+        DebugPortProvider provider = DebugPortProvider.getInstance();
+        provider.setPortList(map);
+    }
+}
diff --git a/ddms/app/src/main/java/com/android/ddms/StaticPortEditDialog.java b/ddms/app/src/main/java/com/android/ddms/StaticPortEditDialog.java
new file mode 100644
index 0000000..c9cb044
--- /dev/null
+++ b/ddms/app/src/main/java/com/android/ddms/StaticPortEditDialog.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright (C) 2007 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.ddms;
+
+import com.android.ddmlib.IDevice;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.ArrayList;
+
+/**
+ * Small dialog box to edit a static port number.
+ */
+public class StaticPortEditDialog extends Dialog {
+
+    private static final int DLG_WIDTH = 400;
+    private static final int DLG_HEIGHT = 200;
+
+    private Shell mParent;
+
+    private Shell mShell;
+
+    private boolean mOk = false;
+
+    private String mAppName;
+
+    private String mPortNumber;
+
+    private Button mOkButton;
+
+    private Label mWarning;
+
+    /** List of ports already in use */
+    private ArrayList<Integer> mPorts;
+
+    /** This is the port being edited. */
+    private int mEditPort = -1;
+    private String mDeviceSn;
+
+    /**
+     * Creates a dialog with empty fields.
+     * @param parent The parent Shell
+     * @param ports The list of already used port numbers.
+     */
+    public StaticPortEditDialog(Shell parent, ArrayList<Integer> ports) {
+        super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL);
+        mPorts = ports;
+        mDeviceSn = IDevice.FIRST_EMULATOR_SN;
+    }
+
+    /**
+     * Creates a dialog with predefined values.
+     * @param shell The parent shell
+     * @param ports The list of already used port numbers.
+     * @param oldDeviceSN the device serial number to display
+     * @param oldAppName The application name to display
+     * @param oldPortNumber The port number to display
+     */
+    public StaticPortEditDialog(Shell shell, ArrayList<Integer> ports,
+            String oldDeviceSN, String oldAppName, String oldPortNumber) {
+        this(shell, ports);
+
+        mDeviceSn = oldDeviceSN;
+        mAppName = oldAppName;
+        mPortNumber = oldPortNumber;
+        mEditPort = Integer.valueOf(mPortNumber);
+    }
+
+    /**
+     * Opens the dialog. The method will return when the user closes the dialog
+     * somehow.
+     *
+     * @return true if ok was pressed, false if cancelled.
+     */
+    public boolean open() {
+        createUI();
+
+        if (mParent == null || mShell == null) {
+            return false;
+        }
+
+        mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT);
+        Rectangle r = mParent.getBounds();
+        // get the center new top left.
+        int cx = r.x + r.width/2;
+        int x = cx - DLG_WIDTH / 2;
+        int cy = r.y + r.height/2;
+        int y = cy - DLG_HEIGHT / 2;
+        mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT);
+
+        mShell.open();
+
+        Display display = mParent.getDisplay();
+        while (!mShell.isDisposed()) {
+            if (!display.readAndDispatch())
+                display.sleep();
+        }
+
+        return mOk;
+    }
+
+    public String getDeviceSN() {
+        return mDeviceSn;
+    }
+
+    public String getAppName() {
+        return mAppName;
+    }
+
+    public int getPortNumber() {
+        return Integer.valueOf(mPortNumber);
+    }
+
+    private void createUI() {
+        mParent = getParent();
+        mShell = new Shell(mParent, getStyle());
+        mShell.setText("Static Port");
+
+        mShell.setLayout(new GridLayout(1, false));
+
+        mShell.addListener(SWT.Close, new Listener() {
+            @Override
+            public void handleEvent(Event event) {
+            }
+        });
+
+        // center part with the edit field
+        Composite main = new Composite(mShell, SWT.NONE);
+        main.setLayoutData(new GridData(GridData.FILL_BOTH));
+        main.setLayout(new GridLayout(2, false));
+
+        Label l0 = new Label(main, SWT.NONE);
+        l0.setText("Device Name:");
+
+        final Text deviceSNText = new Text(main, SWT.SINGLE | SWT.BORDER);
+        deviceSNText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        if (mDeviceSn != null) {
+            deviceSNText.setText(mDeviceSn);
+        }
+        deviceSNText.addModifyListener(new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent e) {
+                mDeviceSn = deviceSNText.getText().trim();
+                validate();
+            }
+        });
+
+        Label l = new Label(main, SWT.NONE);
+        l.setText("Application Name:");
+
+        final Text appNameText = new Text(main, SWT.SINGLE | SWT.BORDER);
+        if (mAppName != null) {
+            appNameText.setText(mAppName);
+        }
+        appNameText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        appNameText.addModifyListener(new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent e) {
+                mAppName = appNameText.getText().trim();
+                validate();
+            }
+        });
+
+        Label l2 = new Label(main, SWT.NONE);
+        l2.setText("Debug Port:");
+
+        final Text debugPortText = new Text(main, SWT.SINGLE | SWT.BORDER);
+        if (mPortNumber != null) {
+            debugPortText.setText(mPortNumber);
+        }
+        debugPortText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        debugPortText.addModifyListener(new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent e) {
+                mPortNumber = debugPortText.getText().trim();
+                validate();
+            }
+        });
+
+        // warning label
+        Composite warningComp = new Composite(mShell, SWT.NONE);
+        warningComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        warningComp.setLayout(new GridLayout(1, true));
+
+        mWarning = new Label(warningComp, SWT.NONE);
+        mWarning.setText("");
+        mWarning.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        // bottom part with the ok/cancel
+        Composite bottomComp = new Composite(mShell, SWT.NONE);
+        bottomComp
+                .setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER));
+        bottomComp.setLayout(new GridLayout(2, true));
+
+        mOkButton = new Button(bottomComp, SWT.NONE);
+        mOkButton.setText("OK");
+        mOkButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                mOk = true;
+                mShell.close();
+            }
+        });
+        mOkButton.setEnabled(false);
+        mShell.setDefaultButton(mOkButton);
+
+        Button cancelButton = new Button(bottomComp, SWT.NONE);
+        cancelButton.setText("Cancel");
+        cancelButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                mShell.close();
+            }
+        });
+
+        validate();
+    }
+
+    /**
+     * Validates the content of the 2 text fields and enable/disable "ok", while
+     * setting up the warning/error message.
+     */
+    private void validate() {
+        // first we reset the warning dialog. This allows us to latter
+        // display warnings.
+        mWarning.setText(""); //$NON-NLS-1$
+
+        // check the device name field is not empty
+        if (mDeviceSn == null || mDeviceSn.length() == 0) {
+            mWarning.setText("Device name missing.");
+            mOkButton.setEnabled(false);
+            return;
+        }
+
+        // check the application name field is not empty
+        if (mAppName == null || mAppName.length() == 0) {
+            mWarning.setText("Application name missing.");
+            mOkButton.setEnabled(false);
+            return;
+        }
+
+        String packageError = "Application name must be a valid Java package name.";
+
+        // validate the package name as well. It must be a fully qualified
+        // java package.
+        String[] packageSegments = mAppName.split("\\."); //$NON-NLS-1$
+        for (String p : packageSegments) {
+            if (p.matches("^[a-zA-Z][a-zA-Z0-9]*") == false) { //$NON-NLS-1$
+                mWarning.setText(packageError);
+                mOkButton.setEnabled(false);
+                return;
+            }
+
+            // lets also display a warning if the package contains upper case
+            // letters.
+            if (p.matches("^[a-z][a-z0-9]*") == false) { //$NON-NLS-1$
+                mWarning.setText("Lower case is recommended for Java packages.");
+            }
+        }
+
+        // the split will not detect the last char being a '.'
+        // so we test it manually
+        if (mAppName.charAt(mAppName.length()-1) == '.') {
+            mWarning.setText(packageError);
+            mOkButton.setEnabled(false);
+            return;
+        }
+
+        // now we test the package name field is not empty.
+        if (mPortNumber == null || mPortNumber.length() == 0) {
+            mWarning.setText("Port Number missing.");
+            mOkButton.setEnabled(false);
+            return;
+        }
+
+        // then we check it only contains digits.
+        if (mPortNumber.matches("[0-9]*") == false) { //$NON-NLS-1$
+            mWarning.setText("Port Number invalid.");
+            mOkButton.setEnabled(false);
+            return;
+        }
+
+        // get the int from the port number to validate
+        long port = Long.valueOf(mPortNumber);
+        if (port >= 32767) {
+            mOkButton.setEnabled(false);
+            return;
+        }
+
+        // check if its in the list of already used ports
+        if (port != mEditPort) {
+            for (Integer i : mPorts) {
+                if (port == i.intValue()) {
+                    mWarning.setText("Port already in use.");
+                    mOkButton.setEnabled(false);
+                    return;
+                }
+            }
+        }
+
+        // at this point there's not error, so we enable the ok button.
+        mOkButton.setEnabled(true);
+    }
+}
diff --git a/ddms/app/src/main/java/com/android/ddms/UIThread.java b/ddms/app/src/main/java/com/android/ddms/UIThread.java
new file mode 100644
index 0000000..1310429
--- /dev/null
+++ b/ddms/app/src/main/java/com/android/ddms/UIThread.java
@@ -0,0 +1,1812 @@
+/*
+ * Copyright (C) 2007 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.ddms;
+
+import com.android.SdkConstants;
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData;
+import com.android.ddmlib.ClientData.IHprofDumpHandler;
+import com.android.ddmlib.ClientData.MethodProfilingStatus;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.Log.ILogOutput;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmlib.SyncException;
+import com.android.ddmlib.SyncService;
+import com.android.ddmuilib.AllocationPanel;
+import com.android.ddmuilib.DdmUiPreferences;
+import com.android.ddmuilib.DevicePanel;
+import com.android.ddmuilib.DevicePanel.IUiSelectionListener;
+import com.android.ddmuilib.EmulatorControlPanel;
+import com.android.ddmuilib.HeapPanel;
+import com.android.ddmuilib.ITableFocusListener;
+import com.android.ddmuilib.ImageLoader;
+import com.android.ddmuilib.InfoPanel;
+import com.android.ddmuilib.NativeHeapPanel;
+import com.android.ddmuilib.ScreenShotDialog;
+import com.android.ddmuilib.SysinfoPanel;
+import com.android.ddmuilib.TablePanel;
+import com.android.ddmuilib.ThreadPanel;
+import com.android.ddmuilib.actions.ToolItemAction;
+import com.android.ddmuilib.explorer.DeviceExplorer;
+import com.android.ddmuilib.handler.BaseFileHandler;
+import com.android.ddmuilib.handler.MethodProfilingHandler;
+import com.android.ddmuilib.log.event.EventLogPanel;
+import com.android.ddmuilib.logcat.LogCatPanel;
+import com.android.ddmuilib.logcat.LogColors;
+import com.android.ddmuilib.logcat.LogFilter;
+import com.android.ddmuilib.logcat.LogPanel;
+import com.android.ddmuilib.logcat.LogPanel.ILogFilterStorageManager;
+import com.android.ddmuilib.net.NetworkPanel;
+import com.android.menubar.IMenuBarCallback;
+import com.android.menubar.IMenuBarEnhancer;
+import com.android.menubar.IMenuBarEnhancer.MenuBarMode;
+import com.android.menubar.MenuBarEnhancer;
+
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.preference.PreferenceStore;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTError;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.ControlListener;
+import org.eclipse.swt.events.MenuAdapter;
+import org.eclipse.swt.events.MenuEvent;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.ShellEvent;
+import org.eclipse.swt.events.ShellListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
+import org.eclipse.swt.widgets.Sash;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.TabFolder;
+import org.eclipse.swt.widgets.TabItem;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.swt.widgets.ToolItem;
+
+import java.io.File;
+import java.util.ArrayList;
+
+/**
+ * This acts as the UI builder. This cannot be its own thread since this prevent using AWT in an
+ * SWT application. So this class mainly builds the ui, and manages communication between the panels
+ * when {@link IDevice} / {@link Client} selection changes.
+ */
+public class UIThread implements IUiSelectionListener, IClientChangeListener {
+    public static final String APP_NAME = "DDMS";
+
+    /*
+     * UI tab panel definitions. The constants here must match up with the array
+     * indices in mPanels. PANEL_CLIENT_LIST is a "virtual" panel representing
+     * the client list.
+     */
+    public static final int PANEL_CLIENT_LIST = -1;
+
+    public static final int PANEL_INFO = 0;
+
+    public static final int PANEL_THREAD = 1;
+
+    public static final int PANEL_HEAP = 2;
+
+    private static final int PANEL_NATIVE_HEAP = 3;
+
+    private static final int PANEL_ALLOCATIONS = 4;
+
+    private static final int PANEL_SYSINFO = 5;
+
+    private static final int PANEL_NETWORK = 6;
+
+    private static final int PANEL_COUNT = 7;
+
+    /** Content is setup in the constructor */
+    private static TablePanel[] mPanels = new TablePanel[PANEL_COUNT];
+
+    private static final String[] mPanelNames = new String[] {
+            "Info", "Threads", "VM Heap", "Native Heap",
+            "Allocation Tracker", "Sysinfo", "Network"
+    };
+
+    private static final String[] mPanelTips = new String[] {
+            "Client information", "Thread status", "VM heap status",
+            "Native heap status", "Allocation Tracker", "Sysinfo graphs",
+            "Network usage"
+    };
+
+    private static final String PREFERENCE_LOGSASH =
+        "logSashLocation"; //$NON-NLS-1$
+    private static final String PREFERENCE_SASH =
+        "sashLocation"; //$NON-NLS-1$
+
+    private static final String PREFS_COL_TIME =
+        "logcat.time"; //$NON-NLS-1$
+    private static final String PREFS_COL_LEVEL =
+        "logcat.level"; //$NON-NLS-1$
+    private static final String PREFS_COL_PID =
+        "logcat.pid"; //$NON-NLS-1$
+    private static final String PREFS_COL_TAG =
+        "logcat.tag"; //$NON-NLS-1$
+    private static final String PREFS_COL_MESSAGE =
+        "logcat.message"; //$NON-NLS-1$
+
+    private static final String PREFS_FILTERS = "logcat.filter"; //$NON-NLS-1$
+
+    // singleton instance
+    private static UIThread mInstance = new UIThread();
+
+    // our display
+    private Display mDisplay;
+
+    // the table we show in the left-hand pane
+    private DevicePanel mDevicePanel;
+
+    private IDevice mCurrentDevice = null;
+    private Client mCurrentClient = null;
+
+    // status line at the bottom of the app window
+    private Label mStatusLine;
+
+    // some toolbar items we need to update
+    private ToolItem mTBShowThreadUpdates;
+    private ToolItem mTBShowHeapUpdates;
+    private ToolItem mTBHalt;
+    private ToolItem mTBCauseGc;
+    private ToolItem mTBDumpHprof;
+    private ToolItem mTBProfiling;
+
+    private final class FilterStorage implements ILogFilterStorageManager {
+
+        @Override
+        public LogFilter[] getFilterFromStore() {
+            String filterPrefs = PrefsDialog.getStore().getString(
+                    PREFS_FILTERS);
+
+            // split in a string per filter
+            String[] filters = filterPrefs.split("\\|"); //$NON-NLS-1$
+
+            ArrayList<LogFilter> list =
+                new ArrayList<LogFilter>(filters.length);
+
+            for (String f : filters) {
+                if (f.length() > 0) {
+                    LogFilter logFilter = new LogFilter();
+                    if (logFilter.loadFromString(f)) {
+                        list.add(logFilter);
+                    }
+                }
+            }
+
+            return list.toArray(new LogFilter[list.size()]);
+        }
+
+        @Override
+        public void saveFilters(LogFilter[] filters) {
+            StringBuilder sb = new StringBuilder();
+            for (LogFilter f : filters) {
+                String filterString = f.toString();
+                sb.append(filterString);
+                sb.append('|');
+            }
+
+            PrefsDialog.getStore().setValue(PREFS_FILTERS, sb.toString());
+        }
+
+        @Override
+        public boolean requiresDefaultFilter() {
+            return true;
+        }
+    }
+
+
+    /**
+     * Flag to indicate whether to use the old or the new logcat view. This is a
+     * temporary workaround that will be removed once the new view is complete.
+     */
+    private static final String USE_OLD_LOGCAT_VIEW =
+            System.getenv("ANDROID_USE_OLD_LOGCAT_VIEW");
+    public static boolean useOldLogCatView() {
+        return USE_OLD_LOGCAT_VIEW != null;
+    }
+
+    private LogPanel mLogPanel; /* only valid when useOldLogCatView() == true */
+    private LogCatPanel mLogCatPanel; /* only valid when useOldLogCatView() == false */
+
+    private ToolItemAction mCreateFilterAction;
+    private ToolItemAction mDeleteFilterAction;
+    private ToolItemAction mEditFilterAction;
+    private ToolItemAction mExportAction;
+    private ToolItemAction mClearAction;
+
+    private ToolItemAction[] mLogLevelActions;
+    private String[] mLogLevelIcons = {
+            "v.png", //$NON-NLS-1S
+            "d.png", //$NON-NLS-1S
+            "i.png", //$NON-NLS-1S
+            "w.png", //$NON-NLS-1S
+            "e.png", //$NON-NLS-1S
+    };
+
+    protected Clipboard mClipboard;
+
+    private MenuItem mCopyMenuItem;
+
+    private MenuItem mSelectAllMenuItem;
+
+    private TableFocusListener mTableListener;
+
+    private DeviceExplorer mExplorer = null;
+    private Shell mExplorerShell = null;
+
+    private EmulatorControlPanel mEmulatorPanel;
+
+    private EventLogPanel mEventLogPanel;
+
+    private Image mTracingStartImage;
+
+    private Image mTracingStopImage;
+
+    private ImageLoader mDdmUiLibLoader;
+
+    private class TableFocusListener implements ITableFocusListener {
+
+        private IFocusedTableActivator mCurrentActivator;
+
+        @Override
+        public void focusGained(IFocusedTableActivator activator) {
+            mCurrentActivator = activator;
+            if (mCopyMenuItem.isDisposed() == false) {
+                mCopyMenuItem.setEnabled(true);
+                mSelectAllMenuItem.setEnabled(true);
+            }
+        }
+
+        @Override
+        public void focusLost(IFocusedTableActivator activator) {
+            // if we move from one table to another, it's unclear
+            // if the old table lose its focus before the new
+            // one gets the focus, so we need to check.
+            if (activator == mCurrentActivator) {
+                activator = null;
+                if (mCopyMenuItem.isDisposed() == false) {
+                    mCopyMenuItem.setEnabled(false);
+                    mSelectAllMenuItem.setEnabled(false);
+                }
+            }
+        }
+
+        public void copy(Clipboard clipboard) {
+            if (mCurrentActivator != null) {
+                mCurrentActivator.copy(clipboard);
+            }
+        }
+
+        public void selectAll() {
+            if (mCurrentActivator != null) {
+                mCurrentActivator.selectAll();
+            }
+        }
+    }
+
+    /**
+     * Handler for HPROF dumps.
+     * This will always prompt the user to save the HPROF file.
+     */
+    private class HProfHandler extends BaseFileHandler implements IHprofDumpHandler {
+
+        public HProfHandler(Shell parentShell) {
+            super(parentShell);
+        }
+
+        @Override
+        public void onEndFailure(final Client client, final String message) {
+            mDisplay.asyncExec(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        displayErrorFromUiThread(
+                                "Unable to create HPROF file for application '%1$s'\n\n%2$s" +
+                                "Check logcat for more information.",
+                                client.getClientData().getClientDescription(),
+                                message != null ? message + "\n\n" : "");
+                    } finally {
+                        // this will make sure the dump hprof button is re-enabled for the
+                        // current selection. as the client is finished dumping an hprof file
+                        enableButtons();
+                    }
+                }
+            });
+        }
+
+        @Override
+        public void onSuccess(final String remoteFilePath, final Client client) {
+            mDisplay.asyncExec(new Runnable() {
+                @Override
+                public void run() {
+                    final IDevice device = client.getDevice();
+                    try {
+                        // get the sync service to pull the HPROF file
+                        final SyncService sync = client.getDevice().getSyncService();
+                        if (sync != null) {
+                            promptAndPull(sync,
+                                    client.getClientData().getClientDescription() + ".hprof",
+                                    remoteFilePath, "Save HPROF file");
+                        } else {
+                            displayErrorFromUiThread(
+                                    "Unable to download HPROF file from device '%1$s'.",
+                                    device.getSerialNumber());
+                        }
+                    } catch (SyncException e) {
+                        if (e.wasCanceled() == false) {
+                            displayErrorFromUiThread(
+                                    "Unable to download HPROF file from device '%1$s'.\n\n%2$s",
+                                    device.getSerialNumber(), e.getMessage());
+                        }
+                    } catch (Exception e) {
+                        displayErrorFromUiThread("Unable to download HPROF file from device '%1$s'.",
+                                device.getSerialNumber());
+
+                    } finally {
+                        // this will make sure the dump hprof button is re-enabled for the
+                        // current selection. as the client is finished dumping an hprof file
+                        enableButtons();
+                    }
+                }
+            });
+        }
+
+        @Override
+        public void onSuccess(final byte[] data, final Client client) {
+            mDisplay.asyncExec(new Runnable() {
+                @Override
+                public void run() {
+                    promptAndSave(client.getClientData().getClientDescription() + ".hprof", data,
+                            "Save HPROF file");
+                }
+            });
+        }
+
+        @Override
+        protected String getDialogTitle() {
+            return "HPROF Error";
+        }
+    }
+
+
+    /**
+     * Generic constructor.
+     */
+    private UIThread() {
+        mPanels[PANEL_INFO] = new InfoPanel();
+        mPanels[PANEL_THREAD] = new ThreadPanel();
+        mPanels[PANEL_HEAP] = new HeapPanel();
+        if (PrefsDialog.getStore().getBoolean(PrefsDialog.SHOW_NATIVE_HEAP)) {
+            if (System.getenv("ANDROID_DDMS_OLD_HEAP_PANEL") != null) {
+                mPanels[PANEL_NATIVE_HEAP] = new NativeHeapPanel();
+            } else {
+                mPanels[PANEL_NATIVE_HEAP] =
+                        new com.android.ddmuilib.heap.NativeHeapPanel(getStore());
+            }
+        } else {
+            mPanels[PANEL_NATIVE_HEAP] = null;
+        }
+        mPanels[PANEL_ALLOCATIONS] = new AllocationPanel();
+        mPanels[PANEL_SYSINFO] = new SysinfoPanel();
+        mPanels[PANEL_NETWORK] = new NetworkPanel();
+    }
+
+    /**
+     * Get singleton instance of the UI thread.
+     */
+    public static UIThread getInstance() {
+        return mInstance;
+    }
+
+    /**
+     * Return the Display. Don't try this unless you're in the UI thread.
+     */
+    public Display getDisplay() {
+        return mDisplay;
+    }
+
+    public void asyncExec(Runnable r) {
+        if (mDisplay != null && mDisplay.isDisposed() == false) {
+            mDisplay.asyncExec(r);
+        }
+    }
+
+    /** returns the IPreferenceStore */
+    public IPreferenceStore getStore() {
+        return PrefsDialog.getStore();
+    }
+
+    /**
+     * Create SWT objects and drive the user interface event loop.
+     * @param ddmsParentLocation location of the folder that contains ddms.
+     */
+    public void runUI(String ddmsParentLocation) {
+        Display.setAppName(APP_NAME);
+        mDisplay = Display.getDefault();
+        final Shell shell = new Shell(mDisplay, SWT.SHELL_TRIM);
+
+        // create the image loaders for DDMS and DDMUILIB
+        mDdmUiLibLoader = ImageLoader.getDdmUiLibLoader();
+
+        shell.setImage(ImageLoader.getLoader(this.getClass()).loadImage(mDisplay,
+                "ddms-128.png", //$NON-NLS-1$
+                100, 50, null));
+
+        Log.setLogOutput(new ILogOutput() {
+            @Override
+            public void printAndPromptLog(final LogLevel logLevel, final String tag,
+                    final String message) {
+                Log.printLog(logLevel, tag, message);
+                // dialog box only run in UI thread..
+                mDisplay.asyncExec(new Runnable() {
+                    @Override
+                    public void run() {
+                        Shell activeShell = mDisplay.getActiveShell();
+                        if (logLevel == LogLevel.ERROR) {
+                            MessageDialog.openError(activeShell, tag, message);
+                        } else {
+                            MessageDialog.openWarning(activeShell, tag, message);
+                        }
+                    }
+                });
+            }
+
+            @Override
+            public void printLog(LogLevel logLevel, String tag, String message) {
+                Log.printLog(logLevel, tag, message);
+            }
+        });
+
+        // set the handler for hprof dump
+        ClientData.setHprofDumpHandler(new HProfHandler(shell));
+        ClientData.setMethodProfilingHandler(new MethodProfilingHandler(shell));
+
+        // [try to] ensure ADB is running
+        // in the new SDK, adb is in the platform-tools, but when run from the command line
+        // in the Android source tree, then adb is next to ddms.
+        String adbLocation;
+        if (ddmsParentLocation != null && ddmsParentLocation.length() != 0) {
+            // check if there's a platform-tools folder
+            File platformTools = new File(new File(ddmsParentLocation).getParent(),
+                    "platform-tools");  //$NON-NLS-1$
+            if (platformTools.isDirectory()) {
+                adbLocation = platformTools.getAbsolutePath() + File.separator +
+                        SdkConstants.FN_ADB;
+            } else {
+                // we're in the Android source tree, then adb is in $ANDROID_HOST_OUT/bin/adb
+                String androidOut = System.getenv("ANDROID_HOST_OUT");
+                if (androidOut != null) {
+                    adbLocation = androidOut + File.separator + "bin" + File.separator +
+                            SdkConstants.FN_ADB;
+                } else {
+                    adbLocation = SdkConstants.FN_ADB;
+                }
+            }
+        } else {
+            adbLocation = SdkConstants.FN_ADB;
+        }
+
+        AndroidDebugBridge.init(true /* debugger support */);
+        AndroidDebugBridge.createBridge(adbLocation, true /* forceNewBridge */);
+
+        // we need to listen to client change to be notified of client status (profiling) change
+        AndroidDebugBridge.addClientChangeListener(this);
+
+        shell.setText("Dalvik Debug Monitor");
+        setConfirmClose(shell);
+        createMenus(shell);
+        createWidgets(shell);
+
+        shell.pack();
+        setSizeAndPosition(shell);
+        shell.open();
+
+        Log.d("ddms", "UI is up");
+
+        while (!shell.isDisposed()) {
+            if (!mDisplay.readAndDispatch())
+                mDisplay.sleep();
+        }
+        if (useOldLogCatView()) {
+            mLogPanel.stopLogCat(true);
+        }
+
+        mDevicePanel.dispose();
+        for (TablePanel panel : mPanels) {
+            if (panel != null) {
+                panel.dispose();
+            }
+        }
+
+        ImageLoader.dispose();
+
+        mDisplay.dispose();
+        Log.d("ddms", "UI is down");
+    }
+
+    /**
+     * Set the size and position of the main window from the preference, and
+     * setup listeners for control events (resize/move of the window)
+     */
+    private void setSizeAndPosition(final Shell shell) {
+        shell.setMinimumSize(400, 200);
+
+        // get the x/y and w/h from the prefs
+        PreferenceStore prefs = PrefsDialog.getStore();
+        int x = prefs.getInt(PrefsDialog.SHELL_X);
+        int y = prefs.getInt(PrefsDialog.SHELL_Y);
+        int w = prefs.getInt(PrefsDialog.SHELL_WIDTH);
+        int h = prefs.getInt(PrefsDialog.SHELL_HEIGHT);
+
+        // check that we're not out of the display area
+        Rectangle rect = mDisplay.getClientArea();
+        // first check the width/height
+        if (w > rect.width) {
+            w = rect.width;
+            prefs.setValue(PrefsDialog.SHELL_WIDTH, rect.width);
+        }
+        if (h > rect.height) {
+            h = rect.height;
+            prefs.setValue(PrefsDialog.SHELL_HEIGHT, rect.height);
+        }
+        // then check x. Make sure the left corner is in the screen
+        if (x < rect.x) {
+            x = rect.x;
+            prefs.setValue(PrefsDialog.SHELL_X, rect.x);
+        } else if (x >= rect.x + rect.width) {
+            x = rect.x + rect.width - w;
+            prefs.setValue(PrefsDialog.SHELL_X, rect.x);
+        }
+        // then check y. Make sure the left corner is in the screen
+        if (y < rect.y) {
+            y = rect.y;
+            prefs.setValue(PrefsDialog.SHELL_Y, rect.y);
+        } else if (y >= rect.y + rect.height) {
+            y = rect.y + rect.height - h;
+            prefs.setValue(PrefsDialog.SHELL_Y, rect.y);
+        }
+
+        // now we can set the location/size
+        shell.setBounds(x, y, w, h);
+
+        // add listener for resize/move
+        shell.addControlListener(new ControlListener() {
+            @Override
+            public void controlMoved(ControlEvent e) {
+                // get the new x/y
+                Rectangle controlBounds = shell.getBounds();
+                // store in pref file
+                PreferenceStore currentPrefs = PrefsDialog.getStore();
+                currentPrefs.setValue(PrefsDialog.SHELL_X, controlBounds.x);
+                currentPrefs.setValue(PrefsDialog.SHELL_Y, controlBounds.y);
+            }
+
+            @Override
+            public void controlResized(ControlEvent e) {
+                // get the new w/h
+                Rectangle controlBounds = shell.getBounds();
+                // store in pref file
+                PreferenceStore currentPrefs = PrefsDialog.getStore();
+                currentPrefs.setValue(PrefsDialog.SHELL_WIDTH, controlBounds.width);
+                currentPrefs.setValue(PrefsDialog.SHELL_HEIGHT, controlBounds.height);
+            }
+        });
+    }
+
+    /**
+     * Set the size and position of the file explorer window from the
+     * preference, and setup listeners for control events (resize/move of
+     * the window)
+     */
+    private void setExplorerSizeAndPosition(final Shell shell) {
+        shell.setMinimumSize(400, 200);
+
+        // get the x/y and w/h from the prefs
+        PreferenceStore prefs = PrefsDialog.getStore();
+        int x = prefs.getInt(PrefsDialog.EXPLORER_SHELL_X);
+        int y = prefs.getInt(PrefsDialog.EXPLORER_SHELL_Y);
+        int w = prefs.getInt(PrefsDialog.EXPLORER_SHELL_WIDTH);
+        int h = prefs.getInt(PrefsDialog.EXPLORER_SHELL_HEIGHT);
+
+        // check that we're not out of the display area
+        Rectangle rect = mDisplay.getClientArea();
+        // first check the width/height
+        if (w > rect.width) {
+            w = rect.width;
+            prefs.setValue(PrefsDialog.EXPLORER_SHELL_WIDTH, rect.width);
+        }
+        if (h > rect.height) {
+            h = rect.height;
+            prefs.setValue(PrefsDialog.EXPLORER_SHELL_HEIGHT, rect.height);
+        }
+        // then check x. Make sure the left corner is in the screen
+        if (x < rect.x) {
+            x = rect.x;
+            prefs.setValue(PrefsDialog.EXPLORER_SHELL_X, rect.x);
+        } else if (x >= rect.x + rect.width) {
+            x = rect.x + rect.width - w;
+            prefs.setValue(PrefsDialog.EXPLORER_SHELL_X, rect.x);
+        }
+        // then check y. Make sure the left corner is in the screen
+        if (y < rect.y) {
+            y = rect.y;
+            prefs.setValue(PrefsDialog.EXPLORER_SHELL_Y, rect.y);
+        } else if (y >= rect.y + rect.height) {
+            y = rect.y + rect.height - h;
+            prefs.setValue(PrefsDialog.EXPLORER_SHELL_Y, rect.y);
+        }
+
+        // now we can set the location/size
+        shell.setBounds(x, y, w, h);
+
+        // add listener for resize/move
+        shell.addControlListener(new ControlListener() {
+            @Override
+            public void controlMoved(ControlEvent e) {
+                // get the new x/y
+                Rectangle controlBounds = shell.getBounds();
+                // store in pref file
+                PreferenceStore currentPrefs = PrefsDialog.getStore();
+                currentPrefs.setValue(PrefsDialog.EXPLORER_SHELL_X, controlBounds.x);
+                currentPrefs.setValue(PrefsDialog.EXPLORER_SHELL_Y, controlBounds.y);
+            }
+
+            @Override
+            public void controlResized(ControlEvent e) {
+                // get the new w/h
+                Rectangle controlBounds = shell.getBounds();
+                // store in pref file
+                PreferenceStore currentPrefs = PrefsDialog.getStore();
+                currentPrefs.setValue(PrefsDialog.EXPLORER_SHELL_WIDTH, controlBounds.width);
+                currentPrefs.setValue(PrefsDialog.EXPLORER_SHELL_HEIGHT, controlBounds.height);
+            }
+        });
+    }
+
+    /*
+     * Set the confirm-before-close dialog.
+     */
+    private void setConfirmClose(final Shell shell) {
+        // Note: there was some commented out code to display a confirmation box
+        // when closing. The feature seems unnecessary and the code was not being
+        // used, so it has been removed.
+    }
+
+    /*
+     * Create the menu bar and items.
+     */
+    private void createMenus(final Shell shell) {
+        // create menu bar
+        Menu menuBar = new Menu(shell, SWT.BAR);
+
+        // create top-level items
+        MenuItem fileItem = new MenuItem(menuBar, SWT.CASCADE);
+        fileItem.setText("&File");
+        MenuItem editItem = new MenuItem(menuBar, SWT.CASCADE);
+        editItem.setText("&Edit");
+        MenuItem actionItem = new MenuItem(menuBar, SWT.CASCADE);
+        actionItem.setText("&Actions");
+        MenuItem deviceItem = new MenuItem(menuBar, SWT.CASCADE);
+        deviceItem.setText("&Device");
+
+        // create top-level menus
+        Menu fileMenu = new Menu(menuBar);
+        fileItem.setMenu(fileMenu);
+        Menu editMenu = new Menu(menuBar);
+        editItem.setMenu(editMenu);
+        Menu actionMenu = new Menu(menuBar);
+        actionItem.setMenu(actionMenu);
+        Menu deviceMenu = new Menu(menuBar);
+        deviceItem.setMenu(deviceMenu);
+
+        MenuItem item;
+
+        // create File menu items
+        item = new MenuItem(fileMenu, SWT.NONE);
+        item.setText("&Static Port Configuration...");
+        item.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                StaticPortConfigDialog dlg = new StaticPortConfigDialog(shell);
+                dlg.open();
+            }
+        });
+
+        IMenuBarEnhancer enhancer = MenuBarEnhancer.setupMenu(APP_NAME, fileMenu,
+                new IMenuBarCallback() {
+            @Override
+            public void printError(String format, Object... args) {
+                Log.e("DDMS Menu Bar", String.format(format, args));
+            }
+
+            @Override
+            public void onPreferencesMenuSelected() {
+                PrefsDialog.run(shell);
+            }
+
+            @Override
+            public void onAboutMenuSelected() {
+                AboutDialog dlg = new AboutDialog(shell);
+                dlg.open();
+            }
+        });
+
+        if (enhancer.getMenuBarMode() == MenuBarMode.GENERIC) {
+            new MenuItem(fileMenu, SWT.SEPARATOR);
+
+            item = new MenuItem(fileMenu, SWT.NONE);
+            item.setText("E&xit\tCtrl-Q");
+            item.setAccelerator('Q' | SWT.MOD1);
+            item.addSelectionListener(new SelectionAdapter() {
+                @Override
+                public void widgetSelected(SelectionEvent e) {
+                    shell.close();
+                }
+            });
+        }
+
+        // create edit menu items
+        mCopyMenuItem = new MenuItem(editMenu, SWT.NONE);
+        mCopyMenuItem.setText("&Copy\tCtrl-C");
+        mCopyMenuItem.setAccelerator('C' | SWT.MOD1);
+        mCopyMenuItem.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                mTableListener.copy(mClipboard);
+            }
+        });
+
+        new MenuItem(editMenu, SWT.SEPARATOR);
+
+        mSelectAllMenuItem = new MenuItem(editMenu, SWT.NONE);
+        mSelectAllMenuItem.setText("Select &All\tCtrl-A");
+        mSelectAllMenuItem.setAccelerator('A' | SWT.MOD1);
+        mSelectAllMenuItem.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                mTableListener.selectAll();
+            }
+        });
+
+        // create Action menu items
+        // TODO: this should come with a confirmation dialog
+        final MenuItem actionHaltItem = new MenuItem(actionMenu, SWT.NONE);
+        actionHaltItem.setText("&Halt VM");
+        actionHaltItem.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                mDevicePanel.killSelectedClient();
+            }
+        });
+
+        final MenuItem actionCauseGcItem = new MenuItem(actionMenu, SWT.NONE);
+        actionCauseGcItem.setText("Cause &GC");
+        actionCauseGcItem.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                mDevicePanel.forceGcOnSelectedClient();
+            }
+        });
+
+        final MenuItem actionResetAdb = new MenuItem(actionMenu, SWT.NONE);
+        actionResetAdb.setText("&Reset adb");
+        actionResetAdb.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                AndroidDebugBridge bridge = AndroidDebugBridge.getBridge();
+                if (bridge != null) {
+                    bridge.restart();
+                }
+            }
+        });
+
+        // configure Action items based on current state
+        actionMenu.addMenuListener(new MenuAdapter() {
+            @Override
+            public void menuShown(MenuEvent e) {
+                actionHaltItem.setEnabled(mTBHalt.getEnabled() && mCurrentClient != null);
+                actionCauseGcItem.setEnabled(mTBCauseGc.getEnabled() && mCurrentClient != null);
+                actionResetAdb.setEnabled(true);
+            }
+        });
+
+        // create Device menu items
+        final MenuItem screenShotItem = new MenuItem(deviceMenu, SWT.NONE);
+
+        // The \tCtrl-S "keybinding text" here isn't right for the Mac - but
+        // it's stripped out and replaced by the proper keyboard accelerator
+        // text (e.g. the unicode symbol for the command key + S) anyway
+        // so it's fine to leave it there for the other platforms.
+        screenShotItem.setText("&Screen capture...\tCtrl-S");
+        screenShotItem.setAccelerator('S' | SWT.MOD1);
+        screenShotItem.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                if (mCurrentDevice != null) {
+                    ScreenShotDialog dlg = new ScreenShotDialog(shell);
+                    dlg.open(mCurrentDevice);
+                }
+            }
+        });
+
+        new MenuItem(deviceMenu, SWT.SEPARATOR);
+
+        final MenuItem explorerItem = new MenuItem(deviceMenu, SWT.NONE);
+        explorerItem.setText("File Explorer...");
+        explorerItem.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                createFileExplorer();
+            }
+        });
+
+        new MenuItem(deviceMenu, SWT.SEPARATOR);
+
+        final MenuItem processItem = new MenuItem(deviceMenu, SWT.NONE);
+        processItem.setText("Show &process status...");
+        processItem.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                DeviceCommandDialog dlg;
+                dlg = new DeviceCommandDialog("ps -x", "ps-x.txt", shell);
+                dlg.open(mCurrentDevice);
+            }
+        });
+
+        final MenuItem deviceStateItem = new MenuItem(deviceMenu, SWT.NONE);
+        deviceStateItem.setText("Dump &device state...");
+        deviceStateItem.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                DeviceCommandDialog dlg;
+                dlg = new DeviceCommandDialog("/system/bin/dumpstate /proc/self/fd/0",
+                        "device-state.txt", shell);
+                dlg.open(mCurrentDevice);
+            }
+        });
+
+        final MenuItem appStateItem = new MenuItem(deviceMenu, SWT.NONE);
+        appStateItem.setText("Dump &app state...");
+        appStateItem.setEnabled(false);
+        appStateItem.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                DeviceCommandDialog dlg;
+                dlg = new DeviceCommandDialog("dumpsys", "app-state.txt", shell);
+                dlg.open(mCurrentDevice);
+            }
+        });
+
+        final MenuItem radioStateItem = new MenuItem(deviceMenu, SWT.NONE);
+        radioStateItem.setText("Dump &radio state...");
+        radioStateItem.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                DeviceCommandDialog dlg;
+                dlg = new DeviceCommandDialog(
+                        "cat /data/logs/radio.4 /data/logs/radio.3"
+                        + " /data/logs/radio.2 /data/logs/radio.1"
+                        + " /data/logs/radio",
+                        "radio-state.txt", shell);
+                dlg.open(mCurrentDevice);
+            }
+        });
+
+        final MenuItem logCatItem = new MenuItem(deviceMenu, SWT.NONE);
+        logCatItem.setText("Run &logcat...");
+        logCatItem.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                DeviceCommandDialog dlg;
+                dlg = new DeviceCommandDialog("logcat '*:d jdwp:w'", "log.txt",
+                        shell);
+                dlg.open(mCurrentDevice);
+            }
+        });
+
+        // configure Action items based on current state
+        deviceMenu.addMenuListener(new MenuAdapter() {
+            @Override
+            public void menuShown(MenuEvent e) {
+                boolean deviceEnabled = mCurrentDevice != null;
+                screenShotItem.setEnabled(deviceEnabled);
+                explorerItem.setEnabled(deviceEnabled);
+                processItem.setEnabled(deviceEnabled);
+                deviceStateItem.setEnabled(deviceEnabled);
+                appStateItem.setEnabled(deviceEnabled);
+                radioStateItem.setEnabled(deviceEnabled);
+                logCatItem.setEnabled(deviceEnabled);
+            }
+        });
+
+        // tell the shell to use this menu
+        shell.setMenuBar(menuBar);
+    }
+
+    /*
+     * Create the widgets in the main application window. The basic layout is a
+     * two-panel sash, with a scrolling list of VMs on the left and detailed
+     * output for a single VM on the right.
+     */
+    private void createWidgets(final Shell shell) {
+        Color darkGray = shell.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY);
+
+        /*
+         * Create three areas: tool bar, split panels, status line
+         */
+        shell.setLayout(new GridLayout(1, false));
+
+        // 1. panel area
+        final Composite panelArea = new Composite(shell, SWT.BORDER);
+
+        // make the panel area absorb all space
+        panelArea.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        // 2. status line.
+        mStatusLine = new Label(shell, SWT.NONE);
+
+        // make status line extend all the way across
+        mStatusLine.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        mStatusLine.setText("Initializing...");
+
+        /*
+         * Configure the split-panel area.
+         */
+        final PreferenceStore prefs = PrefsDialog.getStore();
+
+        Composite topPanel = new Composite(panelArea, SWT.NONE);
+        final Sash sash = new Sash(panelArea, SWT.HORIZONTAL);
+        sash.setBackground(darkGray);
+        Composite bottomPanel = new Composite(panelArea, SWT.NONE);
+
+        panelArea.setLayout(new FormLayout());
+
+        createTopPanel(topPanel, darkGray);
+
+        mClipboard = new Clipboard(panelArea.getDisplay());
+        if (useOldLogCatView()) {
+            createBottomPanel(bottomPanel);
+        } else {
+            createLogCatView(bottomPanel);
+        }
+
+        // form layout data
+        FormData data = new FormData();
+        data.top = new FormAttachment(0, 0);
+        data.bottom = new FormAttachment(sash, 0);
+        data.left = new FormAttachment(0, 0);
+        data.right = new FormAttachment(100, 0);
+        topPanel.setLayoutData(data);
+
+        final FormData sashData = new FormData();
+        if (prefs != null && prefs.contains(PREFERENCE_LOGSASH)) {
+            sashData.top = new FormAttachment(0, prefs.getInt(
+                    PREFERENCE_LOGSASH));
+        } else {
+            sashData.top = new FormAttachment(50,0); // 50% across
+        }
+        sashData.left = new FormAttachment(0, 0);
+        sashData.right = new FormAttachment(100, 0);
+        sash.setLayoutData(sashData);
+
+        data = new FormData();
+        data.top = new FormAttachment(sash, 0);
+        data.bottom = new FormAttachment(100, 0);
+        data.left = new FormAttachment(0, 0);
+        data.right = new FormAttachment(100, 0);
+        bottomPanel.setLayoutData(data);
+
+        // allow resizes, but cap at minPanelWidth
+        sash.addListener(SWT.Selection, new Listener() {
+            @Override
+            public void handleEvent(Event e) {
+                Rectangle sashRect = sash.getBounds();
+                Rectangle panelRect = panelArea.getClientArea();
+                int bottom = panelRect.height - sashRect.height - 100;
+                e.y = Math.max(Math.min(e.y, bottom), 100);
+                if (e.y != sashRect.y) {
+                    sashData.top = new FormAttachment(0, e.y);
+                    if (prefs != null) {
+                        prefs.setValue(PREFERENCE_LOGSASH, e.y);
+                    }
+                    panelArea.layout();
+                }
+            }
+        });
+
+        // add a global focus listener for all the tables
+        mTableListener = new TableFocusListener();
+
+        // now set up the listener in the various panels
+        if (useOldLogCatView()) {
+            mLogPanel.setTableFocusListener(mTableListener);
+        } else {
+            mLogCatPanel.setTableFocusListener(mTableListener);
+        }
+        mEventLogPanel.setTableFocusListener(mTableListener);
+        for (TablePanel p : mPanels) {
+            if (p != null) {
+                p.setTableFocusListener(mTableListener);
+            }
+        }
+
+        mStatusLine.setText("");
+    }
+
+    /*
+     * Populate the tool bar.
+     */
+    private void createDevicePanelToolBar(ToolBar toolBar) {
+        Display display = toolBar.getDisplay();
+
+        // add "show heap updates" button
+        mTBShowHeapUpdates = new ToolItem(toolBar, SWT.CHECK);
+        mTBShowHeapUpdates.setImage(mDdmUiLibLoader.loadImage(display,
+                DevicePanel.ICON_HEAP, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+        mTBShowHeapUpdates.setToolTipText("Show heap updates");
+        mTBShowHeapUpdates.setEnabled(false);
+        mTBShowHeapUpdates.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                if (mCurrentClient != null) {
+                    // boolean status = ((ToolItem)e.item).getSelection();
+                    // invert previous state
+                    boolean enable = !mCurrentClient.isHeapUpdateEnabled();
+                    mCurrentClient.setHeapUpdateEnabled(enable);
+                } else {
+                    e.doit = false; // this has no effect?
+                }
+            }
+        });
+
+        // add "dump HPROF" button
+        mTBDumpHprof = new ToolItem(toolBar, SWT.PUSH);
+        mTBDumpHprof.setToolTipText("Dump HPROF file");
+        mTBDumpHprof.setEnabled(false);
+        mTBDumpHprof.setImage(mDdmUiLibLoader.loadImage(display,
+                DevicePanel.ICON_HPROF, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+        mTBDumpHprof.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                mDevicePanel.dumpHprof();
+
+                // this will make sure the dump hprof button is disabled for the current selection
+                // as the client is already dumping an hprof file
+                enableButtons();
+            }
+        });
+
+        // add "cause GC" button
+        mTBCauseGc = new ToolItem(toolBar, SWT.PUSH);
+        mTBCauseGc.setToolTipText("Cause an immediate GC");
+        mTBCauseGc.setEnabled(false);
+        mTBCauseGc.setImage(mDdmUiLibLoader.loadImage(display,
+                DevicePanel.ICON_GC, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+        mTBCauseGc.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                mDevicePanel.forceGcOnSelectedClient();
+            }
+        });
+
+        new ToolItem(toolBar, SWT.SEPARATOR);
+
+        // add "show thread updates" button
+        mTBShowThreadUpdates = new ToolItem(toolBar, SWT.CHECK);
+        mTBShowThreadUpdates.setImage(mDdmUiLibLoader.loadImage(display,
+                DevicePanel.ICON_THREAD, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+        mTBShowThreadUpdates.setToolTipText("Show thread updates");
+        mTBShowThreadUpdates.setEnabled(false);
+        mTBShowThreadUpdates.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                if (mCurrentClient != null) {
+                    // boolean status = ((ToolItem)e.item).getSelection();
+                    // invert previous state
+                    boolean enable = !mCurrentClient.isThreadUpdateEnabled();
+
+                    mCurrentClient.setThreadUpdateEnabled(enable);
+                } else {
+                    e.doit = false; // this has no effect?
+                }
+            }
+        });
+
+        // add a start/stop method tracing
+        mTracingStartImage = mDdmUiLibLoader.loadImage(display,
+                DevicePanel.ICON_TRACING_START,
+                DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null);
+        mTracingStopImage = mDdmUiLibLoader.loadImage(display,
+                DevicePanel.ICON_TRACING_STOP,
+                DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null);
+        mTBProfiling = new ToolItem(toolBar, SWT.PUSH);
+        mTBProfiling.setToolTipText("Start Method Profiling");
+        mTBProfiling.setEnabled(false);
+        mTBProfiling.setImage(mTracingStartImage);
+        mTBProfiling.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                mDevicePanel.toggleMethodProfiling();
+            }
+        });
+
+        new ToolItem(toolBar, SWT.SEPARATOR);
+
+        // add "kill VM" button; need to make this visually distinct from
+        // the status update buttons
+        mTBHalt = new ToolItem(toolBar, SWT.PUSH);
+        mTBHalt.setToolTipText("Halt the target VM");
+        mTBHalt.setEnabled(false);
+        mTBHalt.setImage(mDdmUiLibLoader.loadImage(display,
+                DevicePanel.ICON_HALT, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+        mTBHalt.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                mDevicePanel.killSelectedClient();
+            }
+        });
+
+       toolBar.pack();
+    }
+
+    private void createTopPanel(final Composite comp, Color darkGray) {
+        final PreferenceStore prefs = PrefsDialog.getStore();
+
+        comp.setLayout(new FormLayout());
+
+        Composite leftPanel = new Composite(comp, SWT.NONE);
+        final Sash sash = new Sash(comp, SWT.VERTICAL);
+        sash.setBackground(darkGray);
+        Composite rightPanel = new Composite(comp, SWT.NONE);
+
+        createLeftPanel(leftPanel);
+        createRightPanel(rightPanel);
+
+        FormData data = new FormData();
+        data.top = new FormAttachment(0, 0);
+        data.bottom = new FormAttachment(100, 0);
+        data.left = new FormAttachment(0, 0);
+        data.right = new FormAttachment(sash, 0);
+        leftPanel.setLayoutData(data);
+
+        final FormData sashData = new FormData();
+        sashData.top = new FormAttachment(0, 0);
+        sashData.bottom = new FormAttachment(100, 0);
+        if (prefs != null && prefs.contains(PREFERENCE_SASH)) {
+            sashData.left = new FormAttachment(0, prefs.getInt(
+                    PREFERENCE_SASH));
+        } else {
+            // position the sash 380 from the right instead of x% (done by using
+            // FormAttachment(x, 0)) in order to keep the sash at the same
+            // position
+            // from the left when the window is resized.
+            // 380px is just enough to display the left table with no horizontal
+            // scrollbar with the default font.
+            sashData.left = new FormAttachment(0, 380);
+        }
+        sash.setLayoutData(sashData);
+
+        data = new FormData();
+        data.top = new FormAttachment(0, 0);
+        data.bottom = new FormAttachment(100, 0);
+        data.left = new FormAttachment(sash, 0);
+        data.right = new FormAttachment(100, 0);
+        rightPanel.setLayoutData(data);
+
+        final int minPanelWidth = 60;
+
+        // allow resizes, but cap at minPanelWidth
+        sash.addListener(SWT.Selection, new Listener() {
+            @Override
+            public void handleEvent(Event e) {
+                Rectangle sashRect = sash.getBounds();
+                Rectangle panelRect = comp.getClientArea();
+                int right = panelRect.width - sashRect.width - minPanelWidth;
+                e.x = Math.max(Math.min(e.x, right), minPanelWidth);
+                if (e.x != sashRect.x) {
+                    sashData.left = new FormAttachment(0, e.x);
+                    if (prefs != null) {
+                        prefs.setValue(PREFERENCE_SASH, e.x);
+                    }
+                    comp.layout();
+                }
+            }
+        });
+    }
+
+    private void createBottomPanel(final Composite comp) {
+        final PreferenceStore prefs = PrefsDialog.getStore();
+
+        // create clipboard
+        Display display = comp.getDisplay();
+
+        LogColors colors = new LogColors();
+
+        colors.infoColor = new Color(display, 0, 127, 0);
+        colors.debugColor = new Color(display, 0, 0, 127);
+        colors.errorColor = new Color(display, 255, 0, 0);
+        colors.warningColor = new Color(display, 255, 127, 0);
+        colors.verboseColor = new Color(display, 0, 0, 0);
+
+        // set the preferences names
+        LogPanel.PREFS_TIME = PREFS_COL_TIME;
+        LogPanel.PREFS_LEVEL = PREFS_COL_LEVEL;
+        LogPanel.PREFS_PID = PREFS_COL_PID;
+        LogPanel.PREFS_TAG = PREFS_COL_TAG;
+        LogPanel.PREFS_MESSAGE = PREFS_COL_MESSAGE;
+
+        comp.setLayout(new GridLayout(1, false));
+
+        ToolBar toolBar = new ToolBar(comp, SWT.HORIZONTAL);
+
+        mCreateFilterAction = new ToolItemAction(toolBar, SWT.PUSH);
+        mCreateFilterAction.item.setToolTipText("Create Filter");
+        mCreateFilterAction.item.setImage(mDdmUiLibLoader.loadImage(mDisplay,
+                "add.png", //$NON-NLS-1$
+                DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+        mCreateFilterAction.item.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                mLogPanel.addFilter();
+            }
+        });
+
+        mEditFilterAction = new ToolItemAction(toolBar, SWT.PUSH);
+        mEditFilterAction.item.setToolTipText("Edit Filter");
+        mEditFilterAction.item.setImage(mDdmUiLibLoader.loadImage(mDisplay,
+                "edit.png", //$NON-NLS-1$
+                DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+        mEditFilterAction.item.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                mLogPanel.editFilter();
+            }
+        });
+
+        mDeleteFilterAction = new ToolItemAction(toolBar, SWT.PUSH);
+        mDeleteFilterAction.item.setToolTipText("Delete Filter");
+        mDeleteFilterAction.item.setImage(mDdmUiLibLoader.loadImage(mDisplay,
+                "delete.png", //$NON-NLS-1$
+                DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+        mDeleteFilterAction.item.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                mLogPanel.deleteFilter();
+            }
+        });
+
+
+        new ToolItem(toolBar, SWT.SEPARATOR);
+
+        LogLevel[] levels = LogLevel.values();
+        mLogLevelActions = new ToolItemAction[mLogLevelIcons.length];
+        for (int i = 0 ; i < mLogLevelActions.length; i++) {
+            String name = levels[i].getStringValue();
+            final ToolItemAction newAction = new ToolItemAction(toolBar, SWT.CHECK);
+            mLogLevelActions[i] = newAction;
+            //newAction.item.setText(name);
+            newAction.item.addSelectionListener(new SelectionAdapter() {
+                @Override
+                public void widgetSelected(SelectionEvent e) {
+                    // disable the other actions and record current index
+                    for (int k = 0 ; k < mLogLevelActions.length; k++) {
+                        ToolItemAction a = mLogLevelActions[k];
+                        if (a == newAction) {
+                            a.setChecked(true);
+
+                            // set the log level
+                            mLogPanel.setCurrentFilterLogLevel(k+2);
+                        } else {
+                            a.setChecked(false);
+                        }
+                    }
+                }
+            });
+
+            newAction.item.setToolTipText(name);
+            newAction.item.setImage(mDdmUiLibLoader.loadImage(mDisplay,
+                    mLogLevelIcons[i],
+                    DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+        }
+
+        new ToolItem(toolBar, SWT.SEPARATOR);
+
+        mClearAction = new ToolItemAction(toolBar, SWT.PUSH);
+        mClearAction.item.setToolTipText("Clear Log");
+
+        mClearAction.item.setImage(mDdmUiLibLoader.loadImage(mDisplay,
+                "clear.png", //$NON-NLS-1$
+                DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+        mClearAction.item.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                mLogPanel.clear();
+            }
+        });
+
+        new ToolItem(toolBar, SWT.SEPARATOR);
+
+        mExportAction = new ToolItemAction(toolBar, SWT.PUSH);
+        mExportAction.item.setToolTipText("Export Selection As Text...");
+        mExportAction.item.setImage(mDdmUiLibLoader.loadImage(mDisplay,
+                "save.png", //$NON-NLS-1$
+                DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+        mExportAction.item.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                mLogPanel.save();
+            }
+        });
+
+
+        toolBar.pack();
+
+        // now create the log view
+        mLogPanel = new LogPanel(colors, new FilterStorage(), LogPanel.FILTER_MANUAL);
+
+        mLogPanel.setActions(mDeleteFilterAction, mEditFilterAction, mLogLevelActions);
+
+        String colMode = prefs.getString(PrefsDialog.LOGCAT_COLUMN_MODE);
+        if (PrefsDialog.LOGCAT_COLUMN_MODE_AUTO.equals(colMode)) {
+            mLogPanel.setColumnMode(LogPanel.COLUMN_MODE_AUTO);
+        }
+
+        String fontStr = PrefsDialog.getStore().getString(PrefsDialog.LOGCAT_FONT);
+        if (fontStr != null) {
+            try {
+                FontData fdat = new FontData(fontStr);
+                mLogPanel.setFont(new Font(display, fdat));
+            } catch (IllegalArgumentException e) {
+                // Looks like fontStr isn't a valid font representation.
+                // We do nothing in this case, the logcat view will use the default font.
+            } catch (SWTError e2) {
+                // Looks like the Font() constructor failed.
+                // We do nothing in this case, the logcat view will use the default font.
+            }
+        }
+
+        mLogPanel.createPanel(comp);
+
+        // and start the logcat
+        mLogPanel.startLogCat(mCurrentDevice);
+    }
+
+    private void createLogCatView(Composite parent) {
+        IPreferenceStore prefStore = DdmUiPreferences.getStore();
+        mLogCatPanel = new LogCatPanel(prefStore);
+        mLogCatPanel.createPanel(parent);
+
+        if (mCurrentDevice != null) {
+            mLogCatPanel.deviceSelected(mCurrentDevice);
+        }
+    }
+
+    /*
+     * Create the contents of the left panel: a table of VMs.
+     */
+    private void createLeftPanel(final Composite comp) {
+        comp.setLayout(new GridLayout(1, false));
+        ToolBar toolBar = new ToolBar(comp, SWT.HORIZONTAL | SWT.RIGHT | SWT.WRAP);
+        toolBar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        createDevicePanelToolBar(toolBar);
+
+        Composite c = new Composite(comp, SWT.NONE);
+        c.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        mDevicePanel = new DevicePanel(true /* showPorts */);
+        mDevicePanel.createPanel(c);
+
+        // add ourselves to the device panel selection listener
+        mDevicePanel.addSelectionListener(this);
+    }
+
+    /*
+     * Create the contents of the right panel: tabs with VM information.
+     */
+    private void createRightPanel(final Composite comp) {
+        TabItem item;
+        TabFolder tabFolder;
+
+        comp.setLayout(new FillLayout());
+
+        tabFolder = new TabFolder(comp, SWT.NONE);
+
+        for (int i = 0; i < mPanels.length; i++) {
+            if (mPanels[i] != null) {
+                item = new TabItem(tabFolder, SWT.NONE);
+                item.setText(mPanelNames[i]);
+                item.setToolTipText(mPanelTips[i]);
+                item.setControl(mPanels[i].createPanel(tabFolder));
+            }
+        }
+
+        // add the emulator control panel to the folders.
+        item = new TabItem(tabFolder, SWT.NONE);
+        item.setText("Emulator Control");
+        item.setToolTipText("Emulator Control Panel");
+        mEmulatorPanel = new EmulatorControlPanel();
+        item.setControl(mEmulatorPanel.createPanel(tabFolder));
+
+        // add the event log panel to the folders.
+        item = new TabItem(tabFolder, SWT.NONE);
+        item.setText("Event Log");
+        item.setToolTipText("Event Log");
+
+        // create the composite that will hold the toolbar and the event log panel.
+        Composite eventLogTopComposite = new Composite(tabFolder, SWT.NONE);
+        item.setControl(eventLogTopComposite);
+        eventLogTopComposite.setLayout(new GridLayout(1, false));
+
+        // create the toolbar and the actions
+        ToolBar toolbar = new ToolBar(eventLogTopComposite, SWT.HORIZONTAL);
+        toolbar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        ToolItemAction optionsAction = new ToolItemAction(toolbar, SWT.PUSH);
+        optionsAction.item.setToolTipText("Opens the options panel");
+        optionsAction.item.setImage(mDdmUiLibLoader.loadImage(comp.getDisplay(),
+                "edit.png", //$NON-NLS-1$
+                DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+
+        ToolItemAction clearAction = new ToolItemAction(toolbar, SWT.PUSH);
+        clearAction.item.setToolTipText("Clears the event log");
+        clearAction.item.setImage(mDdmUiLibLoader.loadImage(comp.getDisplay(),
+                "clear.png", //$NON-NLS-1$
+                DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+
+        new ToolItem(toolbar, SWT.SEPARATOR);
+
+        ToolItemAction saveAction = new ToolItemAction(toolbar, SWT.PUSH);
+        saveAction.item.setToolTipText("Saves the event log");
+        saveAction.item.setImage(mDdmUiLibLoader.loadImage(comp.getDisplay(),
+                "save.png", //$NON-NLS-1$
+                DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+
+        ToolItemAction loadAction = new ToolItemAction(toolbar, SWT.PUSH);
+        loadAction.item.setToolTipText("Loads an event log");
+        loadAction.item.setImage(mDdmUiLibLoader.loadImage(comp.getDisplay(),
+                "load.png", //$NON-NLS-1$
+                DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+
+        ToolItemAction importBugAction = new ToolItemAction(toolbar, SWT.PUSH);
+        importBugAction.item.setToolTipText("Imports a bug report");
+        importBugAction.item.setImage(mDdmUiLibLoader.loadImage(comp.getDisplay(),
+                "importBug.png", //$NON-NLS-1$
+                DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null));
+
+        // create the event log panel
+        mEventLogPanel = new EventLogPanel();
+
+        // set the external actions
+        mEventLogPanel.setActions(optionsAction, clearAction, saveAction, loadAction,
+                importBugAction);
+
+        // create the panel
+        mEventLogPanel.createPanel(eventLogTopComposite);
+    }
+
+    private void createFileExplorer() {
+        if (mExplorer == null) {
+            mExplorerShell = new Shell(mDisplay);
+
+            // create the ui
+            mExplorerShell.setLayout(new GridLayout(1, false));
+
+            // toolbar + action
+            ToolBar toolBar = new ToolBar(mExplorerShell, SWT.HORIZONTAL);
+            toolBar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+            ToolItemAction pullAction = new ToolItemAction(toolBar, SWT.PUSH);
+            pullAction.item.setToolTipText("Pull File from Device");
+            Image image = mDdmUiLibLoader.loadImage("pull.png", mDisplay); //$NON-NLS-1$
+            if (image != null) {
+                pullAction.item.setImage(image);
+            } else {
+                // this is for debugging purpose when the icon is missing
+                pullAction.item.setText("Pull"); //$NON-NLS-1$
+            }
+
+            ToolItemAction pushAction = new ToolItemAction(toolBar, SWT.PUSH);
+            pushAction.item.setToolTipText("Push file onto Device");
+            image = mDdmUiLibLoader.loadImage("push.png", mDisplay); //$NON-NLS-1$
+            if (image != null) {
+                pushAction.item.setImage(image);
+            } else {
+                // this is for debugging purpose when the icon is missing
+                pushAction.item.setText("Push"); //$NON-NLS-1$
+            }
+
+            ToolItemAction deleteAction = new ToolItemAction(toolBar, SWT.PUSH);
+            deleteAction.item.setToolTipText("Delete");
+            image = mDdmUiLibLoader.loadImage("delete.png", mDisplay); //$NON-NLS-1$
+            if (image != null) {
+                deleteAction.item.setImage(image);
+            } else {
+                // this is for debugging purpose when the icon is missing
+                deleteAction.item.setText("Delete"); //$NON-NLS-1$
+            }
+
+            ToolItemAction createNewFolderAction = new ToolItemAction(toolBar, SWT.PUSH);
+            createNewFolderAction.item.setToolTipText("New Folder");
+            image = mDdmUiLibLoader.loadImage("add.png", mDisplay); //$NON-NLS-1$
+            if (image != null) {
+                createNewFolderAction.item.setImage(image);
+            } else {
+                // this is for debugging purpose when the icon is missing
+                createNewFolderAction.item.setText("New Folder"); //$NON-NLS-1$
+            }
+
+            // device explorer
+            mExplorer = new DeviceExplorer();
+            mExplorer.setActions(pushAction, pullAction, deleteAction, createNewFolderAction);
+
+            pullAction.item.addSelectionListener(new SelectionAdapter() {
+                @Override
+                public void widgetSelected(SelectionEvent e) {
+                    mExplorer.pullSelection();
+                }
+            });
+            pullAction.setEnabled(false);
+
+            pushAction.item.addSelectionListener(new SelectionAdapter() {
+                @Override
+                public void widgetSelected(SelectionEvent e) {
+                    mExplorer.pushIntoSelection();
+                }
+            });
+            pushAction.setEnabled(false);
+
+            deleteAction.item.addSelectionListener(new SelectionAdapter() {
+                @Override
+                public void widgetSelected(SelectionEvent e) {
+                    mExplorer.deleteSelection();
+                }
+            });
+            deleteAction.setEnabled(false);
+
+            createNewFolderAction.item.addSelectionListener(new SelectionAdapter() {
+                @Override
+                public void widgetSelected(SelectionEvent e) {
+                    mExplorer.createNewFolderInSelection();
+                }
+            });
+            createNewFolderAction.setEnabled(false);
+
+            Composite parent = new Composite(mExplorerShell, SWT.NONE);
+            parent.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+            mExplorer.createPanel(parent);
+            mExplorer.switchDevice(mCurrentDevice);
+
+            mExplorerShell.addShellListener(new ShellListener() {
+                @Override
+                public void shellActivated(ShellEvent e) {
+                    // pass
+                }
+
+                @Override
+                public void shellClosed(ShellEvent e) {
+                    mExplorer = null;
+                    mExplorerShell = null;
+                }
+
+                @Override
+                public void shellDeactivated(ShellEvent e) {
+                    // pass
+                }
+
+                @Override
+                public void shellDeiconified(ShellEvent e) {
+                    // pass
+                }
+
+                @Override
+                public void shellIconified(ShellEvent e) {
+                    // pass
+                }
+            });
+
+            mExplorerShell.pack();
+            setExplorerSizeAndPosition(mExplorerShell);
+            mExplorerShell.open();
+        } else {
+            if (mExplorerShell != null) {
+                mExplorerShell.forceActive();
+            }
+        }
+    }
+
+    /**
+     * Set the status line. TODO: make this a stack, so we can safely have
+     * multiple things trying to set it all at once. Also specify an expiration?
+     */
+    public void setStatusLine(final String str) {
+        try {
+            mDisplay.asyncExec(new Runnable() {
+                @Override
+                public void run() {
+                    doSetStatusLine(str);
+                }
+            });
+        } catch (SWTException swte) {
+            if (!mDisplay.isDisposed())
+                throw swte;
+        }
+    }
+
+    private void doSetStatusLine(String str) {
+        if (mStatusLine.isDisposed())
+            return;
+
+        if (!mStatusLine.getText().equals(str)) {
+            mStatusLine.setText(str);
+
+            // try { Thread.sleep(100); }
+            // catch (InterruptedException ie) {}
+        }
+    }
+
+    public void displayError(final String msg) {
+        try {
+            mDisplay.syncExec(new Runnable() {
+                @Override
+                public void run() {
+                    MessageDialog.openError(mDisplay.getActiveShell(), "Error",
+                            msg);
+                }
+            });
+        } catch (SWTException swte) {
+            if (!mDisplay.isDisposed())
+                throw swte;
+        }
+    }
+
+    private void enableButtons() {
+        if (mCurrentClient != null) {
+            mTBShowThreadUpdates.setSelection(mCurrentClient.isThreadUpdateEnabled());
+            mTBShowThreadUpdates.setEnabled(true);
+            mTBShowHeapUpdates.setSelection(mCurrentClient.isHeapUpdateEnabled());
+            mTBShowHeapUpdates.setEnabled(true);
+            mTBHalt.setEnabled(true);
+            mTBCauseGc.setEnabled(true);
+
+            ClientData data = mCurrentClient.getClientData();
+
+            if (data.hasFeature(ClientData.FEATURE_HPROF)) {
+                mTBDumpHprof.setEnabled(data.hasPendingHprofDump() == false);
+                mTBDumpHprof.setToolTipText("Dump HPROF file");
+            } else {
+                mTBDumpHprof.setEnabled(false);
+                mTBDumpHprof.setToolTipText("Dump HPROF file (not supported by this VM)");
+            }
+
+            if (data.hasFeature(ClientData.FEATURE_PROFILING)) {
+                mTBProfiling.setEnabled(true);
+                if (data.getMethodProfilingStatus() == MethodProfilingStatus.ON) {
+                    mTBProfiling.setToolTipText("Stop Method Profiling");
+                    mTBProfiling.setImage(mTracingStopImage);
+                } else {
+                    mTBProfiling.setToolTipText("Start Method Profiling");
+                    mTBProfiling.setImage(mTracingStartImage);
+                }
+            } else {
+                mTBProfiling.setEnabled(false);
+                mTBProfiling.setImage(mTracingStartImage);
+                mTBProfiling.setToolTipText("Start Method Profiling (not supported by this VM)");
+            }
+        } else {
+            // list is empty, disable these
+            mTBShowThreadUpdates.setSelection(false);
+            mTBShowThreadUpdates.setEnabled(false);
+            mTBShowHeapUpdates.setSelection(false);
+            mTBShowHeapUpdates.setEnabled(false);
+            mTBHalt.setEnabled(false);
+            mTBCauseGc.setEnabled(false);
+
+            mTBDumpHprof.setEnabled(false);
+            mTBDumpHprof.setToolTipText("Dump HPROF file");
+
+            mTBProfiling.setEnabled(false);
+            mTBProfiling.setImage(mTracingStartImage);
+            mTBProfiling.setToolTipText("Start Method Profiling");
+        }
+    }
+
+    /**
+     * Sent when a new {@link IDevice} and {@link Client} are selected.
+     * @param selectedDevice the selected device. If null, no devices are selected.
+     * @param selectedClient The selected client. If null, no clients are selected.
+     *
+     * @see IUiSelectionListener
+     */
+    @Override
+    public void selectionChanged(IDevice selectedDevice, Client selectedClient) {
+        if (mCurrentDevice != selectedDevice) {
+            mCurrentDevice = selectedDevice;
+            for (TablePanel panel : mPanels) {
+                if (panel != null) {
+                    panel.deviceSelected(mCurrentDevice);
+                }
+            }
+
+            mEmulatorPanel.deviceSelected(mCurrentDevice);
+            if (useOldLogCatView()) {
+                mLogPanel.deviceSelected(mCurrentDevice);
+            } else {
+                mLogCatPanel.deviceSelected(mCurrentDevice);
+            }
+            if (mEventLogPanel != null) {
+                mEventLogPanel.deviceSelected(mCurrentDevice);
+            }
+
+            if (mExplorer != null) {
+                mExplorer.switchDevice(mCurrentDevice);
+            }
+        }
+
+        if (mCurrentClient != selectedClient) {
+            AndroidDebugBridge.getBridge().setSelectedClient(selectedClient);
+            mCurrentClient = selectedClient;
+            for (TablePanel panel : mPanels) {
+                if (panel != null) {
+                    panel.clientSelected(mCurrentClient);
+                }
+            }
+
+            enableButtons();
+        }
+    }
+
+    @Override
+    public void clientChanged(Client client, int changeMask) {
+        if ((changeMask & Client.CHANGE_METHOD_PROFILING_STATUS) ==
+                Client.CHANGE_METHOD_PROFILING_STATUS) {
+            if (mCurrentClient == client) {
+                mDisplay.asyncExec(new Runnable() {
+                    @Override
+                    public void run() {
+                        // force refresh of the button enabled state.
+                        enableButtons();
+                    }
+                });
+            }
+        }
+    }
+}
diff --git a/ddms/app/src/main/resources/images/ddms-128.png b/ddms/app/src/main/resources/images/ddms-128.png
new file mode 100644
index 0000000..392a8f3
Binary files /dev/null and b/ddms/app/src/main/resources/images/ddms-128.png differ
diff --git a/ddms/ddmuilib/.classpath b/ddms/ddmuilib/.classpath
new file mode 100644
index 0000000..07c1198
--- /dev/null
+++ b/ddms/ddmuilib/.classpath
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src/main/java"/>
+	<classpathentry kind="src" path="src/test/java"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+	<classpathentry combineaccessrules="false" exported="true" kind="src" path="/ddmlib"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/3"/>
+	<classpathentry exported="true" kind="var" path="ANDROID_OUT_FRAMEWORK/swt.jar"/>
+	<classpathentry exported="true" kind="var" path="ANDROID_SRC/prebuilts/tools/common/m2/repository/com/android/external/eclipse/org-eclipse-core-commands/3.6.0/org-eclipse-core-commands-3.6.0.jar"/>
+	<classpathentry exported="true" kind="var" path="ANDROID_SRC/prebuilts/tools/common/m2/repository/com/android/external/eclipse/org-eclipse-equinox-common/3.6.0/org-eclipse-equinox-common-3.6.0.jar"/>
+	<classpathentry exported="true" kind="var" path="ANDROID_SRC/prebuilts/tools/common/m2/repository/com/android/external/eclipse/org-eclipse-jface/3.6.2/org-eclipse-jface-3.6.2.jar"/>
+	<classpathentry exported="true" kind="var" path="ANDROID_SRC/prebuilts/tools/common/m2/repository/jfree/jfreechart/1.0.9/jfreechart-1.0.9.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/m2/repository/jfree/jfreechart/1.0.9/jfreechart-1.0.9-sources.jar"/>
+	<classpathentry exported="true" kind="var" path="ANDROID_SRC/prebuilts/tools/common/m2/repository/jfree/jfreechart-swt/1.0.9/jfreechart-swt-1.0.9.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/m2/repository/jfree/jfreechart-swt/1.0.9/jfreechart-swt-1.0.9-sources.jar"/>
+	<classpathentry exported="true" kind="var" path="ANDROID_SRC/prebuilts/tools/common/m2/repository/jfree/jcommon/1.0.12/jcommon-1.0.12.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/m2/repository/jfree/jcommon/1.0.12/jcommon-1.0.12-sources.jar"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/ddms/ddmuilib/.project b/ddms/ddmuilib/.project
new file mode 100644
index 0000000..29cb2f2
--- /dev/null
+++ b/ddms/ddmuilib/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>ddmuilib</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>
diff --git a/ddms/ddmuilib/.settings/org.eclipse.jdt.core.prefs b/ddms/ddmuilib/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..9dbff07
--- /dev/null
+++ b/ddms/ddmuilib/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,98 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
+org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault
+org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled
+org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
+org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
+org.eclipse.jdt.core.compiler.problem.deadCode=warning
+org.eclipse.jdt.core.compiler.problem.deprecation=warning
+org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
+org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
+org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
+org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning
+org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled
+org.eclipse.jdt.core.compiler.problem.fieldHiding=warning
+org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
+org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=error
+org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
+org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled
+org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
+org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning
+org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning
+org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
+org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
+org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
+org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
+org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
+org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
+org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
+org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
+org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
+org.eclipse.jdt.core.compiler.problem.nullReference=error
+org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning
+org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore
+org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
+org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning
+org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
+org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
+org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
+org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled
+org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
+org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
+org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
+org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled
+org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning
+org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error
+org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning
+org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.unusedImport=warning
+org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
+org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
+org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning
+org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled
+org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
+org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
+org.eclipse.jdt.core.compiler.source=1.6
diff --git a/ddms/ddmuilib/NOTICE b/ddms/ddmuilib/NOTICE
new file mode 100644
index 0000000..c5b1efa
--- /dev/null
+++ b/ddms/ddmuilib/NOTICE
@@ -0,0 +1,190 @@
+
+   Copyright (c) 2005-2008, 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.
+
+   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.
+
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
diff --git a/ddms/ddmuilib/README b/ddms/ddmuilib/README
new file mode 100644
index 0000000..971e211
--- /dev/null
+++ b/ddms/ddmuilib/README
@@ -0,0 +1,14 @@
+Using the Eclipse projects for ddmuilib.
+
+ddmuilib requires SWT to compile.
+
+SWT is available in the depot under prebuild/<platform>/swt
+
+Because the build path cannot contain relative path that are not inside the project directory,
+the .classpath file references a user library called ANDROID_SWT.
+
+In order to compile the project, make a user library called ANDROID_SWT containing the jar files
+available at prebuild/<platform>/swt.
+
+You also need a user library called ANDROID_JFREECHART containing the jar files
+available at prebuild/common/jfreechart.
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/AbstractBufferFindTarget.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/AbstractBufferFindTarget.java
new file mode 100644
index 0000000..13a787a
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/AbstractBufferFindTarget.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2012 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.ddmuilib;
+
+import java.util.regex.Pattern;
+
+/**
+ * {@link AbstractBufferFindTarget} implements methods to find items inside a buffer. It takes
+ * care of the logic to search backwards/forwards in the buffer, wrapping around when necessary.
+ * The actual contents of the buffer should be provided by the classes that extend this.
+ */
+public abstract class AbstractBufferFindTarget implements IFindTarget {
+    private int mCurrentSearchIndex;
+
+    // Single element cache of the last search regex
+    private Pattern mLastSearchPattern;
+    private String mLastSearchText;
+
+    @Override
+    public boolean findAndSelect(String text, boolean isNewSearch, boolean searchForward) {
+        boolean found = false;
+        int maxIndex = getItemCount();
+
+        synchronized (this) {
+            // Find starting index for this search
+            if (isNewSearch) {
+                // for new searches, start from an appropriate place as provided by the delegate
+                mCurrentSearchIndex = getStartingIndex();
+            } else {
+                // for ongoing searches (finding next match for the same term), continue from
+                // the current result index
+                mCurrentSearchIndex = getNext(mCurrentSearchIndex, searchForward, maxIndex);
+            }
+
+            // Create a regex pattern based on the search term.
+            Pattern pattern;
+            if (text.equals(mLastSearchText)) {
+                pattern = mLastSearchPattern;
+            } else {
+                pattern = Pattern.compile(text, Pattern.CASE_INSENSITIVE);
+                mLastSearchPattern = pattern;
+                mLastSearchText = text;
+            }
+
+            // Iterate through the list of items. The search ends if we have gone through
+            // all items once.
+            int index = mCurrentSearchIndex;
+            do {
+                String msgText = getItem(mCurrentSearchIndex);
+                if (msgText != null && pattern.matcher(msgText).find()) {
+                    found = true;
+                    break;
+                }
+
+                mCurrentSearchIndex = getNext(mCurrentSearchIndex, searchForward, maxIndex);
+            } while (index != mCurrentSearchIndex); // loop through entire contents once
+        }
+
+        if (found) {
+            selectAndReveal(mCurrentSearchIndex);
+        }
+
+        return found;
+    }
+
+    /** Indicate that the log buffer has scrolled by certain number of elements */
+    public void scrollBy(int delta) {
+        synchronized (this) {
+            if (mCurrentSearchIndex > 0) {
+                mCurrentSearchIndex = Math.max(0, mCurrentSearchIndex - delta);
+            }
+        }
+    }
+
+    private int getNext(int index, boolean searchForward, int max) {
+        // increment or decrement index
+        index = searchForward ? index + 1 : index - 1;
+
+        // take care of underflow
+        if (index == -1) {
+            index = max - 1;
+        }
+
+        // ..and overflow
+        if (index == max) {
+            index = 0;
+        }
+
+        return index;
+    }
+
+    /** Obtain the number of items in the buffer */
+    public abstract int getItemCount();
+
+    /** Obtain the item at given index */
+    public abstract String getItem(int index);
+
+    /** Select and reveal the item at given index */
+    public abstract void selectAndReveal(int index);
+
+    /** Obtain the index from which search should begin */
+    public abstract int getStartingIndex();
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/Addr2Line.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/Addr2Line.java
new file mode 100644
index 0000000..10799ec
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/Addr2Line.java
@@ -0,0 +1,355 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib;
+
+import com.android.ddmlib.Log;
+import com.android.ddmlib.NativeLibraryMapInfo;
+import com.android.ddmlib.NativeStackCallInfo;
+
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Represents an addr2line process to get filename/method information from a
+ * memory address.<br>
+ * Each process can only handle one library, which should be provided when
+ * creating a new process.<br>
+ * <br>
+ * The processes take some time to load as they need to parse the library files.
+ * For this reason, processes cannot be manually started. Instead the class
+ * keeps an internal list of processes and one asks for a process for a specific
+ * library, using <code>getProcess(String library)<code>.<br></br>
+ * Internally, the processes are started in pipe mode to be able to query them
+ * with multiple addresses.
+ */
+public class Addr2Line {
+    private static final String ANDROID_SYMBOLS_ENVVAR = "ANDROID_SYMBOLS";
+
+    private static final String LIBRARY_NOT_FOUND_MESSAGE_FORMAT =
+            "Unable to locate library %s on disk. Addresses mapping to this library "
+          + "will not be resolved. In order to fix this, set the the library search path "
+          + "in the UI, or set the environment variable " + ANDROID_SYMBOLS_ENVVAR + ".";
+
+    /**
+     * Loaded processes list. This is also used as a locking object for any
+     * methods dealing with starting/stopping/creating processes/querying for
+     * method.
+     */
+    private static final HashMap<String, Addr2Line> sProcessCache =
+            new HashMap<String, Addr2Line>();
+
+    /**
+     * byte array representing a carriage return. Used to push addresses in the
+     * process pipes.
+     */
+    private static final byte[] sCrLf = {
+        '\n'
+    };
+
+    /** Path to the library */
+    private NativeLibraryMapInfo mLibrary;
+
+    /** the command line process */
+    private Process mProcess;
+
+    /** buffer to read the result of the command line process from */
+    private BufferedReader mResultReader;
+
+    /**
+     * output stream to provide new addresses to decode to the command line
+     * process
+     */
+    private BufferedOutputStream mAddressWriter;
+
+    private static final String DEFAULT_LIBRARY_SYMBOLS_FOLDER;
+    static {
+        String symbols = System.getenv(ANDROID_SYMBOLS_ENVVAR);
+        if (symbols == null) {
+            DEFAULT_LIBRARY_SYMBOLS_FOLDER = DdmUiPreferences.getSymbolDirectory();
+        } else {
+            DEFAULT_LIBRARY_SYMBOLS_FOLDER = symbols;
+        }
+    }
+
+    private static List<String> mLibrarySearchPaths = new ArrayList<String>();
+
+    /**
+     * Set the search path where libraries should be found.
+     * @param path search path to use, can be a colon separated list of paths if multiple folders
+     * should be searched
+     */
+    public static void setSearchPath(String path) {
+        mLibrarySearchPaths.clear();
+        mLibrarySearchPaths.addAll(Arrays.asList(path.split(":")));
+    }
+
+    /**
+     * Returns the instance of a Addr2Line process for the specified library.
+     * <br>The library should be in a format that makes<br>
+     * <code>$ANDROID_PRODUCT_OUT + "/symbols" + library</code> a valid file.
+     *
+     * @param library the library in which to look for addresses.
+     * @return a new Addr2Line object representing a started process, ready to
+     *         be queried for addresses. If any error happened when launching a
+     *         new process, <code>null</code> will be returned.
+     */
+    public static Addr2Line getProcess(final NativeLibraryMapInfo library) {
+        String libName = library.getLibraryName();
+
+        // synchronize around the hashmap object
+        if (libName != null) {
+            synchronized (sProcessCache) {
+                // look for an existing process
+                Addr2Line process = sProcessCache.get(libName);
+
+                // if we don't find one, we create it
+                if (process == null) {
+                    process = new Addr2Line(library);
+
+                    // then we start it
+                    boolean status = process.start();
+
+                    if (status) {
+                        // if starting the process worked, then we add it to the
+                        // list.
+                        sProcessCache.put(libName, process);
+                    } else {
+                        // otherwise we just drop the object, to return null
+                        process = null;
+                    }
+                }
+                // return the process
+                return process;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Construct the object with a library name. The library should be present
+     * in the search path as provided by ANDROID_SYMBOLS, ANDROID_OUT/symbols, or in the user
+     * provided search path.
+     *
+     * @param library the library in which to look for address.
+     */
+    private Addr2Line(final NativeLibraryMapInfo library) {
+        mLibrary = library;
+    }
+
+    /**
+     * Search for the library in the library search path and obtain the full path to where it
+     * is found.
+     * @return fully resolved path to the library if found in search path, null otherwise
+     */
+    private String getLibraryPath(String library) {
+        // first check the symbols folder
+        String path = DEFAULT_LIBRARY_SYMBOLS_FOLDER + library;
+        if (new File(path).exists()) {
+            return path;
+        }
+
+        for (String p : mLibrarySearchPaths) {
+            // try appending the full path on device
+            String fullPath = p + "/" + library;
+            if (new File(fullPath).exists()) {
+                return fullPath;
+            }
+
+            // try appending basename(library)
+            fullPath = p + "/" + new File(library).getName();
+            if (new File(fullPath).exists()) {
+                return fullPath;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Starts the command line process.
+     *
+     * @return true if the process was started, false if it failed to start, or
+     *         if there was any other errors.
+     */
+    private boolean start() {
+        // because this is only called from getProcess() we know we don't need
+        // to synchronize this code.
+
+        String addr2Line = System.getenv("ANDROID_ADDR2LINE");
+        if (addr2Line == null) {
+            addr2Line = DdmUiPreferences.getAddr2Line();
+        }
+
+        // build the command line
+        String[] command = new String[5];
+        command[0] = addr2Line;
+        command[1] = "-C";
+        command[2] = "-f";
+        command[3] = "-e";
+
+        String fullPath = getLibraryPath(mLibrary.getLibraryName());
+        if (fullPath == null) {
+            String msg = String.format(LIBRARY_NOT_FOUND_MESSAGE_FORMAT, mLibrary.getLibraryName());
+            Log.e("ddm-Addr2Line", msg);
+            return false;
+        }
+
+        command[4] = fullPath;
+
+        try {
+            // attempt to start the process
+            mProcess = Runtime.getRuntime().exec(command);
+
+            if (mProcess != null) {
+                // get the result reader
+                InputStreamReader is = new InputStreamReader(mProcess
+                        .getInputStream());
+                mResultReader = new BufferedReader(is);
+
+                // get the outstream to write the addresses
+                mAddressWriter = new BufferedOutputStream(mProcess
+                        .getOutputStream());
+
+                // check our streams are here
+                if (mResultReader == null || mAddressWriter == null) {
+                    // not here? stop the process and return false;
+                    mProcess.destroy();
+                    mProcess = null;
+                    return false;
+                }
+
+                // return a success
+                return true;
+            }
+
+        } catch (IOException e) {
+            // log the error
+            String msg = String.format(
+                    "Error while trying to start %1$s process for library %2$s",
+                    DdmUiPreferences.getAddr2Line(), mLibrary);
+            Log.e("ddm-Addr2Line", msg);
+
+            // drop the process just in case
+            if (mProcess != null) {
+                mProcess.destroy();
+                mProcess = null;
+            }
+        }
+
+        // we can be here either cause the allocation of mProcess failed, or we
+        // caught an exception
+        return false;
+    }
+
+    /**
+     * Stops the command line process.
+     */
+    public void stop() {
+        synchronized (sProcessCache) {
+            if (mProcess != null) {
+                // remove the process from the list
+                sProcessCache.remove(mLibrary);
+
+                // then stops the process
+                mProcess.destroy();
+
+                // set the reference to null.
+                // this allows to make sure another thread calling getAddress()
+                // will not query a stopped thread
+                mProcess = null;
+            }
+        }
+    }
+
+    /**
+     * Stops all current running processes.
+     */
+    public static void stopAll() {
+        // because of concurrent access (and our use of HashMap.values()), we
+        // can't rely on the synchronized inside stop(). We need to put one
+        // around the whole loop.
+        synchronized (sProcessCache) {
+            // just a basic loop on all the values in the hashmap and call to
+            // stop();
+            Collection<Addr2Line> col = sProcessCache.values();
+            for (Addr2Line a2l : col) {
+                a2l.stop();
+            }
+        }
+    }
+
+    /**
+     * Looks up an address and returns method name, source file name, and line
+     * number.
+     *
+     * @param addr the address to look up
+     * @return a BacktraceInfo object containing the method/filename/linenumber
+     *         or null if the process we stopped before the query could be
+     *         processed, or if an IO exception happened.
+     */
+    public NativeStackCallInfo getAddress(long addr) {
+        long offset = addr - mLibrary.getStartAddress();
+
+        // even though we don't access the hashmap object, we need to
+        // synchronized on it to prevent
+        // another thread from stopping the process we're going to query.
+        synchronized (sProcessCache) {
+            // check the process is still alive/allocated
+            if (mProcess != null) {
+                // prepare to the write the address to the output buffer.
+
+                // first, conversion to a string containing the hex value.
+                String tmp = Long.toString(offset, 16);
+
+                try {
+                    // write the address to the buffer
+                    mAddressWriter.write(tmp.getBytes());
+
+                    // add CR-LF
+                    mAddressWriter.write(sCrLf);
+
+                    // flush it all.
+                    mAddressWriter.flush();
+
+                    // read the result. We need to read 2 lines
+                    String method = mResultReader.readLine();
+                    String source = mResultReader.readLine();
+
+                    // make the backtrace object and return it
+                    if (method != null && source != null) {
+                        return new NativeStackCallInfo(addr, mLibrary.getLibraryName(), method, source);
+                    }
+                } catch (IOException e) {
+                    // log the error
+                    Log.e("ddms",
+                            "Error while trying to get information for addr: "
+                                    + tmp + " in library: " + mLibrary);
+                    // we'll return null later
+                }
+            }
+        }
+        return null;
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/AllocationPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/AllocationPanel.java
new file mode 100644
index 0000000..a48f73d
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/AllocationPanel.java
@@ -0,0 +1,651 @@
+/*
+ * Copyright (C) 2008 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.ddmuilib;
+
+import com.android.ddmlib.AllocationInfo;
+import com.android.ddmlib.AllocationInfo.AllocationSorter;
+import com.android.ddmlib.AllocationInfo.SortMode;
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData.AllocationTrackingStatus;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Sash;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * Base class for our information panels.
+ */
+public class AllocationPanel extends TablePanel {
+
+    private final static String PREFS_ALLOC_COL_NUMBER = "allocPanel.Col00"; //$NON-NLS-1$
+    private final static String PREFS_ALLOC_COL_SIZE = "allocPanel.Col0"; //$NON-NLS-1$
+    private final static String PREFS_ALLOC_COL_CLASS = "allocPanel.Col1"; //$NON-NLS-1$
+    private final static String PREFS_ALLOC_COL_THREAD = "allocPanel.Col2"; //$NON-NLS-1$
+    private final static String PREFS_ALLOC_COL_TRACE_CLASS = "allocPanel.Col3"; //$NON-NLS-1$
+    private final static String PREFS_ALLOC_COL_TRACE_METHOD = "allocPanel.Col4"; //$NON-NLS-1$
+
+    private final static String PREFS_ALLOC_SASH = "allocPanel.sash"; //$NON-NLS-1$
+
+    private static final String PREFS_STACK_COLUMN = "allocPanel.stack.col0"; //$NON-NLS-1$
+
+    private Composite mAllocationBase;
+    private Table mAllocationTable;
+    private TableViewer mAllocationViewer;
+
+    private StackTracePanel mStackTracePanel;
+    private Table mStackTraceTable;
+    private Button mEnableButton;
+    private Button mRequestButton;
+    private Button mTraceFilterCheck;
+
+    private final AllocationSorter mSorter = new AllocationSorter();
+    private TableColumn mSortColumn;
+    private Image mSortUpImg;
+    private Image mSortDownImg;
+    private String mFilterText = null;
+
+    /**
+     * Content Provider to display the allocations of a client.
+     * Expected input is a {@link Client} object, elements used in the table are of type
+     * {@link AllocationInfo}.
+     */
+    private class AllocationContentProvider implements IStructuredContentProvider {
+        @Override
+        public Object[] getElements(Object inputElement) {
+            if (inputElement instanceof Client) {
+                AllocationInfo[] allocs = ((Client)inputElement).getClientData().getAllocations();
+                if (allocs != null) {
+                    if (mFilterText != null && mFilterText.length() > 0) {
+                        allocs = getFilteredAllocations(allocs, mFilterText);
+                    }
+                    Arrays.sort(allocs, mSorter);
+                    return allocs;
+                }
+            }
+
+            return new Object[0];
+        }
+
+        @Override
+        public void dispose() {
+            // pass
+        }
+
+        @Override
+        public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+            // pass
+        }
+    }
+
+    /**
+     * A Label Provider to use with {@link AllocationContentProvider}. It expects the elements to be
+     * of type {@link AllocationInfo}.
+     */
+    private static class AllocationLabelProvider implements ITableLabelProvider {
+
+        @Override
+        public Image getColumnImage(Object element, int columnIndex) {
+            return null;
+        }
+
+        @Override
+        public String getColumnText(Object element, int columnIndex) {
+            if (element instanceof AllocationInfo) {
+                AllocationInfo alloc = (AllocationInfo)element;
+                switch (columnIndex) {
+                    case 0:
+                        return Integer.toString(alloc.getAllocNumber());
+                    case 1:
+                        return Integer.toString(alloc.getSize());
+                    case 2:
+                        return alloc.getAllocatedClass();
+                    case 3:
+                        return Short.toString(alloc.getThreadId());
+                    case 4:
+                        return alloc.getFirstTraceClassName();
+                    case 5:
+                        return alloc.getFirstTraceMethodName();
+                }
+            }
+
+            return null;
+        }
+
+        @Override
+        public void addListener(ILabelProviderListener listener) {
+            // pass
+        }
+
+        @Override
+        public void dispose() {
+            // pass
+        }
+
+        @Override
+        public boolean isLabelProperty(Object element, String property) {
+            // pass
+            return false;
+        }
+
+        @Override
+        public void removeListener(ILabelProviderListener listener) {
+            // pass
+        }
+    }
+
+    /**
+     * Create our control(s).
+     */
+    @Override
+    protected Control createControl(Composite parent) {
+        final IPreferenceStore store = DdmUiPreferences.getStore();
+
+        Display display = parent.getDisplay();
+
+        // get some images
+        mSortUpImg = ImageLoader.getDdmUiLibLoader().loadImage("sort_up.png", display);
+        mSortDownImg = ImageLoader.getDdmUiLibLoader().loadImage("sort_down.png", display);
+
+        // base composite for selected client with enabled thread update.
+        mAllocationBase = new Composite(parent, SWT.NONE);
+        mAllocationBase.setLayout(new FormLayout());
+
+        // table above the sash
+        Composite topParent = new Composite(mAllocationBase, SWT.NONE);
+        topParent.setLayout(new GridLayout(6, false));
+
+        mEnableButton = new Button(topParent, SWT.PUSH);
+        mEnableButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                Client current = getCurrentClient();
+                AllocationTrackingStatus status = current.getClientData().getAllocationStatus();
+                if (status == AllocationTrackingStatus.ON) {
+                    current.enableAllocationTracker(false);
+                } else {
+                    current.enableAllocationTracker(true);
+                }
+                current.requestAllocationStatus();
+            }
+        });
+
+        mRequestButton = new Button(topParent, SWT.PUSH);
+        mRequestButton.setText("Get Allocations");
+        mRequestButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                getCurrentClient().requestAllocationDetails();
+            }
+        });
+
+        setUpButtons(false /* enabled */, AllocationTrackingStatus.OFF);
+
+        GridData gridData;
+
+        Composite spacer = new Composite(topParent, SWT.NONE);
+        spacer.setLayoutData(gridData = new GridData(GridData.FILL_HORIZONTAL));
+
+        new Label(topParent, SWT.NONE).setText("Filter:");
+
+        final Text filterText = new Text(topParent, SWT.BORDER);
+        filterText.setLayoutData(gridData = new GridData(GridData.FILL_HORIZONTAL));
+        gridData.widthHint = 200;
+
+        filterText.addModifyListener(new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent arg0) {
+                mFilterText  = filterText.getText().trim();
+                mAllocationViewer.refresh();
+            }
+        });
+
+        mTraceFilterCheck = new Button(topParent, SWT.CHECK);
+        mTraceFilterCheck.setText("Inc. trace");
+        mTraceFilterCheck.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                mAllocationViewer.refresh();
+            }
+        });
+
+        mAllocationTable = new Table(topParent, SWT.MULTI | SWT.FULL_SELECTION);
+        mAllocationTable.setLayoutData(gridData = new GridData(GridData.FILL_BOTH));
+        gridData.horizontalSpan = 6;
+        mAllocationTable.setHeaderVisible(true);
+        mAllocationTable.setLinesVisible(true);
+
+        final TableColumn numberCol = TableHelper.createTableColumn(
+                mAllocationTable,
+                "Alloc Order",
+                SWT.RIGHT,
+                "Alloc Order", //$NON-NLS-1$
+                PREFS_ALLOC_COL_NUMBER, store);
+        numberCol.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                setSortColumn(numberCol, SortMode.NUMBER);
+            }
+        });
+
+        final TableColumn sizeCol = TableHelper.createTableColumn(
+                mAllocationTable,
+                "Allocation Size",
+                SWT.RIGHT,
+                "888", //$NON-NLS-1$
+                PREFS_ALLOC_COL_SIZE, store);
+        sizeCol.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                setSortColumn(sizeCol, SortMode.SIZE);
+            }
+        });
+
+        final TableColumn classCol = TableHelper.createTableColumn(
+                mAllocationTable,
+                "Allocated Class",
+                SWT.LEFT,
+                "Allocated Class", //$NON-NLS-1$
+                PREFS_ALLOC_COL_CLASS, store);
+        classCol.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                setSortColumn(classCol, SortMode.CLASS);
+            }
+        });
+
+        final TableColumn threadCol = TableHelper.createTableColumn(
+                mAllocationTable,
+                "Thread Id",
+                SWT.LEFT,
+                "999", //$NON-NLS-1$
+                PREFS_ALLOC_COL_THREAD, store);
+        threadCol.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                setSortColumn(threadCol, SortMode.THREAD);
+            }
+        });
+
+        final TableColumn inClassCol = TableHelper.createTableColumn(
+                mAllocationTable,
+                "Allocated in",
+                SWT.LEFT,
+                "utime", //$NON-NLS-1$
+                PREFS_ALLOC_COL_TRACE_CLASS, store);
+        inClassCol.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                setSortColumn(inClassCol, SortMode.IN_CLASS);
+            }
+        });
+
+        final TableColumn inMethodCol = TableHelper.createTableColumn(
+                mAllocationTable,
+                "Allocated in",
+                SWT.LEFT,
+                "utime", //$NON-NLS-1$
+                PREFS_ALLOC_COL_TRACE_METHOD, store);
+        inMethodCol.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                setSortColumn(inMethodCol, SortMode.IN_METHOD);
+            }
+        });
+
+        // init the default sort colum
+        switch (mSorter.getSortMode()) {
+            case SIZE:
+                mSortColumn = sizeCol;
+                break;
+            case CLASS:
+                mSortColumn = classCol;
+                break;
+            case THREAD:
+                mSortColumn = threadCol;
+                break;
+            case IN_CLASS:
+                mSortColumn = inClassCol;
+                break;
+            case IN_METHOD:
+                mSortColumn = inMethodCol;
+                break;
+        }
+
+        mSortColumn.setImage(mSorter.isDescending() ? mSortDownImg : mSortUpImg);
+
+        mAllocationViewer = new TableViewer(mAllocationTable);
+        mAllocationViewer.setContentProvider(new AllocationContentProvider());
+        mAllocationViewer.setLabelProvider(new AllocationLabelProvider());
+
+        mAllocationViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+            @Override
+            public void selectionChanged(SelectionChangedEvent event) {
+                AllocationInfo selectedAlloc = getAllocationSelection(event.getSelection());
+                updateAllocationStackTrace(selectedAlloc);
+            }
+        });
+
+        // the separating sash
+        final Sash sash = new Sash(mAllocationBase, SWT.HORIZONTAL);
+        Color darkGray = parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY);
+        sash.setBackground(darkGray);
+
+        // the UI below the sash
+        mStackTracePanel = new StackTracePanel();
+        mStackTraceTable = mStackTracePanel.createPanel(mAllocationBase, PREFS_STACK_COLUMN, store);
+
+        // now setup the sash.
+        // form layout data
+        FormData data = new FormData();
+        data.top = new FormAttachment(0, 0);
+        data.bottom = new FormAttachment(sash, 0);
+        data.left = new FormAttachment(0, 0);
+        data.right = new FormAttachment(100, 0);
+        topParent.setLayoutData(data);
+
+        final FormData sashData = new FormData();
+        if (store != null && store.contains(PREFS_ALLOC_SASH)) {
+            sashData.top = new FormAttachment(0, store.getInt(PREFS_ALLOC_SASH));
+        } else {
+            sashData.top = new FormAttachment(50,0); // 50% across
+        }
+        sashData.left = new FormAttachment(0, 0);
+        sashData.right = new FormAttachment(100, 0);
+        sash.setLayoutData(sashData);
+
+        data = new FormData();
+        data.top = new FormAttachment(sash, 0);
+        data.bottom = new FormAttachment(100, 0);
+        data.left = new FormAttachment(0, 0);
+        data.right = new FormAttachment(100, 0);
+        mStackTraceTable.setLayoutData(data);
+
+        // allow resizes, but cap at minPanelWidth
+        sash.addListener(SWT.Selection, new Listener() {
+            @Override
+            public void handleEvent(Event e) {
+                Rectangle sashRect = sash.getBounds();
+                Rectangle panelRect = mAllocationBase.getClientArea();
+                int bottom = panelRect.height - sashRect.height - 100;
+                e.y = Math.max(Math.min(e.y, bottom), 100);
+                if (e.y != sashRect.y) {
+                    sashData.top = new FormAttachment(0, e.y);
+                    store.setValue(PREFS_ALLOC_SASH, e.y);
+                    mAllocationBase.layout();
+                }
+            }
+        });
+
+        return mAllocationBase;
+    }
+
+    @Override
+    public void dispose() {
+        mSortUpImg.dispose();
+        mSortDownImg.dispose();
+        super.dispose();
+    }
+
+    /**
+     * Sets the focus to the proper control inside the panel.
+     */
+    @Override
+    public void setFocus() {
+        mAllocationTable.setFocus();
+    }
+
+    /**
+     * Sent when an existing client information changed.
+     * <p/>
+     * This is sent from a non UI thread.
+     * @param client the updated client.
+     * @param changeMask the bit mask describing the changed properties. It can contain
+     * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME}
+     * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE},
+     * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
+     * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
+     *
+     * @see IClientChangeListener#clientChanged(Client, int)
+     */
+    @Override
+    public void clientChanged(final Client client, int changeMask) {
+        if (client == getCurrentClient()) {
+            if ((changeMask & Client.CHANGE_HEAP_ALLOCATIONS) != 0) {
+                try {
+                    mAllocationTable.getDisplay().asyncExec(new Runnable() {
+                        @Override
+                        public void run() {
+                            mAllocationViewer.refresh();
+                            updateAllocationStackCall();
+                        }
+                    });
+                } catch (SWTException e) {
+                    // widget is disposed, we do nothing
+                }
+            } else if ((changeMask & Client.CHANGE_HEAP_ALLOCATION_STATUS) != 0) {
+                try {
+                    mAllocationTable.getDisplay().asyncExec(new Runnable() {
+                        @Override
+                        public void run() {
+                            setUpButtons(true, client.getClientData().getAllocationStatus());
+                        }
+                    });
+                } catch (SWTException e) {
+                    // widget is disposed, we do nothing
+                }
+            }
+        }
+    }
+
+    /**
+     * Sent when a new device is selected. The new device can be accessed
+     * with {@link #getCurrentDevice()}.
+     */
+    @Override
+    public void deviceSelected() {
+        // pass
+    }
+
+    /**
+     * Sent when a new client is selected. The new client can be accessed
+     * with {@link #getCurrentClient()}.
+     */
+    @Override
+    public void clientSelected() {
+        if (mAllocationTable.isDisposed()) {
+            return;
+        }
+
+        Client client = getCurrentClient();
+
+        mStackTracePanel.setCurrentClient(client);
+        mStackTracePanel.setViewerInput(null); // always empty on client selection change.
+
+        if (client != null) {
+            setUpButtons(true /* enabled */, client.getClientData().getAllocationStatus());
+        } else {
+            setUpButtons(false /* enabled */, AllocationTrackingStatus.OFF);
+        }
+
+        mAllocationViewer.setInput(client);
+    }
+
+    /**
+     * Updates the stack call of the currently selected thread.
+     * <p/>
+     * This <b>must</b> be called from the UI thread.
+     */
+    private void updateAllocationStackCall() {
+        Client client = getCurrentClient();
+        if (client != null) {
+            // get the current selection in the ThreadTable
+            AllocationInfo selectedAlloc = getAllocationSelection(null);
+
+            if (selectedAlloc != null) {
+                updateAllocationStackTrace(selectedAlloc);
+            } else {
+                updateAllocationStackTrace(null);
+            }
+        }
+    }
+
+    /**
+     * updates the stackcall of the specified allocation. If <code>null</code> the UI is emptied
+     * of current data.
+     * @param thread
+     */
+    private void updateAllocationStackTrace(AllocationInfo alloc) {
+        mStackTracePanel.setViewerInput(alloc);
+    }
+
+    @Override
+    protected void setTableFocusListener() {
+        addTableToFocusListener(mAllocationTable);
+        addTableToFocusListener(mStackTraceTable);
+    }
+
+    /**
+     * Returns the current allocation selection or <code>null</code> if none is found.
+     * If a {@link ISelection} object is specified, the first {@link AllocationInfo} from this
+     * selection is returned, otherwise, the <code>ISelection</code> returned by
+     * {@link TableViewer#getSelection()} is used.
+     * @param selection the {@link ISelection} to use, or <code>null</code>
+     */
+    private AllocationInfo getAllocationSelection(ISelection selection) {
+        if (selection == null) {
+            selection = mAllocationViewer.getSelection();
+        }
+
+        if (selection instanceof IStructuredSelection) {
+            IStructuredSelection structuredSelection = (IStructuredSelection)selection;
+            Object object = structuredSelection.getFirstElement();
+            if (object instanceof AllocationInfo) {
+                return (AllocationInfo)object;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     *
+     * @param enabled
+     * @param trackingStatus
+     */
+    private void setUpButtons(boolean enabled, AllocationTrackingStatus trackingStatus) {
+        if (enabled) {
+            switch (trackingStatus) {
+                case UNKNOWN:
+                    mEnableButton.setText("?");
+                    mEnableButton.setEnabled(false);
+                    mRequestButton.setEnabled(false);
+                    break;
+                case OFF:
+                    mEnableButton.setText("Start Tracking");
+                    mEnableButton.setEnabled(true);
+                    mRequestButton.setEnabled(false);
+                    break;
+                case ON:
+                    mEnableButton.setText("Stop Tracking");
+                    mEnableButton.setEnabled(true);
+                    mRequestButton.setEnabled(true);
+                    break;
+            }
+        } else {
+            mEnableButton.setEnabled(false);
+            mRequestButton.setEnabled(false);
+            mEnableButton.setText("Start Tracking");
+        }
+    }
+
+    private void setSortColumn(final TableColumn column, SortMode sortMode) {
+        // set the new sort mode
+        mSorter.setSortMode(sortMode);
+
+        mAllocationTable.setRedraw(false);
+
+        // remove image from previous sort colum
+        if (mSortColumn != column) {
+            mSortColumn.setImage(null);
+        }
+
+        mSortColumn = column;
+        if (mSorter.isDescending()) {
+            mSortColumn.setImage(mSortDownImg);
+        } else {
+            mSortColumn.setImage(mSortUpImg);
+        }
+
+        mAllocationTable.setRedraw(true);
+        mAllocationViewer.refresh();
+    }
+
+    private AllocationInfo[] getFilteredAllocations(AllocationInfo[] allocations,
+            String filterText) {
+        ArrayList<AllocationInfo> results = new ArrayList<AllocationInfo>();
+        // Using default locale here such that the locale-specific c
+        Locale locale = Locale.getDefault();
+        filterText = filterText.toLowerCase(locale);
+        boolean fullTrace = mTraceFilterCheck.getSelection();
+
+        for (AllocationInfo info : allocations) {
+            if (info.filter(filterText, fullTrace, locale)) {
+                results.add(info);
+            }
+        }
+
+        return results.toArray(new AllocationInfo[results.size()]);
+    }
+
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/BackgroundThread.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/BackgroundThread.java
new file mode 100644
index 0000000..0ed4c95
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/BackgroundThread.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib;
+
+import com.android.ddmlib.Log;
+
+/**
+ * base background thread class. The class provides a synchronous quit method
+ * which sets a quitting flag to true. Inheriting classes should regularly test
+ * this flag with <code>isQuitting()</code> and should finish if the flag is
+ * true.
+ */
+public abstract class BackgroundThread extends Thread {
+    private boolean mQuit = false;
+
+    /**
+     * Tell the thread to exit. This is usually called from the UI thread. The
+     * call is synchronous and will only return once the thread has terminated
+     * itself.
+     */
+    public final void quit() {
+        mQuit = true;
+        Log.d("ddms", "Waiting for BackgroundThread to quit");
+        try {
+            this.join();
+        } catch (InterruptedException ie) {
+            ie.printStackTrace();
+        }
+    }
+
+    /** returns if the thread was asked to quit. */
+    protected final boolean isQuitting() {
+        return mQuit;
+    }
+
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/BaseHeapPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/BaseHeapPanel.java
new file mode 100644
index 0000000..3e66ea5
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/BaseHeapPanel.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib;
+
+import com.android.ddmlib.HeapSegment;
+import com.android.ddmlib.ClientData.HeapData;
+import com.android.ddmlib.HeapSegment.HeapSegmentElement;
+
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.PaletteData;
+
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.TreeMap;
+
+
+/**
+ * Base Panel for heap panels.
+ */
+public abstract class BaseHeapPanel extends TablePanel {
+
+    /** store the processed heap segment, so that we don't recompute Image for nothing */
+    protected byte[] mProcessedHeapData;
+    private Map<Integer, ArrayList<HeapSegmentElement>> mHeapMap;
+
+    /**
+     * Serialize the heap data into an array. The resulting array is available through
+     * <code>getSerializedData()</code>.
+     * @param heapData The heap data to serialize
+     * @return true if the data changed.
+     */
+    protected boolean serializeHeapData(HeapData heapData) {
+        Collection<HeapSegment> heapSegments;
+
+        // Atomically get and clear the heap data.
+        synchronized (heapData) {
+            // get the segments
+            heapSegments = heapData.getHeapSegments();
+            
+            
+            if (heapSegments != null) {
+                // if they are not null, we never processed them.
+                // Before we process then, we drop them from the HeapData
+                heapData.clearHeapData();
+
+                // process them into a linear byte[]
+                doSerializeHeapData(heapSegments);
+                heapData.setProcessedHeapData(mProcessedHeapData);
+                heapData.setProcessedHeapMap(mHeapMap);
+                
+            } else {
+                // the heap segments are null. Let see if the heapData contains a 
+                // list that is already processed.
+                
+                byte[] pixData = heapData.getProcessedHeapData();
+                
+                // and compare it to the one we currently have in the panel.
+                if (pixData == mProcessedHeapData) {
+                    // looks like its the same
+                    return false;
+                } else {
+                    mProcessedHeapData = pixData;
+                }
+                
+                Map<Integer, ArrayList<HeapSegmentElement>> heapMap =
+                    heapData.getProcessedHeapMap();
+                mHeapMap = heapMap;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns the serialized heap data
+     */
+    protected byte[] getSerializedData() {
+        return mProcessedHeapData;
+    }
+
+    /**
+     * Processes and serialize the heapData.
+     * <p/>
+     * The resulting serialized array is {@link #mProcessedHeapData}.
+     * <p/>
+     * the resulting map is {@link #mHeapMap}.
+     * @param heapData the collection of {@link HeapSegment} that forms the heap data.
+     */
+    private void doSerializeHeapData(Collection<HeapSegment> heapData) {
+        mHeapMap = new TreeMap<Integer, ArrayList<HeapSegmentElement>>();
+
+        Iterator<HeapSegment> iterator;
+        ByteArrayOutputStream out;
+
+        out = new ByteArrayOutputStream(4 * 1024);
+
+        iterator = heapData.iterator();
+        while (iterator.hasNext()) {
+            HeapSegment hs = iterator.next();
+
+            HeapSegmentElement e = null;
+            while (true) {
+                int v;
+
+                e = hs.getNextElement(null);
+                if (e == null) {
+                    break;
+                }
+                
+                if (e.getSolidity() == HeapSegmentElement.SOLIDITY_FREE) {
+                    v = 1;
+                } else {
+                    v = e.getKind() + 2;
+                }
+                
+                // put the element in the map
+                ArrayList<HeapSegmentElement> elementList = mHeapMap.get(v);
+                if (elementList == null) {
+                    elementList = new ArrayList<HeapSegmentElement>();
+                    mHeapMap.put(v, elementList);
+                }
+                elementList.add(e);
+
+
+                int len = e.getLength() / 8;
+                while (len > 0) {
+                    out.write(v);
+                    --len;
+                }
+            }
+        }
+        mProcessedHeapData = out.toByteArray();
+        
+        // sort the segment element in the heap info.
+        Collection<ArrayList<HeapSegmentElement>> elementLists = mHeapMap.values();
+        for (ArrayList<HeapSegmentElement> elementList : elementLists) {
+            Collections.sort(elementList);
+        }
+    }
+    
+    /**
+     * Creates a linear image of the heap data.
+     * @param pixData
+     * @param h
+     * @param palette
+     * @return
+     */
+    protected ImageData createLinearHeapImage(byte[] pixData, int h, PaletteData palette) {
+        int w = pixData.length / h;
+        if (pixData.length % h != 0) {
+            w++;
+        }
+
+        // Create the heap image.
+        ImageData id = new ImageData(w, h, 8, palette);
+
+        int x = 0;
+        int y = 0;
+        for (byte b : pixData) {
+            if (b >= 0) {
+                id.setPixel(x, y, b);
+            }
+
+            y++;
+            if (y >= h) {
+                y = 0;
+                x++;
+            }
+        }
+
+        return id;
+    }
+
+
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ClientDisplayPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ClientDisplayPanel.java
new file mode 100644
index 0000000..a711933
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ClientDisplayPanel.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib;
+
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+
+public abstract class ClientDisplayPanel extends SelectionDependentPanel
+        implements IClientChangeListener {
+
+    @Override
+    protected void postCreation() {
+        AndroidDebugBridge.addClientChangeListener(this);
+    }
+
+    public void dispose() {
+        AndroidDebugBridge.removeClientChangeListener(this);
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/DdmUiPreferences.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/DdmUiPreferences.java
new file mode 100644
index 0000000..db3642b
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/DdmUiPreferences.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+
+/**
+ * Preference entry point for ddmuilib. Allows the lib to access a preference
+ * store (org.eclipse.jface.preference.IPreferenceStore) defined by the
+ * application that includes the lib.
+ */
+public final class DdmUiPreferences {
+
+    public static final int DEFAULT_THREAD_REFRESH_INTERVAL = 4;  // seconds
+
+    private static int sThreadRefreshInterval = DEFAULT_THREAD_REFRESH_INTERVAL;
+
+    private static IPreferenceStore mStore;
+
+    private static String sSymbolLocation =""; //$NON-NLS-1$
+    private static String sAddr2LineLocation =""; //$NON-NLS-1$
+    private static String sTraceviewLocation =""; //$NON-NLS-1$
+
+    public static void setStore(IPreferenceStore store) {
+        mStore = store;
+    }
+
+    public static IPreferenceStore getStore() {
+        return mStore;
+    }
+
+    public static int getThreadRefreshInterval() {
+        return sThreadRefreshInterval;
+    }
+
+    public static void setThreadRefreshInterval(int port) {
+        sThreadRefreshInterval = port;
+    }
+
+    public static String getSymbolDirectory() {
+        return sSymbolLocation;
+    }
+
+    public static void setSymbolsLocation(String location) {
+        sSymbolLocation = location;
+    }
+
+    public static String getAddr2Line() {
+        return sAddr2LineLocation;
+    }
+
+    public static void setAddr2LineLocation(String location) {
+        sAddr2LineLocation = location;
+    }
+
+    public static String getTraceview() {
+        return sTraceviewLocation;
+    }
+
+    public static void setTraceviewLocation(String location) {
+        sTraceviewLocation = location;
+    }
+
+
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/DevicePanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/DevicePanel.java
new file mode 100644
index 0000000..a24b8a0
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/DevicePanel.java
@@ -0,0 +1,784 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib;
+
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener;
+import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData;
+import com.android.ddmlib.ClientData.DebuggerStatus;
+import com.android.ddmlib.DdmPreferences;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.IDevice.DeviceState;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.TreePath;
+import org.eclipse.jface.viewers.TreeSelection;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeColumn;
+import org.eclipse.swt.widgets.TreeItem;
+
+import java.util.ArrayList;
+import java.util.Locale;
+
+/**
+ * A display of both the devices and their clients.
+ */
+public final class DevicePanel extends Panel implements IDebugBridgeChangeListener,
+        IDeviceChangeListener, IClientChangeListener {
+
+    private final static String PREFS_COL_NAME_SERIAL = "devicePanel.Col0"; //$NON-NLS-1$
+    private final static String PREFS_COL_PID_STATE = "devicePanel.Col1"; //$NON-NLS-1$
+    private final static String PREFS_COL_PORT_BUILD = "devicePanel.Col4"; //$NON-NLS-1$
+
+    private final static int DEVICE_COL_SERIAL = 0;
+    private final static int DEVICE_COL_STATE = 1;
+    // col 2, 3 not used.
+    private final static int DEVICE_COL_BUILD = 4;
+
+    private final static int CLIENT_COL_NAME = 0;
+    private final static int CLIENT_COL_PID = 1;
+    private final static int CLIENT_COL_THREAD = 2;
+    private final static int CLIENT_COL_HEAP = 3;
+    private final static int CLIENT_COL_PORT = 4;
+
+    public final static int ICON_WIDTH = 16;
+    public final static String ICON_THREAD = "thread.png"; //$NON-NLS-1$
+    public final static String ICON_HEAP = "heap.png"; //$NON-NLS-1$
+    public final static String ICON_HALT = "halt.png"; //$NON-NLS-1$
+    public final static String ICON_GC = "gc.png"; //$NON-NLS-1$
+    public final static String ICON_HPROF = "hprof.png"; //$NON-NLS-1$
+    public final static String ICON_TRACING_START = "tracing_start.png"; //$NON-NLS-1$
+    public final static String ICON_TRACING_STOP = "tracing_stop.png"; //$NON-NLS-1$
+
+    private IDevice mCurrentDevice;
+    private Client mCurrentClient;
+
+    private Tree mTree;
+    private TreeViewer mTreeViewer;
+
+    private Image mDeviceImage;
+    private Image mEmulatorImage;
+
+    private Image mThreadImage;
+    private Image mHeapImage;
+    private Image mWaitingImage;
+    private Image mDebuggerImage;
+    private Image mDebugErrorImage;
+
+    private final ArrayList<IUiSelectionListener> mListeners = new ArrayList<IUiSelectionListener>();
+
+    private final ArrayList<IDevice> mDevicesToExpand = new ArrayList<IDevice>();
+
+    private boolean mAdvancedPortSupport;
+
+    /**
+     * A Content provider for the {@link TreeViewer}.
+     * <p/>
+     * The input is a {@link AndroidDebugBridge}. First level elements are {@link IDevice} objects,
+     * and second level elements are {@link Client} object.
+     */
+    private class ContentProvider implements ITreeContentProvider {
+        @Override
+        public Object[] getChildren(Object parentElement) {
+            if (parentElement instanceof IDevice) {
+                return ((IDevice)parentElement).getClients();
+            }
+            return new Object[0];
+        }
+
+        @Override
+        public Object getParent(Object element) {
+            if (element instanceof Client) {
+                return ((Client)element).getDevice();
+            }
+            return null;
+        }
+
+        @Override
+        public boolean hasChildren(Object element) {
+            if (element instanceof IDevice) {
+                return ((IDevice)element).hasClients();
+            }
+
+            // Clients never have children.
+            return false;
+        }
+
+        @Override
+        public Object[] getElements(Object inputElement) {
+            if (inputElement instanceof AndroidDebugBridge) {
+                return ((AndroidDebugBridge)inputElement).getDevices();
+            }
+            return new Object[0];
+        }
+
+        @Override
+        public void dispose() {
+            // pass
+        }
+
+        @Override
+        public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+            // pass
+        }
+    }
+
+    /**
+     * A Label Provider for the {@link TreeViewer} in {@link DevicePanel}. It provides
+     * labels and images for {@link IDevice} and {@link Client} objects.
+     */
+    private class LabelProvider implements ITableLabelProvider {
+        @Override
+        public Image getColumnImage(Object element, int columnIndex) {
+            if (columnIndex == DEVICE_COL_SERIAL && element instanceof IDevice) {
+                IDevice device = (IDevice)element;
+                if (device.isEmulator()) {
+                    return mEmulatorImage;
+                }
+
+                return mDeviceImage;
+            } else if (element instanceof Client) {
+                Client client = (Client)element;
+                ClientData cd = client.getClientData();
+
+                switch (columnIndex) {
+                    case CLIENT_COL_NAME:
+                        switch (cd.getDebuggerConnectionStatus()) {
+                            case DEFAULT:
+                                return null;
+                            case WAITING:
+                                return mWaitingImage;
+                            case ATTACHED:
+                                return mDebuggerImage;
+                            case ERROR:
+                                return mDebugErrorImage;
+                        }
+                        return null;
+                    case CLIENT_COL_THREAD:
+                        if (client.isThreadUpdateEnabled()) {
+                            return mThreadImage;
+                        }
+                        return null;
+                    case CLIENT_COL_HEAP:
+                        if (client.isHeapUpdateEnabled()) {
+                            return mHeapImage;
+                        }
+                        return null;
+                }
+            }
+            return null;
+        }
+
+        @Override
+        public String getColumnText(Object element, int columnIndex) {
+            if (element instanceof IDevice) {
+                IDevice device = (IDevice)element;
+                switch (columnIndex) {
+                    case DEVICE_COL_SERIAL:
+                        return device.getName();
+                    case DEVICE_COL_STATE:
+                        return getStateString(device);
+                    case DEVICE_COL_BUILD: {
+                        String version = device.getProperty(IDevice.PROP_BUILD_VERSION);
+                        if (version != null) {
+                            String debuggable = device.getProperty(IDevice.PROP_DEBUGGABLE);
+                            if (device.isEmulator()) {
+                                String avdName = device.getAvdName();
+                                if (avdName == null) {
+                                    avdName = "?"; // the device is probably not online yet, so
+                                                   // we don't know its AVD name just yet.
+                                }
+                                if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$
+                                    return String.format("%1$s [%2$s, debug]", avdName,
+                                            version);
+                                } else {
+                                    return String.format("%1$s [%2$s]", avdName, version); //$NON-NLS-1$
+                                }
+                            } else {
+                                if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$
+                                    return String.format("%1$s, debug", version);
+                                } else {
+                                    return String.format("%1$s", version); //$NON-NLS-1$
+                                }
+                            }
+                        } else {
+                            return "unknown";
+                        }
+                    }
+                }
+            } else if (element instanceof Client) {
+                Client client = (Client)element;
+                ClientData cd = client.getClientData();
+
+                switch (columnIndex) {
+                    case CLIENT_COL_NAME:
+                        String name = cd.getClientDescription();
+                        if (name != null) {
+                            if (cd.isValidUserId() && cd.getUserId() != 0) {
+                                return String.format(Locale.US, "%s (%d)", name, cd.getUserId());
+                            } else {
+                                return name;
+                            }
+                        }
+                        return "?";
+                    case CLIENT_COL_PID:
+                        return Integer.toString(cd.getPid());
+                    case CLIENT_COL_PORT:
+                        if (mAdvancedPortSupport) {
+                            int port = client.getDebuggerListenPort();
+                            String portString = "?";
+                            if (port != 0) {
+                                portString = Integer.toString(port);
+                            }
+                            if (client.isSelectedClient()) {
+                                return String.format("%1$s / %2$d", portString, //$NON-NLS-1$
+                                        DdmPreferences.getSelectedDebugPort());
+                            }
+
+                            return portString;
+                        }
+                }
+            }
+            return null;
+        }
+
+        @Override
+        public void addListener(ILabelProviderListener listener) {
+            // pass
+        }
+
+        @Override
+        public void dispose() {
+            // pass
+        }
+
+        @Override
+        public boolean isLabelProperty(Object element, String property) {
+            // pass
+            return false;
+        }
+
+        @Override
+        public void removeListener(ILabelProviderListener listener) {
+            // pass
+        }
+    }
+
+    /**
+     * Classes which implement this interface provide methods that deals
+     * with {@link IDevice} and {@link Client} selection changes coming from the ui.
+     */
+    public interface IUiSelectionListener {
+        /**
+         * Sent when a new {@link IDevice} and {@link Client} are selected.
+         * @param selectedDevice the selected device. If null, no devices are selected.
+         * @param selectedClient The selected client. If null, no clients are selected.
+         */
+        public void selectionChanged(IDevice selectedDevice, Client selectedClient);
+    }
+
+    /**
+     * Creates the {@link DevicePanel} object.
+     * @param loader
+     * @param advancedPortSupport if true the device panel will add support for selected client port
+     * and display the ports in the ui.
+     */
+    public DevicePanel(boolean advancedPortSupport) {
+        mAdvancedPortSupport = advancedPortSupport;
+    }
+
+    public void addSelectionListener(IUiSelectionListener listener) {
+        mListeners.add(listener);
+    }
+
+    public void removeSelectionListener(IUiSelectionListener listener) {
+        mListeners.remove(listener);
+    }
+
+    @Override
+    protected Control createControl(Composite parent) {
+        loadImages(parent.getDisplay());
+
+        parent.setLayout(new FillLayout());
+
+        // create the tree and its column
+        mTree = new Tree(parent, SWT.SINGLE | SWT.FULL_SELECTION);
+        mTree.setHeaderVisible(true);
+        mTree.setLinesVisible(true);
+
+        IPreferenceStore store = DdmUiPreferences.getStore();
+
+        TableHelper.createTreeColumn(mTree, "Name", SWT.LEFT,
+                "com.android.home", //$NON-NLS-1$
+                PREFS_COL_NAME_SERIAL, store);
+        TableHelper.createTreeColumn(mTree, "", SWT.LEFT, //$NON-NLS-1$
+                "Offline", //$NON-NLS-1$
+                PREFS_COL_PID_STATE, store);
+
+        TreeColumn col = new TreeColumn(mTree, SWT.NONE);
+        col.setWidth(ICON_WIDTH + 8);
+        col.setResizable(false);
+        col = new TreeColumn(mTree, SWT.NONE);
+        col.setWidth(ICON_WIDTH + 8);
+        col.setResizable(false);
+
+        TableHelper.createTreeColumn(mTree, "", SWT.LEFT, //$NON-NLS-1$
+                "9999-9999", //$NON-NLS-1$
+                PREFS_COL_PORT_BUILD, store);
+
+        // create the tree viewer
+        mTreeViewer = new TreeViewer(mTree);
+
+        // make the device auto expanded.
+        mTreeViewer.setAutoExpandLevel(TreeViewer.ALL_LEVELS);
+
+        // set up the content and label providers.
+        mTreeViewer.setContentProvider(new ContentProvider());
+        mTreeViewer.setLabelProvider(new LabelProvider());
+
+        mTree.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                notifyListeners();
+            }
+        });
+
+        return mTree;
+    }
+
+    /**
+     * Sets the focus to the proper control inside the panel.
+     */
+    @Override
+    public void setFocus() {
+        mTree.setFocus();
+    }
+
+    @Override
+    protected void postCreation() {
+        // ask for notification of changes in AndroidDebugBridge (a new one is created when
+        // adb is restarted from a different location), IDevice and Client objects.
+        AndroidDebugBridge.addDebugBridgeChangeListener(this);
+        AndroidDebugBridge.addDeviceChangeListener(this);
+        AndroidDebugBridge.addClientChangeListener(this);
+    }
+
+    public void dispose() {
+        AndroidDebugBridge.removeDebugBridgeChangeListener(this);
+        AndroidDebugBridge.removeDeviceChangeListener(this);
+        AndroidDebugBridge.removeClientChangeListener(this);
+    }
+
+    /**
+     * Returns the selected {@link Client}. May be null.
+     */
+    public Client getSelectedClient() {
+        return mCurrentClient;
+    }
+
+    /**
+     * Returns the selected {@link IDevice}. If a {@link Client} is selected, it returns the
+     * IDevice object containing the client.
+     */
+    public IDevice getSelectedDevice() {
+        return mCurrentDevice;
+    }
+
+    /**
+     * Kills the selected {@link Client} by sending its VM a halt command.
+     */
+    public void killSelectedClient() {
+        if (mCurrentClient != null) {
+            Client client = mCurrentClient;
+
+            // reset the selection to the device.
+            TreePath treePath = new TreePath(new Object[] { mCurrentDevice });
+            TreeSelection treeSelection = new TreeSelection(treePath);
+            mTreeViewer.setSelection(treeSelection);
+
+            client.kill();
+        }
+    }
+
+    /**
+     * Forces a GC on the selected {@link Client}.
+     */
+    public void forceGcOnSelectedClient() {
+        if (mCurrentClient != null) {
+            mCurrentClient.executeGarbageCollector();
+        }
+    }
+
+    public void dumpHprof() {
+        if (mCurrentClient != null) {
+            mCurrentClient.dumpHprof();
+        }
+    }
+
+    public void toggleMethodProfiling() {
+        if (mCurrentClient != null) {
+            mCurrentClient.toggleMethodProfiling();
+        }
+    }
+
+    public void setEnabledHeapOnSelectedClient(boolean enable) {
+        if (mCurrentClient != null) {
+            mCurrentClient.setHeapUpdateEnabled(enable);
+        }
+    }
+
+    public void setEnabledThreadOnSelectedClient(boolean enable) {
+        if (mCurrentClient != null) {
+            mCurrentClient.setThreadUpdateEnabled(enable);
+        }
+    }
+
+    /**
+     * Sent when a new {@link AndroidDebugBridge} is started.
+     * <p/>
+     * This is sent from a non UI thread.
+     * @param bridge the new {@link AndroidDebugBridge} object.
+     *
+     * @see IDebugBridgeChangeListener#serverChanged(AndroidDebugBridge)
+     */
+    @Override
+    public void bridgeChanged(final AndroidDebugBridge bridge) {
+        if (mTree.isDisposed() == false) {
+            exec(new Runnable() {
+                @Override
+                public void run() {
+                    if (mTree.isDisposed() == false) {
+                        // set up the data source.
+                        mTreeViewer.setInput(bridge);
+
+                        // notify the listener of a possible selection change.
+                        notifyListeners();
+                    } else {
+                        // tree is disposed, we need to do something.
+                        // lets remove ourselves from the listener.
+                        AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this);
+                        AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this);
+                        AndroidDebugBridge.removeClientChangeListener(DevicePanel.this);
+                    }
+                }
+            });
+        }
+
+        // all current devices are obsolete
+        synchronized (mDevicesToExpand) {
+            mDevicesToExpand.clear();
+        }
+    }
+
+    /**
+     * Sent when the a device is connected to the {@link AndroidDebugBridge}.
+     * <p/>
+     * This is sent from a non UI thread.
+     * @param device the new device.
+     *
+     * @see IDeviceChangeListener#deviceConnected(IDevice)
+     */
+    @Override
+    public void deviceConnected(IDevice device) {
+        exec(new Runnable() {
+            @Override
+            public void run() {
+                if (mTree.isDisposed() == false) {
+                    // refresh all
+                    mTreeViewer.refresh();
+
+                    // notify the listener of a possible selection change.
+                    notifyListeners();
+                } else {
+                    // tree is disposed, we need to do something.
+                    // lets remove ourselves from the listener.
+                    AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this);
+                    AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this);
+                    AndroidDebugBridge.removeClientChangeListener(DevicePanel.this);
+                }
+            }
+        });
+
+        // if it doesn't have clients yet, it'll need to be manually expanded when it gets them.
+        if (device.hasClients() == false) {
+            synchronized (mDevicesToExpand) {
+                mDevicesToExpand.add(device);
+            }
+        }
+    }
+
+    /**
+     * Sent when the a device is connected to the {@link AndroidDebugBridge}.
+     * <p/>
+     * This is sent from a non UI thread.
+     * @param device the new device.
+     *
+     * @see IDeviceChangeListener#deviceDisconnected(IDevice)
+     */
+    @Override
+    public void deviceDisconnected(IDevice device) {
+        deviceConnected(device);
+
+        // just in case, we remove it from the list of devices to expand.
+        synchronized (mDevicesToExpand) {
+            mDevicesToExpand.remove(device);
+        }
+    }
+
+    /**
+     * Sent when a device data changed, or when clients are started/terminated on the device.
+     * <p/>
+     * This is sent from a non UI thread.
+     * @param device the device that was updated.
+     * @param changeMask the mask indicating what changed.
+     *
+     * @see IDeviceChangeListener#deviceChanged(IDevice)
+     */
+    @Override
+    public void deviceChanged(final IDevice device, int changeMask) {
+        boolean expand = false;
+        synchronized (mDevicesToExpand) {
+            int index = mDevicesToExpand.indexOf(device);
+            if (device.hasClients() && index != -1) {
+                mDevicesToExpand.remove(index);
+                expand = true;
+            }
+        }
+
+        final boolean finalExpand = expand;
+
+        exec(new Runnable() {
+            @Override
+            public void run() {
+                if (mTree.isDisposed() == false) {
+                    // look if the current device is selected. This is done in case the current
+                    // client of this particular device was killed. In this case, we'll need to
+                    // manually reselect the device.
+
+                    IDevice selectedDevice = getSelectedDevice();
+
+                    // refresh the device
+                    mTreeViewer.refresh(device);
+
+                    // if the selected device was the changed device and the new selection is
+                    // empty, we reselect the device.
+                    if (selectedDevice == device && mTreeViewer.getSelection().isEmpty()) {
+                        mTreeViewer.setSelection(new TreeSelection(new TreePath(
+                                new Object[] { device })));
+                    }
+
+                    // notify the listener of a possible selection change.
+                    notifyListeners();
+
+                    if (finalExpand) {
+                        mTreeViewer.setExpandedState(device, true);
+                    }
+                } else {
+                    // tree is disposed, we need to do something.
+                    // lets remove ourselves from the listener.
+                    AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this);
+                    AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this);
+                    AndroidDebugBridge.removeClientChangeListener(DevicePanel.this);
+                }
+            }
+        });
+    }
+
+    /**
+     * Sent when an existing client information changed.
+     * <p/>
+     * This is sent from a non UI thread.
+     * @param client the updated client.
+     * @param changeMask the bit mask describing the changed properties. It can contain
+     * any of the following values: {@link Client#CHANGE_INFO},
+     * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE},
+     * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
+     * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
+     *
+     * @see IClientChangeListener#clientChanged(Client, int)
+     */
+    @Override
+    public void clientChanged(final Client client, final int changeMask) {
+        exec(new Runnable() {
+            @Override
+            public void run() {
+                if (mTree.isDisposed() == false) {
+                    // refresh the client
+                    mTreeViewer.refresh(client);
+
+                    if ((changeMask & Client.CHANGE_DEBUGGER_STATUS) ==
+                            Client.CHANGE_DEBUGGER_STATUS &&
+                            client.getClientData().getDebuggerConnectionStatus() ==
+                                DebuggerStatus.WAITING) {
+                        // make sure the device is expanded. Normally the setSelection below
+                        // will auto expand, but the children of device may not already exist
+                        // at this time. Forcing an expand will make the TreeViewer create them.
+                        IDevice device = client.getDevice();
+                        if (mTreeViewer.getExpandedState(device) == false) {
+                            mTreeViewer.setExpandedState(device, true);
+                        }
+
+                        // create and set the selection
+                        TreePath treePath = new TreePath(new Object[] { device, client});
+                        TreeSelection treeSelection = new TreeSelection(treePath);
+                        mTreeViewer.setSelection(treeSelection);
+
+                        if (mAdvancedPortSupport) {
+                            client.setAsSelectedClient();
+                        }
+
+                        // notify the listener of a possible selection change.
+                        notifyListeners(device, client);
+                    }
+                } else {
+                    // tree is disposed, we need to do something.
+                    // lets remove ourselves from the listener.
+                    AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this);
+                    AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this);
+                    AndroidDebugBridge.removeClientChangeListener(DevicePanel.this);
+                }
+            }
+        });
+    }
+
+    private void loadImages(Display display) {
+        ImageLoader loader = ImageLoader.getDdmUiLibLoader();
+
+        if (mDeviceImage == null) {
+            mDeviceImage = loader.loadImage(display, "device.png", //$NON-NLS-1$
+                    ICON_WIDTH, ICON_WIDTH,
+                    display.getSystemColor(SWT.COLOR_RED));
+        }
+        if (mEmulatorImage == null) {
+            mEmulatorImage = loader.loadImage(display,
+                    "emulator.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$
+                    display.getSystemColor(SWT.COLOR_BLUE));
+        }
+        if (mThreadImage == null) {
+            mThreadImage = loader.loadImage(display, ICON_THREAD,
+                    ICON_WIDTH, ICON_WIDTH,
+                    display.getSystemColor(SWT.COLOR_YELLOW));
+        }
+        if (mHeapImage == null) {
+            mHeapImage = loader.loadImage(display, ICON_HEAP,
+                    ICON_WIDTH, ICON_WIDTH,
+                    display.getSystemColor(SWT.COLOR_BLUE));
+        }
+        if (mWaitingImage == null) {
+            mWaitingImage = loader.loadImage(display,
+                    "debug-wait.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$
+                    display.getSystemColor(SWT.COLOR_RED));
+        }
+        if (mDebuggerImage == null) {
+            mDebuggerImage = loader.loadImage(display,
+                    "debug-attach.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$
+                    display.getSystemColor(SWT.COLOR_GREEN));
+        }
+        if (mDebugErrorImage == null) {
+            mDebugErrorImage = loader.loadImage(display,
+                    "debug-error.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$
+                    display.getSystemColor(SWT.COLOR_RED));
+        }
+    }
+
+    /**
+     * Returns a display string representing the state of the device.
+     * @param d the device
+     */
+    private static String getStateString(IDevice d) {
+        DeviceState deviceState = d.getState();
+        if (deviceState == DeviceState.ONLINE) {
+            return "Online";
+        } else if (deviceState == DeviceState.OFFLINE) {
+            return "Offline";
+        } else if (deviceState == DeviceState.BOOTLOADER) {
+            return "Bootloader";
+        }
+
+        return "??";
+    }
+
+    /**
+     * Executes the {@link Runnable} in the UI thread.
+     * @param runnable the runnable to execute.
+     */
+    private void exec(Runnable runnable) {
+        try {
+            Display display = mTree.getDisplay();
+            display.asyncExec(runnable);
+        } catch (SWTException e) {
+            // tree is disposed, we need to do something. lets remove ourselves from the listener.
+            AndroidDebugBridge.removeDebugBridgeChangeListener(this);
+            AndroidDebugBridge.removeDeviceChangeListener(this);
+            AndroidDebugBridge.removeClientChangeListener(this);
+        }
+    }
+
+    private void notifyListeners() {
+        // get the selection
+        TreeItem[] items = mTree.getSelection();
+
+        Client client = null;
+        IDevice device = null;
+
+        if (items.length == 1) {
+            Object object = items[0].getData();
+            if (object instanceof Client) {
+                client = (Client)object;
+                device = client.getDevice();
+            } else if (object instanceof IDevice) {
+                device = (IDevice)object;
+            }
+        }
+
+        notifyListeners(device, client);
+    }
+
+    private void notifyListeners(IDevice selectedDevice, Client selectedClient) {
+        if (selectedDevice != mCurrentDevice || selectedClient != mCurrentClient) {
+            mCurrentDevice = selectedDevice;
+            mCurrentClient = selectedClient;
+
+            for (IUiSelectionListener listener : mListeners) {
+                // notify the listener with a try/catch-all to make sure this thread won't die
+                // because of an uncaught exception before all the listeners were notified.
+                try {
+                    listener.selectionChanged(selectedDevice, selectedClient);
+                } catch (Exception e) {
+                }
+            }
+        }
+    }
+
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/EmulatorControlPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/EmulatorControlPanel.java
new file mode 100644
index 0000000..82aed98
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/EmulatorControlPanel.java
@@ -0,0 +1,1463 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib;
+
+import com.android.ddmlib.EmulatorConsole;
+import com.android.ddmlib.EmulatorConsole.GsmMode;
+import com.android.ddmlib.EmulatorConsole.GsmStatus;
+import com.android.ddmlib.EmulatorConsole.NetworkStatus;
+import com.android.ddmlib.IDevice;
+import com.android.ddmuilib.location.CoordinateControls;
+import com.android.ddmuilib.location.GpxParser;
+import com.android.ddmuilib.location.GpxParser.Track;
+import com.android.ddmuilib.location.KmlParser;
+import com.android.ddmuilib.location.TrackContentProvider;
+import com.android.ddmuilib.location.TrackLabelProvider;
+import com.android.ddmuilib.location.TrackPoint;
+import com.android.ddmuilib.location.WayPoint;
+import com.android.ddmuilib.location.WayPointContentProvider;
+import com.android.ddmuilib.location.WayPointLabelProvider;
+
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.custom.StackLayout;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.TabFolder;
+import org.eclipse.swt.widgets.TabItem;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.Text;
+
+/**
+ * Panel to control the emulator using EmulatorConsole objects.
+ */
+public class EmulatorControlPanel extends SelectionDependentPanel {
+
+    // default location: Patio outside Charlie's
+    private final static double DEFAULT_LONGITUDE = -122.084095;
+    private final static double DEFAULT_LATITUDE = 37.422006;
+
+    private final static String SPEED_FORMAT = "Speed: %1$dX";
+
+
+    /**
+     * Map between the display gsm mode and the internal tag used by the display.
+     */
+    private final static String[][] GSM_MODES = new String[][] {
+        { "unregistered", GsmMode.UNREGISTERED.getTag() },
+        { "home", GsmMode.HOME.getTag() },
+        { "roaming", GsmMode.ROAMING.getTag() },
+        { "searching", GsmMode.SEARCHING.getTag() },
+        { "denied", GsmMode.DENIED.getTag() },
+    };
+
+    private final static String[] NETWORK_SPEEDS = new String[] {
+        "Full",
+        "GSM",
+        "HSCSD",
+        "GPRS",
+        "EDGE",
+        "UMTS",
+        "HSDPA",
+    };
+
+    private final static String[] NETWORK_LATENCIES = new String[] {
+        "None",
+        "GPRS",
+        "EDGE",
+        "UMTS",
+    };
+
+    private final static int[] PLAY_SPEEDS = new int[] { 1, 2, 5, 10, 20, 50 };
+
+    private final static String RE_PHONE_NUMBER = "^[+#0-9]+$"; //$NON-NLS-1$
+    private final static String PREFS_WAYPOINT_COL_NAME = "emulatorControl.waypoint.name"; //$NON-NLS-1$
+    private final static String PREFS_WAYPOINT_COL_LONGITUDE = "emulatorControl.waypoint.longitude"; //$NON-NLS-1$
+    private final static String PREFS_WAYPOINT_COL_LATITUDE = "emulatorControl.waypoint.latitude"; //$NON-NLS-1$
+    private final static String PREFS_WAYPOINT_COL_ELEVATION = "emulatorControl.waypoint.elevation"; //$NON-NLS-1$
+    private final static String PREFS_WAYPOINT_COL_DESCRIPTION = "emulatorControl.waypoint.desc"; //$NON-NLS-1$
+    private final static String PREFS_TRACK_COL_NAME = "emulatorControl.track.name"; //$NON-NLS-1$
+    private final static String PREFS_TRACK_COL_COUNT = "emulatorControl.track.count"; //$NON-NLS-1$
+    private final static String PREFS_TRACK_COL_FIRST = "emulatorControl.track.first"; //$NON-NLS-1$
+    private final static String PREFS_TRACK_COL_LAST = "emulatorControl.track.last"; //$NON-NLS-1$
+    private final static String PREFS_TRACK_COL_COMMENT = "emulatorControl.track.comment"; //$NON-NLS-1$
+
+    private EmulatorConsole mEmulatorConsole;
+
+    private Composite mParent;
+
+    private Label mVoiceLabel;
+    private Combo mVoiceMode;
+    private Label mDataLabel;
+    private Combo mDataMode;
+    private Label mSpeedLabel;
+    private Combo mNetworkSpeed;
+    private Label mLatencyLabel;
+    private Combo mNetworkLatency;
+
+    private Label mNumberLabel;
+    private Text mPhoneNumber;
+
+    private Button mVoiceButton;
+    private Button mSmsButton;
+
+    private Label mMessageLabel;
+    private Text mSmsMessage;
+
+    private Button mCallButton;
+    private Button mCancelButton;
+
+    private TabFolder mLocationFolders;
+
+    private Button mDecimalButton;
+    private Button mSexagesimalButton;
+    private CoordinateControls mLongitudeControls;
+    private CoordinateControls mLatitudeControls;
+    private Button mGpxUploadButton;
+    private Table mGpxWayPointTable;
+    private Table mGpxTrackTable;
+    private Button mKmlUploadButton;
+    private Table mKmlWayPointTable;
+
+    private Button mPlayGpxButton;
+    private Button mGpxBackwardButton;
+    private Button mGpxForwardButton;
+    private Button mGpxSpeedButton;
+    private Button mPlayKmlButton;
+    private Button mKmlBackwardButton;
+    private Button mKmlForwardButton;
+    private Button mKmlSpeedButton;
+
+    private Image mPlayImage;
+    private Image mPauseImage;
+
+    private Thread mPlayingThread;
+    private boolean mPlayingTrack;
+    private int mPlayDirection = 1;
+    private int mSpeed;
+    private int mSpeedIndex;
+
+    private final SelectionAdapter mDirectionButtonAdapter = new SelectionAdapter() {
+        @Override
+        public void widgetSelected(SelectionEvent e) {
+            Button b = (Button)e.getSource();
+            if (b.getSelection() == false) {
+                // basically the button was unselected, which we don't allow.
+                // so we reselect it.
+                b.setSelection(true);
+                return;
+            }
+
+            // now handle selection change.
+            if (b == mGpxForwardButton || b == mKmlForwardButton) {
+                mGpxBackwardButton.setSelection(false);
+                mGpxForwardButton.setSelection(true);
+                mKmlBackwardButton.setSelection(false);
+                mKmlForwardButton.setSelection(true);
+                mPlayDirection = 1;
+
+            } else {
+                mGpxBackwardButton.setSelection(true);
+                mGpxForwardButton.setSelection(false);
+                mKmlBackwardButton.setSelection(true);
+                mKmlForwardButton.setSelection(false);
+                mPlayDirection = -1;
+            }
+        }
+    };
+
+    private final SelectionAdapter mSpeedButtonAdapter = new SelectionAdapter() {
+        @Override
+        public void widgetSelected(SelectionEvent e) {
+            mSpeedIndex = (mSpeedIndex+1) % PLAY_SPEEDS.length;
+            mSpeed = PLAY_SPEEDS[mSpeedIndex];
+
+            mGpxSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed));
+            mGpxPlayControls.pack();
+            mKmlSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed));
+            mKmlPlayControls.pack();
+
+            if (mPlayingThread != null) {
+                mPlayingThread.interrupt();
+            }
+        }
+     };
+    private Composite mKmlPlayControls;
+    private Composite mGpxPlayControls;
+
+
+    public EmulatorControlPanel() {
+    }
+
+    /**
+     * Sent when a new device is selected. The new device can be accessed
+     * with {@link #getCurrentDevice()}
+     */
+    @Override
+    public void deviceSelected() {
+        handleNewDevice(getCurrentDevice());
+    }
+
+    /**
+     * Sent when a new client is selected. The new client can be accessed
+     * with {@link #getCurrentClient()}
+     */
+    @Override
+    public void clientSelected() {
+        // pass
+    }
+
+    /**
+     * Creates a control capable of displaying some information.  This is
+     * called once, when the application is initializing, from the UI thread.
+     */
+    @Override
+    protected Control createControl(Composite parent) {
+        mParent = parent;
+
+        final ScrolledComposite scollingParent = new ScrolledComposite(parent, SWT.V_SCROLL);
+        scollingParent.setExpandVertical(true);
+        scollingParent.setExpandHorizontal(true);
+        scollingParent.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        final Composite top = new Composite(scollingParent, SWT.NONE);
+        scollingParent.setContent(top);
+        top.setLayout(new GridLayout(1, false));
+
+        // set the resize for the scrolling to work (why isn't that done automatically?!?)
+        scollingParent.addControlListener(new ControlAdapter() {
+            @Override
+            public void controlResized(ControlEvent e) {
+                Rectangle r = scollingParent.getClientArea();
+                scollingParent.setMinSize(top.computeSize(r.width, SWT.DEFAULT));
+            }
+        });
+
+        createRadioControls(top);
+
+        createCallControls(top);
+
+        createLocationControls(top);
+
+        doEnable(false);
+
+        top.layout();
+        Rectangle r = scollingParent.getClientArea();
+        scollingParent.setMinSize(top.computeSize(r.width, SWT.DEFAULT));
+
+        return scollingParent;
+    }
+
+    /**
+     * Create Radio (on/off/roaming, for voice/data) controls.
+     * @param top
+     */
+    private void createRadioControls(final Composite top) {
+        Group g1 = new Group(top, SWT.NONE);
+        g1.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        g1.setLayout(new GridLayout(2, false));
+        g1.setText("Telephony Status");
+
+        // the inside of the group is 2 composite so that all the column of the controls (mainly
+        // combos) have the same width, while not taking the whole screen width
+        Composite insideGroup = new Composite(g1, SWT.NONE);
+        GridLayout gl = new GridLayout(4, false);
+        gl.marginBottom = gl.marginHeight = gl.marginLeft = gl.marginRight = 0;
+        insideGroup.setLayout(gl);
+
+        mVoiceLabel = new Label(insideGroup, SWT.NONE);
+        mVoiceLabel.setText("Voice:");
+        mVoiceLabel.setAlignment(SWT.RIGHT);
+
+        mVoiceMode = new Combo(insideGroup, SWT.READ_ONLY);
+        mVoiceMode.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        for (String[] mode : GSM_MODES) {
+            mVoiceMode.add(mode[0]);
+        }
+        mVoiceMode.addSelectionListener(new SelectionAdapter() {
+            // called when selection changes
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                setVoiceMode(mVoiceMode.getSelectionIndex());
+            }
+        });
+
+        mSpeedLabel = new Label(insideGroup, SWT.NONE);
+        mSpeedLabel.setText("Speed:");
+        mSpeedLabel.setAlignment(SWT.RIGHT);
+
+        mNetworkSpeed = new Combo(insideGroup, SWT.READ_ONLY);
+        mNetworkSpeed.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        for (String mode : NETWORK_SPEEDS) {
+            mNetworkSpeed.add(mode);
+        }
+        mNetworkSpeed.addSelectionListener(new SelectionAdapter() {
+            // called when selection changes
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                setNetworkSpeed(mNetworkSpeed.getSelectionIndex());
+            }
+        });
+
+        mDataLabel = new Label(insideGroup, SWT.NONE);
+        mDataLabel.setText("Data:");
+        mDataLabel.setAlignment(SWT.RIGHT);
+
+        mDataMode = new Combo(insideGroup, SWT.READ_ONLY);
+        mDataMode.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        for (String[] mode : GSM_MODES) {
+            mDataMode.add(mode[0]);
+        }
+        mDataMode.addSelectionListener(new SelectionAdapter() {
+            // called when selection changes
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                setDataMode(mDataMode.getSelectionIndex());
+            }
+        });
+
+        mLatencyLabel = new Label(insideGroup, SWT.NONE);
+        mLatencyLabel.setText("Latency:");
+        mLatencyLabel.setAlignment(SWT.RIGHT);
+
+        mNetworkLatency = new Combo(insideGroup, SWT.READ_ONLY);
+        mNetworkLatency.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        for (String mode : NETWORK_LATENCIES) {
+            mNetworkLatency.add(mode);
+        }
+        mNetworkLatency.addSelectionListener(new SelectionAdapter() {
+            // called when selection changes
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                setNetworkLatency(mNetworkLatency.getSelectionIndex());
+            }
+        });
+
+        // now an empty label to take the rest of the width of the group
+        Label l = new Label(g1, SWT.NONE);
+        l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+    }
+
+    /**
+     * Create Voice/SMS call/hang up controls
+     * @param top
+     */
+    private void createCallControls(final Composite top) {
+        GridLayout gl;
+        Group g2 = new Group(top, SWT.NONE);
+        g2.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        g2.setLayout(new GridLayout(1, false));
+        g2.setText("Telephony Actions");
+
+        // horizontal composite for label + text field
+        Composite phoneComp = new Composite(g2, SWT.NONE);
+        phoneComp.setLayoutData(new GridData(GridData.FILL_BOTH));
+        gl = new GridLayout(2, false);
+        gl.marginBottom = gl.marginHeight = gl.marginLeft = gl.marginRight = 0;
+        phoneComp.setLayout(gl);
+
+        mNumberLabel = new Label(phoneComp, SWT.NONE);
+        mNumberLabel.setText("Incoming number:");
+
+        mPhoneNumber = new Text(phoneComp, SWT.BORDER | SWT.LEFT | SWT.SINGLE);
+        mPhoneNumber.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mPhoneNumber.addModifyListener(new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent e) {
+                // Reenable the widgets based on the content of the text.
+                // doEnable checks the validity of the phone number to enable/disable some
+                // widgets.
+                // Looks like we're getting a callback at creation time, so we can't
+                // suppose that we are enabled when the text is modified...
+                doEnable(mEmulatorConsole != null);
+            }
+        });
+
+        mVoiceButton = new Button(phoneComp, SWT.RADIO);
+        GridData gd = new GridData();
+        gd.horizontalSpan = 2;
+        mVoiceButton.setText("Voice");
+        mVoiceButton.setLayoutData(gd);
+        mVoiceButton.setEnabled(false);
+        mVoiceButton.setSelection(true);
+        mVoiceButton.addSelectionListener(new SelectionAdapter() {
+            // called when selection changes
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                doEnable(true);
+
+                if (mVoiceButton.getSelection()) {
+                    mCallButton.setText("Call");
+                } else {
+                    mCallButton.setText("Send");
+                }
+            }
+        });
+
+        mSmsButton = new Button(phoneComp, SWT.RADIO);
+        mSmsButton.setText("SMS");
+        gd = new GridData();
+        gd.horizontalSpan = 2;
+        mSmsButton.setLayoutData(gd);
+        mSmsButton.setEnabled(false);
+        // Since there are only 2 radio buttons, we can put a listener on only one (they
+        // are both called on select and unselect event.
+
+        mMessageLabel = new Label(phoneComp, SWT.NONE);
+        gd = new GridData();
+        gd.verticalAlignment = SWT.TOP;
+        mMessageLabel.setLayoutData(gd);
+        mMessageLabel.setText("Message:");
+        mMessageLabel.setEnabled(false);
+
+        mSmsMessage = new Text(phoneComp, SWT.BORDER | SWT.LEFT | SWT.MULTI | SWT.WRAP | SWT.V_SCROLL);
+        mSmsMessage.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+        gd.heightHint = 70;
+        mSmsMessage.setEnabled(false);
+
+        // composite to put the 2 buttons horizontally
+        Composite g2ButtonComp = new Composite(g2, SWT.NONE);
+        g2ButtonComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        gl = new GridLayout(2, false);
+        gl.marginWidth = gl.marginHeight = 0;
+        g2ButtonComp.setLayout(gl);
+
+        // now a button below the phone number
+        mCallButton = new Button(g2ButtonComp, SWT.PUSH);
+        mCallButton.setText("Call");
+        mCallButton.setEnabled(false);
+        mCallButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                if (mEmulatorConsole != null) {
+                    if (mVoiceButton.getSelection()) {
+                        processCommandResult(mEmulatorConsole.call(mPhoneNumber.getText().trim()));
+                    } else {
+                        // we need to encode the message. We need to replace the carriage return
+                        // character by the 2 character string \n.
+                        // Because of this the \ character needs to be escaped as well.
+                        // ReplaceAll() expects regexp so \ char are escaped twice.
+                        String message = mSmsMessage.getText();
+                        message = message.replaceAll("\\\\", //$NON-NLS-1$
+                                "\\\\\\\\"); //$NON-NLS-1$
+
+                        // While the normal line delimiter is returned by Text.getLineDelimiter()
+                        // it seems copy pasting text coming from somewhere else could have another
+                        // delimited. For this reason, we'll replace is several steps
+
+                        // replace the dual CR-LF
+                        message = message.replaceAll("\r\n", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-2$
+
+                        // replace remaining stand alone \n
+                        message = message.replaceAll("\n", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-2$
+
+                        // replace remaining stand alone \r
+                        message = message.replaceAll("\r", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-2$
+
+                        processCommandResult(mEmulatorConsole.sendSms(mPhoneNumber.getText().trim(),
+                                message));
+                    }
+                }
+            }
+        });
+
+        mCancelButton = new Button(g2ButtonComp, SWT.PUSH);
+        mCancelButton.setText("Hang Up");
+        mCancelButton.setEnabled(false);
+        mCancelButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                if (mEmulatorConsole != null) {
+                    if (mVoiceButton.getSelection()) {
+                        processCommandResult(mEmulatorConsole.cancelCall(
+                                mPhoneNumber.getText().trim()));
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * Create Location controls.
+     * @param top
+     */
+    private void createLocationControls(final Composite top) {
+        Label l = new Label(top, SWT.NONE);
+        l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        l.setText("Location Controls");
+
+        mLocationFolders = new TabFolder(top, SWT.NONE);
+        mLocationFolders.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        Composite manualLocationComp = new Composite(mLocationFolders, SWT.NONE);
+        TabItem item = new TabItem(mLocationFolders, SWT.NONE);
+        item.setText("Manual");
+        item.setControl(manualLocationComp);
+
+        createManualLocationControl(manualLocationComp);
+
+        ImageLoader loader = ImageLoader.getDdmUiLibLoader();
+        mPlayImage = loader.loadImage("play.png", mParent.getDisplay()); //$NON-NLS-1$
+        mPauseImage = loader.loadImage("pause.png", mParent.getDisplay()); //$NON-NLS-1$
+
+        Composite gpxLocationComp = new Composite(mLocationFolders, SWT.NONE);
+        item = new TabItem(mLocationFolders, SWT.NONE);
+        item.setText("GPX");
+        item.setControl(gpxLocationComp);
+
+        createGpxLocationControl(gpxLocationComp);
+
+        Composite kmlLocationComp = new Composite(mLocationFolders, SWT.NONE);
+        kmlLocationComp.setLayout(new FillLayout());
+        item = new TabItem(mLocationFolders, SWT.NONE);
+        item.setText("KML");
+        item.setControl(kmlLocationComp);
+
+        createKmlLocationControl(kmlLocationComp);
+    }
+
+    private void createManualLocationControl(Composite manualLocationComp) {
+        final StackLayout sl;
+        GridLayout gl;
+        Label label;
+
+        manualLocationComp.setLayout(new GridLayout(1, false));
+        mDecimalButton = new Button(manualLocationComp, SWT.RADIO);
+        mDecimalButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mDecimalButton.setText("Decimal");
+        mSexagesimalButton = new Button(manualLocationComp, SWT.RADIO);
+        mSexagesimalButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mSexagesimalButton.setText("Sexagesimal");
+
+        // composite to hold and switching between the 2 modes.
+        final Composite content = new Composite(manualLocationComp, SWT.NONE);
+        content.setLayout(sl = new StackLayout());
+
+        // decimal display
+        final Composite decimalContent = new Composite(content, SWT.NONE);
+        decimalContent.setLayout(gl = new GridLayout(2, false));
+        gl.marginHeight = gl.marginWidth = 0;
+
+        mLongitudeControls = new CoordinateControls();
+        mLatitudeControls = new CoordinateControls();
+
+        label = new Label(decimalContent, SWT.NONE);
+        label.setText("Longitude");
+
+        mLongitudeControls.createDecimalText(decimalContent);
+
+        label = new Label(decimalContent, SWT.NONE);
+        label.setText("Latitude");
+
+        mLatitudeControls.createDecimalText(decimalContent);
+
+        // sexagesimal content
+        final Composite sexagesimalContent = new Composite(content, SWT.NONE);
+        sexagesimalContent.setLayout(gl = new GridLayout(7, false));
+        gl.marginHeight = gl.marginWidth = 0;
+
+        label = new Label(sexagesimalContent, SWT.NONE);
+        label.setText("Longitude");
+
+        mLongitudeControls.createSexagesimalDegreeText(sexagesimalContent);
+
+        label = new Label(sexagesimalContent, SWT.NONE);
+        label.setText("\u00B0"); // degree character
+
+        mLongitudeControls.createSexagesimalMinuteText(sexagesimalContent);
+
+        label = new Label(sexagesimalContent, SWT.NONE);
+        label.setText("'");
+
+        mLongitudeControls.createSexagesimalSecondText(sexagesimalContent);
+
+        label = new Label(sexagesimalContent, SWT.NONE);
+        label.setText("\"");
+
+        label = new Label(sexagesimalContent, SWT.NONE);
+        label.setText("Latitude");
+
+        mLatitudeControls.createSexagesimalDegreeText(sexagesimalContent);
+
+        label = new Label(sexagesimalContent, SWT.NONE);
+        label.setText("\u00B0");
+
+        mLatitudeControls.createSexagesimalMinuteText(sexagesimalContent);
+
+        label = new Label(sexagesimalContent, SWT.NONE);
+        label.setText("'");
+
+        mLatitudeControls.createSexagesimalSecondText(sexagesimalContent);
+
+        label = new Label(sexagesimalContent, SWT.NONE);
+        label.setText("\"");
+
+        // set the default display to decimal
+        sl.topControl = decimalContent;
+        mDecimalButton.setSelection(true);
+
+        mDecimalButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                if (mDecimalButton.getSelection()) {
+                    sl.topControl = decimalContent;
+                } else {
+                    sl.topControl = sexagesimalContent;
+                }
+                content.layout();
+            }
+        });
+
+        Button sendButton = new Button(manualLocationComp, SWT.PUSH);
+        sendButton.setText("Send");
+        sendButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                if (mEmulatorConsole != null) {
+                    processCommandResult(mEmulatorConsole.sendLocation(
+                            mLongitudeControls.getValue(), mLatitudeControls.getValue(), 0));
+                }
+            }
+        });
+
+        mLongitudeControls.setValue(DEFAULT_LONGITUDE);
+        mLatitudeControls.setValue(DEFAULT_LATITUDE);
+    }
+
+    private void createGpxLocationControl(Composite gpxLocationComp) {
+        GridData gd;
+
+        IPreferenceStore store = DdmUiPreferences.getStore();
+
+        gpxLocationComp.setLayout(new GridLayout(1, false));
+
+        mGpxUploadButton = new Button(gpxLocationComp, SWT.PUSH);
+        mGpxUploadButton.setText("Load GPX...");
+
+        // Table for way point
+        mGpxWayPointTable = new Table(gpxLocationComp,
+                SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION);
+        mGpxWayPointTable.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+        gd.heightHint = 100;
+        mGpxWayPointTable.setHeaderVisible(true);
+        mGpxWayPointTable.setLinesVisible(true);
+
+        TableHelper.createTableColumn(mGpxWayPointTable, "Name", SWT.LEFT,
+                "Some Name",
+                PREFS_WAYPOINT_COL_NAME, store);
+        TableHelper.createTableColumn(mGpxWayPointTable, "Longitude", SWT.LEFT,
+                "-199.999999",
+                PREFS_WAYPOINT_COL_LONGITUDE, store);
+        TableHelper.createTableColumn(mGpxWayPointTable, "Latitude", SWT.LEFT,
+                "-199.999999",
+                PREFS_WAYPOINT_COL_LATITUDE, store);
+        TableHelper.createTableColumn(mGpxWayPointTable, "Elevation", SWT.LEFT,
+                "99999.9",
+                PREFS_WAYPOINT_COL_ELEVATION, store);
+        TableHelper.createTableColumn(mGpxWayPointTable, "Description", SWT.LEFT,
+                "Some Description",
+                PREFS_WAYPOINT_COL_DESCRIPTION, store);
+
+        final TableViewer gpxWayPointViewer = new TableViewer(mGpxWayPointTable);
+        gpxWayPointViewer.setContentProvider(new WayPointContentProvider());
+        gpxWayPointViewer.setLabelProvider(new WayPointLabelProvider());
+
+        gpxWayPointViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+            @Override
+            public void selectionChanged(SelectionChangedEvent event) {
+                ISelection selection = event.getSelection();
+                if (selection instanceof IStructuredSelection) {
+                    IStructuredSelection structuredSelection = (IStructuredSelection)selection;
+                    Object selectedObject = structuredSelection.getFirstElement();
+                    if (selectedObject instanceof WayPoint) {
+                        WayPoint wayPoint = (WayPoint)selectedObject;
+
+                        if (mEmulatorConsole != null && mPlayingTrack == false) {
+                            processCommandResult(mEmulatorConsole.sendLocation(
+                                    wayPoint.getLongitude(), wayPoint.getLatitude(),
+                                    wayPoint.getElevation()));
+                        }
+                    }
+                }
+            }
+        });
+
+        // table for tracks.
+        mGpxTrackTable = new Table(gpxLocationComp,
+                SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION);
+        mGpxTrackTable.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+        gd.heightHint = 100;
+        mGpxTrackTable.setHeaderVisible(true);
+        mGpxTrackTable.setLinesVisible(true);
+
+        TableHelper.createTableColumn(mGpxTrackTable, "Name", SWT.LEFT,
+                "Some very long name",
+                PREFS_TRACK_COL_NAME, store);
+        TableHelper.createTableColumn(mGpxTrackTable, "Point Count", SWT.RIGHT,
+                "9999",
+                PREFS_TRACK_COL_COUNT, store);
+        TableHelper.createTableColumn(mGpxTrackTable, "First Point Time", SWT.LEFT,
+                "999-99-99T99:99:99Z",
+                PREFS_TRACK_COL_FIRST, store);
+        TableHelper.createTableColumn(mGpxTrackTable, "Last Point Time", SWT.LEFT,
+                "999-99-99T99:99:99Z",
+                PREFS_TRACK_COL_LAST, store);
+        TableHelper.createTableColumn(mGpxTrackTable, "Comment", SWT.LEFT,
+                "-199.999999",
+                PREFS_TRACK_COL_COMMENT, store);
+
+        final TableViewer gpxTrackViewer = new TableViewer(mGpxTrackTable);
+        gpxTrackViewer.setContentProvider(new TrackContentProvider());
+        gpxTrackViewer.setLabelProvider(new TrackLabelProvider());
+
+        gpxTrackViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+            @Override
+            public void selectionChanged(SelectionChangedEvent event) {
+                ISelection selection = event.getSelection();
+                if (selection instanceof IStructuredSelection) {
+                    IStructuredSelection structuredSelection = (IStructuredSelection)selection;
+                    Object selectedObject = structuredSelection.getFirstElement();
+                    if (selectedObject instanceof Track) {
+                        Track track = (Track)selectedObject;
+
+                        if (mEmulatorConsole != null && mPlayingTrack == false) {
+                            TrackPoint[] points = track.getPoints();
+                            processCommandResult(mEmulatorConsole.sendLocation(
+                                    points[0].getLongitude(), points[0].getLatitude(),
+                                    points[0].getElevation()));
+                        }
+
+                        mPlayGpxButton.setEnabled(true);
+                        mGpxBackwardButton.setEnabled(true);
+                        mGpxForwardButton.setEnabled(true);
+                        mGpxSpeedButton.setEnabled(true);
+
+                        return;
+                    }
+                }
+
+                mPlayGpxButton.setEnabled(false);
+                mGpxBackwardButton.setEnabled(false);
+                mGpxForwardButton.setEnabled(false);
+                mGpxSpeedButton.setEnabled(false);
+            }
+        });
+
+        mGpxUploadButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN);
+
+                fileDialog.setText("Load GPX File");
+                fileDialog.setFilterExtensions(new String[] { "*.gpx" } );
+
+                String fileName = fileDialog.open();
+                if (fileName != null) {
+                    GpxParser parser = new GpxParser(fileName);
+                    if (parser.parse()) {
+                        gpxWayPointViewer.setInput(parser.getWayPoints());
+                        gpxTrackViewer.setInput(parser.getTracks());
+                    }
+                }
+            }
+        });
+
+        mGpxPlayControls = new Composite(gpxLocationComp, SWT.NONE);
+        GridLayout gl;
+        mGpxPlayControls.setLayout(gl = new GridLayout(5, false));
+        gl.marginHeight = gl.marginWidth = 0;
+        mGpxPlayControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        mPlayGpxButton = new Button(mGpxPlayControls, SWT.PUSH | SWT.FLAT);
+        mPlayGpxButton.setImage(mPlayImage);
+        mPlayGpxButton.addSelectionListener(new SelectionAdapter() {
+           @Override
+            public void widgetSelected(SelectionEvent e) {
+               if (mPlayingTrack == false) {
+                   ISelection selection = gpxTrackViewer.getSelection();
+                   if (selection.isEmpty() == false && selection instanceof IStructuredSelection) {
+                       IStructuredSelection structuredSelection = (IStructuredSelection)selection;
+                       Object selectedObject = structuredSelection.getFirstElement();
+                       if (selectedObject instanceof Track) {
+                           Track track = (Track)selectedObject;
+                           playTrack(track);
+                       }
+                   }
+               } else {
+                   // if we're playing, then we pause
+                   mPlayingTrack = false;
+                   if (mPlayingThread != null) {
+                       mPlayingThread.interrupt();
+                   }
+               }
+            }
+        });
+
+        Label separator = new Label(mGpxPlayControls, SWT.SEPARATOR | SWT.VERTICAL);
+        separator.setLayoutData(gd = new GridData(
+                GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL));
+        gd.heightHint = 0;
+
+        ImageLoader loader = ImageLoader.getDdmUiLibLoader();
+        mGpxBackwardButton = new Button(mGpxPlayControls, SWT.TOGGLE | SWT.FLAT);
+        mGpxBackwardButton.setImage(loader.loadImage("backward.png", mParent.getDisplay())); //$NON-NLS-1$
+        mGpxBackwardButton.setSelection(false);
+        mGpxBackwardButton.addSelectionListener(mDirectionButtonAdapter);
+        mGpxForwardButton = new Button(mGpxPlayControls, SWT.TOGGLE | SWT.FLAT);
+        mGpxForwardButton.setImage(loader.loadImage("forward.png", mParent.getDisplay())); //$NON-NLS-1$
+        mGpxForwardButton.setSelection(true);
+        mGpxForwardButton.addSelectionListener(mDirectionButtonAdapter);
+
+        mGpxSpeedButton = new Button(mGpxPlayControls, SWT.PUSH | SWT.FLAT);
+
+        mSpeedIndex = 0;
+        mSpeed = PLAY_SPEEDS[mSpeedIndex];
+
+        mGpxSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed));
+        mGpxSpeedButton.addSelectionListener(mSpeedButtonAdapter);
+
+        mPlayGpxButton.setEnabled(false);
+        mGpxBackwardButton.setEnabled(false);
+        mGpxForwardButton.setEnabled(false);
+        mGpxSpeedButton.setEnabled(false);
+
+    }
+
+    private void createKmlLocationControl(Composite kmlLocationComp) {
+        GridData gd;
+
+        IPreferenceStore store = DdmUiPreferences.getStore();
+
+        kmlLocationComp.setLayout(new GridLayout(1, false));
+
+        mKmlUploadButton = new Button(kmlLocationComp, SWT.PUSH);
+        mKmlUploadButton.setText("Load KML...");
+
+        // Table for way point
+        mKmlWayPointTable = new Table(kmlLocationComp,
+                SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION);
+        mKmlWayPointTable.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+        gd.heightHint = 200;
+        mKmlWayPointTable.setHeaderVisible(true);
+        mKmlWayPointTable.setLinesVisible(true);
+
+        TableHelper.createTableColumn(mKmlWayPointTable, "Name", SWT.LEFT,
+                "Some Name",
+                PREFS_WAYPOINT_COL_NAME, store);
+        TableHelper.createTableColumn(mKmlWayPointTable, "Longitude", SWT.LEFT,
+                "-199.999999",
+                PREFS_WAYPOINT_COL_LONGITUDE, store);
+        TableHelper.createTableColumn(mKmlWayPointTable, "Latitude", SWT.LEFT,
+                "-199.999999",
+                PREFS_WAYPOINT_COL_LATITUDE, store);
+        TableHelper.createTableColumn(mKmlWayPointTable, "Elevation", SWT.LEFT,
+                "99999.9",
+                PREFS_WAYPOINT_COL_ELEVATION, store);
+        TableHelper.createTableColumn(mKmlWayPointTable, "Description", SWT.LEFT,
+                "Some Description",
+                PREFS_WAYPOINT_COL_DESCRIPTION, store);
+
+        final TableViewer kmlWayPointViewer = new TableViewer(mKmlWayPointTable);
+        kmlWayPointViewer.setContentProvider(new WayPointContentProvider());
+        kmlWayPointViewer.setLabelProvider(new WayPointLabelProvider());
+
+        mKmlUploadButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN);
+
+                fileDialog.setText("Load KML File");
+                fileDialog.setFilterExtensions(new String[] { "*.kml" } );
+
+                String fileName = fileDialog.open();
+                if (fileName != null) {
+                    KmlParser parser = new KmlParser(fileName);
+                    if (parser.parse()) {
+                        kmlWayPointViewer.setInput(parser.getWayPoints());
+
+                        mPlayKmlButton.setEnabled(true);
+                        mKmlBackwardButton.setEnabled(true);
+                        mKmlForwardButton.setEnabled(true);
+                        mKmlSpeedButton.setEnabled(true);
+                    }
+                }
+            }
+        });
+
+        kmlWayPointViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+            @Override
+            public void selectionChanged(SelectionChangedEvent event) {
+                ISelection selection = event.getSelection();
+                if (selection instanceof IStructuredSelection) {
+                    IStructuredSelection structuredSelection = (IStructuredSelection)selection;
+                    Object selectedObject = structuredSelection.getFirstElement();
+                    if (selectedObject instanceof WayPoint) {
+                        WayPoint wayPoint = (WayPoint)selectedObject;
+
+                        if (mEmulatorConsole != null && mPlayingTrack == false) {
+                            processCommandResult(mEmulatorConsole.sendLocation(
+                                    wayPoint.getLongitude(), wayPoint.getLatitude(),
+                                    wayPoint.getElevation()));
+                        }
+                    }
+                }
+            }
+        });
+
+
+
+        mKmlPlayControls = new Composite(kmlLocationComp, SWT.NONE);
+        GridLayout gl;
+        mKmlPlayControls.setLayout(gl = new GridLayout(5, false));
+        gl.marginHeight = gl.marginWidth = 0;
+        mKmlPlayControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        mPlayKmlButton = new Button(mKmlPlayControls, SWT.PUSH | SWT.FLAT);
+        mPlayKmlButton.setImage(mPlayImage);
+        mPlayKmlButton.addSelectionListener(new SelectionAdapter() {
+           @Override
+            public void widgetSelected(SelectionEvent e) {
+               if (mPlayingTrack == false) {
+                   Object input = kmlWayPointViewer.getInput();
+                   if (input instanceof WayPoint[]) {
+                       playKml((WayPoint[])input);
+                   }
+               } else {
+                   // if we're playing, then we pause
+                   mPlayingTrack = false;
+                   if (mPlayingThread != null) {
+                       mPlayingThread.interrupt();
+                   }
+               }
+            }
+        });
+
+        Label separator = new Label(mKmlPlayControls, SWT.SEPARATOR | SWT.VERTICAL);
+        separator.setLayoutData(gd = new GridData(
+                GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL));
+        gd.heightHint = 0;
+
+        ImageLoader loader = ImageLoader.getDdmUiLibLoader();
+        mKmlBackwardButton = new Button(mKmlPlayControls, SWT.TOGGLE | SWT.FLAT);
+        mKmlBackwardButton.setImage(loader.loadImage("backward.png", mParent.getDisplay())); //$NON-NLS-1$
+        mKmlBackwardButton.setSelection(false);
+        mKmlBackwardButton.addSelectionListener(mDirectionButtonAdapter);
+        mKmlForwardButton = new Button(mKmlPlayControls, SWT.TOGGLE | SWT.FLAT);
+        mKmlForwardButton.setImage(loader.loadImage("forward.png", mParent.getDisplay())); //$NON-NLS-1$
+        mKmlForwardButton.setSelection(true);
+        mKmlForwardButton.addSelectionListener(mDirectionButtonAdapter);
+
+        mKmlSpeedButton = new Button(mKmlPlayControls, SWT.PUSH | SWT.FLAT);
+
+        mSpeedIndex = 0;
+        mSpeed = PLAY_SPEEDS[mSpeedIndex];
+
+        mKmlSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed));
+        mKmlSpeedButton.addSelectionListener(mSpeedButtonAdapter);
+
+        mPlayKmlButton.setEnabled(false);
+        mKmlBackwardButton.setEnabled(false);
+        mKmlForwardButton.setEnabled(false);
+        mKmlSpeedButton.setEnabled(false);
+    }
+
+    /**
+     * Sets the focus to the proper control inside the panel.
+     */
+    @Override
+    public void setFocus() {
+    }
+
+    @Override
+    protected void postCreation() {
+        // pass
+    }
+
+    private synchronized void setDataMode(int selectionIndex) {
+        if (mEmulatorConsole != null) {
+            processCommandResult(mEmulatorConsole.setGsmDataMode(
+                    GsmMode.getEnum(GSM_MODES[selectionIndex][1])));
+        }
+    }
+
+    private synchronized void setVoiceMode(int selectionIndex) {
+        if (mEmulatorConsole != null) {
+            processCommandResult(mEmulatorConsole.setGsmVoiceMode(
+                    GsmMode.getEnum(GSM_MODES[selectionIndex][1])));
+        }
+    }
+
+    private synchronized void setNetworkLatency(int selectionIndex) {
+        if (mEmulatorConsole != null) {
+            processCommandResult(mEmulatorConsole.setNetworkLatency(selectionIndex));
+        }
+    }
+
+    private synchronized void setNetworkSpeed(int selectionIndex) {
+        if (mEmulatorConsole != null) {
+            processCommandResult(mEmulatorConsole.setNetworkSpeed(selectionIndex));
+        }
+    }
+
+
+    /**
+     * Callback on device selection change.
+     * @param device the new selected device
+     */
+    public void handleNewDevice(IDevice device) {
+        if (mParent.isDisposed()) {
+            return;
+        }
+        // unlink to previous console.
+        synchronized (this) {
+            mEmulatorConsole = null;
+        }
+
+        try {
+            // get the emulator console for this device
+            // First we need the device itself
+            if (device != null) {
+                GsmStatus gsm = null;
+                NetworkStatus netstatus = null;
+
+                synchronized (this) {
+                    mEmulatorConsole = EmulatorConsole.getConsole(device);
+                    if (mEmulatorConsole != null) {
+                        // get the gsm status
+                        gsm = mEmulatorConsole.getGsmStatus();
+                        netstatus = mEmulatorConsole.getNetworkStatus();
+
+                        if (gsm == null || netstatus == null) {
+                            mEmulatorConsole = null;
+                        }
+                    }
+                }
+
+                if (gsm != null && netstatus != null) {
+                    Display d = mParent.getDisplay();
+                    if (d.isDisposed() == false) {
+                        final GsmStatus f_gsm = gsm;
+                        final NetworkStatus f_netstatus = netstatus;
+
+                        d.asyncExec(new Runnable() {
+                            @Override
+                            public void run() {
+                                if (f_gsm.voice != GsmMode.UNKNOWN) {
+                                    mVoiceMode.select(getGsmComboIndex(f_gsm.voice));
+                                } else {
+                                    mVoiceMode.clearSelection();
+                                }
+                                if (f_gsm.data != GsmMode.UNKNOWN) {
+                                    mDataMode.select(getGsmComboIndex(f_gsm.data));
+                                } else {
+                                    mDataMode.clearSelection();
+                                }
+
+                                if (f_netstatus.speed != -1) {
+                                    mNetworkSpeed.select(f_netstatus.speed);
+                                } else {
+                                    mNetworkSpeed.clearSelection();
+                                }
+
+                                if (f_netstatus.latency != -1) {
+                                    mNetworkLatency.select(f_netstatus.latency);
+                                } else {
+                                    mNetworkLatency.clearSelection();
+                                }
+                            }
+                        });
+                    }
+                }
+            }
+        } finally {
+            // enable/disable the ui
+            boolean enable = false;
+            synchronized (this) {
+                enable = mEmulatorConsole != null;
+            }
+
+            enable(enable);
+        }
+    }
+
+    /**
+     * Enable or disable the ui. Can be called from non ui threads.
+     * @param enabled
+     */
+    private void enable(final boolean enabled) {
+        try {
+            Display d = mParent.getDisplay();
+            d.asyncExec(new Runnable() {
+                @Override
+                public void run() {
+                    if (mParent.isDisposed() == false) {
+                        doEnable(enabled);
+                    }
+                }
+            });
+        } catch (SWTException e) {
+            // disposed. do nothing
+        }
+    }
+
+    private boolean isValidPhoneNumber() {
+        String number = mPhoneNumber.getText().trim();
+
+        return number.matches(RE_PHONE_NUMBER);
+    }
+
+    /**
+     * Enable or disable the ui. Cannot be called from non ui threads.
+     * @param enabled
+     */
+    protected void doEnable(boolean enabled) {
+        mVoiceLabel.setEnabled(enabled);
+        mVoiceMode.setEnabled(enabled);
+
+        mDataLabel.setEnabled(enabled);
+        mDataMode.setEnabled(enabled);
+
+        mSpeedLabel.setEnabled(enabled);
+        mNetworkSpeed.setEnabled(enabled);
+
+        mLatencyLabel.setEnabled(enabled);
+        mNetworkLatency.setEnabled(enabled);
+
+        // Calling setEnabled on a text field will trigger a modifyText event, so we don't do it
+        // if we don't need to.
+        if (mPhoneNumber.isEnabled() != enabled) {
+            mNumberLabel.setEnabled(enabled);
+            mPhoneNumber.setEnabled(enabled);
+        }
+
+        boolean valid = isValidPhoneNumber();
+
+        mVoiceButton.setEnabled(enabled && valid);
+        mSmsButton.setEnabled(enabled && valid);
+
+        boolean smsValid = enabled && valid && mSmsButton.getSelection();
+
+        // Calling setEnabled on a text field will trigger a modifyText event, so we don't do it
+        // if we don't need to.
+        if (mSmsMessage.isEnabled() != smsValid) {
+            mMessageLabel.setEnabled(smsValid);
+            mSmsMessage.setEnabled(smsValid);
+        }
+        if (enabled == false) {
+            mSmsMessage.setText(""); //$NON-NLs-1$
+        }
+
+        mCallButton.setEnabled(enabled && valid);
+        mCancelButton.setEnabled(enabled && valid && mVoiceButton.getSelection());
+
+        if (enabled == false) {
+            mVoiceMode.clearSelection();
+            mDataMode.clearSelection();
+            mNetworkSpeed.clearSelection();
+            mNetworkLatency.clearSelection();
+            if (mPhoneNumber.getText().length() > 0) {
+                mPhoneNumber.setText(""); //$NON-NLS-1$
+            }
+        }
+
+        // location controls
+        mLocationFolders.setEnabled(enabled);
+
+        mDecimalButton.setEnabled(enabled);
+        mSexagesimalButton.setEnabled(enabled);
+        mLongitudeControls.setEnabled(enabled);
+        mLatitudeControls.setEnabled(enabled);
+
+        mGpxUploadButton.setEnabled(enabled);
+        mGpxWayPointTable.setEnabled(enabled);
+        mGpxTrackTable.setEnabled(enabled);
+        mKmlUploadButton.setEnabled(enabled);
+        mKmlWayPointTable.setEnabled(enabled);
+    }
+
+    /**
+     * Returns the index of the combo item matching a specific GsmMode.
+     * @param mode
+     */
+    private int getGsmComboIndex(GsmMode mode) {
+        for (int i = 0 ; i < GSM_MODES.length; i++) {
+            String[] modes = GSM_MODES[i];
+            if (mode.getTag().equals(modes[1])) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Processes the result of a command sent to the console.
+     * @param result the result of the command.
+     */
+    private boolean processCommandResult(final String result) {
+        if (result != EmulatorConsole.RESULT_OK) {
+            try {
+                mParent.getDisplay().asyncExec(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (mParent.isDisposed() == false) {
+                            MessageDialog.openError(mParent.getShell(), "Emulator Console",
+                                    result);
+                        }
+                    }
+                });
+            } catch (SWTException e) {
+                // we're quitting, just ignore
+            }
+
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * @param track
+     */
+    private void playTrack(final Track track) {
+        // no need to synchronize this check, the worst that can happen, is we start the thread
+        // for nothing.
+        if (mEmulatorConsole != null) {
+            mPlayGpxButton.setImage(mPauseImage);
+            mPlayKmlButton.setImage(mPauseImage);
+            mPlayingTrack = true;
+
+            mPlayingThread = new Thread() {
+                @Override
+                public void run() {
+                    try {
+                        TrackPoint[] trackPoints = track.getPoints();
+                        int count = trackPoints.length;
+
+                        // get the start index.
+                        int start = 0;
+                        if (mPlayDirection == -1) {
+                            start = count - 1;
+                        }
+
+                        for (int p = start; p >= 0 && p < count; p += mPlayDirection) {
+                            if (mPlayingTrack == false) {
+                                return;
+                            }
+
+                            // get the current point and send its location to
+                            // the emulator.
+                            final TrackPoint trackPoint = trackPoints[p];
+
+                            synchronized (EmulatorControlPanel.this) {
+                                if (mEmulatorConsole == null ||
+                                        processCommandResult(mEmulatorConsole.sendLocation(
+                                                trackPoint.getLongitude(), trackPoint.getLatitude(),
+                                                trackPoint.getElevation())) == false) {
+                                    return;
+                                }
+                            }
+
+                            // if this is not the final point, then get the next one and
+                            // compute the delta time
+                            int nextIndex = p + mPlayDirection;
+                            if (nextIndex >=0 && nextIndex < count) {
+                                TrackPoint nextPoint = trackPoints[nextIndex];
+
+                                long delta = nextPoint.getTime() - trackPoint.getTime();
+                                if (delta < 0) {
+                                    delta = -delta;
+                                }
+
+                                long startTime = System.currentTimeMillis();
+
+                                try {
+                                    sleep(delta / mSpeed);
+                                } catch (InterruptedException e) {
+                                    if (mPlayingTrack == false) {
+                                        return;
+                                    }
+
+                                    // we got interrupted, lets make sure we can play
+                                    do {
+                                        long waited = System.currentTimeMillis() - startTime;
+                                        long needToWait = delta / mSpeed;
+                                        if (waited < needToWait) {
+                                            try {
+                                                sleep(needToWait - waited);
+                                            } catch (InterruptedException e1) {
+                                                // we'll just loop and wait again if needed.
+                                                // unless we're supposed to stop
+                                                if (mPlayingTrack == false) {
+                                                    return;
+                                                }
+                                            }
+                                        } else {
+                                            break;
+                                        }
+                                    } while (true);
+                                }
+                            }
+                        }
+                    } finally {
+                        mPlayingTrack = false;
+                        try {
+                            mParent.getDisplay().asyncExec(new Runnable() {
+                                @Override
+                                public void run() {
+                                    if (mPlayGpxButton.isDisposed() == false) {
+                                        mPlayGpxButton.setImage(mPlayImage);
+                                        mPlayKmlButton.setImage(mPlayImage);
+                                    }
+                                }
+                            });
+                        } catch (SWTException e) {
+                            // we're quitting, just ignore
+                        }
+                    }
+                }
+            };
+
+            mPlayingThread.start();
+        }
+    }
+
+    private void playKml(final WayPoint[] trackPoints) {
+        // no need to synchronize this check, the worst that can happen, is we start the thread
+        // for nothing.
+        if (mEmulatorConsole != null) {
+            mPlayGpxButton.setImage(mPauseImage);
+            mPlayKmlButton.setImage(mPauseImage);
+            mPlayingTrack = true;
+
+            mPlayingThread = new Thread() {
+                @Override
+                public void run() {
+                    try {
+                        int count = trackPoints.length;
+
+                        // get the start index.
+                        int start = 0;
+                        if (mPlayDirection == -1) {
+                            start = count - 1;
+                        }
+
+                        for (int p = start; p >= 0 && p < count; p += mPlayDirection) {
+                            if (mPlayingTrack == false) {
+                                return;
+                            }
+
+                            // get the current point and send its location to
+                            // the emulator.
+                            WayPoint trackPoint = trackPoints[p];
+
+                            synchronized (EmulatorControlPanel.this) {
+                                if (mEmulatorConsole == null ||
+                                        processCommandResult(mEmulatorConsole.sendLocation(
+                                                trackPoint.getLongitude(), trackPoint.getLatitude(),
+                                                trackPoint.getElevation())) == false) {
+                                    return;
+                                }
+                            }
+
+                            // if this is not the final point, then get the next one and
+                            // compute the delta time
+                            int nextIndex = p + mPlayDirection;
+                            if (nextIndex >=0 && nextIndex < count) {
+
+                                long delta = 1000; // 1 second
+                                if (delta < 0) {
+                                    delta = -delta;
+                                }
+
+                                long startTime = System.currentTimeMillis();
+
+                                try {
+                                    sleep(delta / mSpeed);
+                                } catch (InterruptedException e) {
+                                    if (mPlayingTrack == false) {
+                                        return;
+                                    }
+
+                                    // we got interrupted, lets make sure we can play
+                                    do {
+                                        long waited = System.currentTimeMillis() - startTime;
+                                        long needToWait = delta / mSpeed;
+                                        if (waited < needToWait) {
+                                            try {
+                                                sleep(needToWait - waited);
+                                            } catch (InterruptedException e1) {
+                                                // we'll just loop and wait again if needed.
+                                                // unless we're supposed to stop
+                                                if (mPlayingTrack == false) {
+                                                    return;
+                                                }
+                                            }
+                                        } else {
+                                            break;
+                                        }
+                                    } while (true);
+                                }
+                            }
+                        }
+                    } finally {
+                        mPlayingTrack = false;
+                        try {
+                            mParent.getDisplay().asyncExec(new Runnable() {
+                                @Override
+                                public void run() {
+                                    if (mPlayGpxButton.isDisposed() == false) {
+                                        mPlayGpxButton.setImage(mPlayImage);
+                                        mPlayKmlButton.setImage(mPlayImage);
+                                    }
+                                }
+                            });
+                        } catch (SWTException e) {
+                            // we're quitting, just ignore
+                        }
+                    }
+                }
+            };
+
+            mPlayingThread.start();
+        }
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/FindDialog.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/FindDialog.java
new file mode 100644
index 0000000..fe3f438
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/FindDialog.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2012 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.ddmuilib;
+
+
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+/**
+ * {@link FindDialog} provides a text box where users can enter text that should be
+ * searched for in the target editor/view. The buttons "Find Previous" and "Find Next"
+ * allow users to search forwards/backwards. This dialog simply provides a front end for the user
+ * and the actual task of searching is delegated to the {@link IFindTarget}.
+ */
+public class FindDialog extends Dialog {
+    private Label mStatusLabel;
+    private Button mFindNext;
+    private Button mFindPrevious;
+    private final IFindTarget mTarget;
+    private Text mSearchText;
+    private String mPreviousSearchText;
+    private final int mDefaultButtonId;
+
+    /** Id of the "Find Next" button */
+    public static final int FIND_NEXT_ID = IDialogConstants.CLIENT_ID;
+
+    /** Id of the "Find Previous button */
+    public static final int FIND_PREVIOUS_ID = IDialogConstants.CLIENT_ID + 1;
+
+    public FindDialog(Shell shell, IFindTarget target) {
+        this(shell, target, FIND_PREVIOUS_ID);
+    }
+
+    /**
+     * Construct a find dialog.
+     * @param shell shell to use
+     * @param target delegate to be invoked on user action
+     * @param defaultButtonId one of {@code #FIND_NEXT_ID} or {@code #FIND_PREVIOUS_ID}.
+     */
+    public FindDialog(Shell shell, IFindTarget target, int defaultButtonId) {
+        super(shell);
+
+        mTarget = target;
+        mDefaultButtonId = defaultButtonId;
+
+        setShellStyle((getShellStyle() & ~SWT.APPLICATION_MODAL) | SWT.MODELESS);
+        setBlockOnOpen(true);
+    }
+
+    @Override
+    protected Control createDialogArea(Composite parent) {
+        Composite panel = new Composite(parent, SWT.NONE);
+        panel.setLayout(new GridLayout(2, false));
+        panel.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        Label lblMessage = new Label(panel, SWT.NONE);
+        lblMessage.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
+        lblMessage.setText("Find:");
+
+        mSearchText = new Text(panel, SWT.BORDER);
+        mSearchText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+        mSearchText.addModifyListener(new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent e) {
+                boolean hasText = !mSearchText.getText().trim().isEmpty();
+                mFindNext.setEnabled(hasText);
+                mFindPrevious.setEnabled(hasText);
+            }
+        });
+
+        mStatusLabel = new Label(panel, SWT.NONE);
+        mStatusLabel.setForeground(getShell().getDisplay().getSystemColor(SWT.COLOR_DARK_RED));
+        GridData gd = new GridData();
+        gd.horizontalSpan = 2;
+        gd.grabExcessHorizontalSpace = true;
+        mStatusLabel.setLayoutData(gd);
+
+        return panel;
+    }
+
+    @Override
+    protected void createButtonsForButtonBar(Composite parent) {
+        createButton(parent, IDialogConstants.CLOSE_ID, IDialogConstants.CLOSE_LABEL, false);
+
+        mFindNext = createButton(parent, FIND_NEXT_ID, "Find Next",
+                mDefaultButtonId == FIND_NEXT_ID);
+        mFindPrevious = createButton(parent, FIND_PREVIOUS_ID, "Find Previous",
+                mDefaultButtonId != FIND_NEXT_ID);
+        mFindNext.setEnabled(false);
+        mFindPrevious.setEnabled(false);
+    }
+
+    @Override
+    protected void buttonPressed(int buttonId) {
+        if (buttonId == IDialogConstants.CLOSE_ID) {
+            close();
+            return;
+        }
+
+        if (buttonId == FIND_PREVIOUS_ID || buttonId == FIND_NEXT_ID) {
+            if (mTarget != null) {
+                String searchText = mSearchText.getText();
+                boolean newSearch = !searchText.equals(mPreviousSearchText);
+                mPreviousSearchText = searchText;
+                boolean searchForward = buttonId == FIND_NEXT_ID;
+
+                boolean hasMatches = mTarget.findAndSelect(searchText, newSearch, searchForward);
+                if (!hasMatches) {
+                    mStatusLabel.setText("String not found");
+                    mStatusLabel.pack();
+                } else {
+                    mStatusLabel.setText("");
+                }
+            }
+        }
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/HeapPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/HeapPanel.java
new file mode 100644
index 0000000..d0af8b0
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/HeapPanel.java
@@ -0,0 +1,1310 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib;
+
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData;
+import com.android.ddmlib.HeapSegment.HeapSegmentElement;
+import com.android.ddmlib.Log;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.custom.StackLayout;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.PaletteData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.TableItem;
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.axis.CategoryAxis;
+import org.jfree.chart.axis.CategoryLabelPositions;
+import org.jfree.chart.labels.CategoryToolTipGenerator;
+import org.jfree.chart.plot.CategoryPlot;
+import org.jfree.chart.plot.Plot;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.renderer.category.CategoryItemRenderer;
+import org.jfree.chart.title.TextTitle;
+import org.jfree.data.category.CategoryDataset;
+import org.jfree.data.category.DefaultCategoryDataset;
+import org.jfree.experimental.chart.swt.ChartComposite;
+import org.jfree.experimental.swt.SWTUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+
+/**
+ * Base class for our information panels.
+ */
+public final class HeapPanel extends BaseHeapPanel {
+    private static final String PREFS_STATS_COL_TYPE = "heapPanel.col0"; //$NON-NLS-1$
+    private static final String PREFS_STATS_COL_COUNT = "heapPanel.col1"; //$NON-NLS-1$
+    private static final String PREFS_STATS_COL_SIZE = "heapPanel.col2"; //$NON-NLS-1$
+    private static final String PREFS_STATS_COL_SMALLEST = "heapPanel.col3"; //$NON-NLS-1$
+    private static final String PREFS_STATS_COL_LARGEST = "heapPanel.col4"; //$NON-NLS-1$
+    private static final String PREFS_STATS_COL_MEDIAN = "heapPanel.col5"; //$NON-NLS-1$
+    private static final String PREFS_STATS_COL_AVERAGE = "heapPanel.col6"; //$NON-NLS-1$
+
+    /* args to setUpdateStatus() */
+    private static final int NOT_SELECTED   = 0;
+    private static final int NOT_ENABLED    = 1;
+    private static final int ENABLED        = 2;
+
+    /** color palette and map legend. NATIVE is the last enum is a 0 based enum list, so we need
+     * Native+1 at least. We also need 2 more entries for free area and expansion area.  */
+    private static final int NUM_PALETTE_ENTRIES = HeapSegmentElement.KIND_NATIVE+2 +1;
+    private static final String[] mMapLegend = new String[NUM_PALETTE_ENTRIES];
+    private static final PaletteData mMapPalette = createPalette();
+
+    private static final boolean DISPLAY_HEAP_BITMAP = false;
+    private static final boolean DISPLAY_HILBERT_BITMAP = false;
+
+    private static final int PLACEHOLDER_HILBERT_SIZE = 200;
+    private static final int PLACEHOLDER_LINEAR_V_SIZE = 100;
+    private static final int PLACEHOLDER_LINEAR_H_SIZE = 300;
+
+    private static final int[] ZOOMS = {100, 50, 25};
+
+    private static final NumberFormat sByteFormatter = NumberFormat.getInstance();
+    private static final NumberFormat sLargeByteFormatter = NumberFormat.getInstance();
+    private static final NumberFormat sCountFormatter = NumberFormat.getInstance();
+
+    static {
+        sByteFormatter.setMinimumFractionDigits(0);
+        sByteFormatter.setMaximumFractionDigits(1);
+        sLargeByteFormatter.setMinimumFractionDigits(3);
+        sLargeByteFormatter.setMaximumFractionDigits(3);
+
+        sCountFormatter.setGroupingUsed(true);
+    }
+
+    private Display mDisplay;
+
+    private Composite mTop; // real top
+    private Label mUpdateStatus;
+    private Table mHeapSummary;
+    private Combo mDisplayMode;
+
+    //private ScrolledComposite mScrolledComposite;
+
+    private Composite mDisplayBase; // base of the displays.
+    private StackLayout mDisplayStack;
+
+    private Composite mStatisticsBase;
+    private Table mStatisticsTable;
+    private JFreeChart mChart;
+    private ChartComposite mChartComposite;
+    private Button mGcButton;
+    private DefaultCategoryDataset mAllocCountDataSet;
+
+    private Composite mLinearBase;
+    private Label mLinearHeapImage;
+
+    private Composite mHilbertBase;
+    private Label mHilbertHeapImage;
+    private Group mLegend;
+    private Combo mZoom;
+
+    /** Image used for the hilbert display. Since we recreate a new image every time, we
+     * keep this one around to dispose it. */
+    private Image mHilbertImage;
+    private Image mLinearImage;
+    private Composite[] mLayout;
+
+    /*
+     * Create color palette for map.  Set up titles for legend.
+     */
+    private static PaletteData createPalette() {
+        RGB colors[] = new RGB[NUM_PALETTE_ENTRIES];
+        colors[0]
+                = new RGB(192, 192, 192); // non-heap pixels are gray
+        mMapLegend[0]
+                = "(heap expansion area)";
+
+        colors[1]
+                = new RGB(0, 0, 0);       // free chunks are black
+        mMapLegend[1]
+                = "free";
+
+        colors[HeapSegmentElement.KIND_OBJECT + 2]
+                = new RGB(0, 0, 255);     // objects are blue
+        mMapLegend[HeapSegmentElement.KIND_OBJECT + 2]
+                = "data object";
+
+        colors[HeapSegmentElement.KIND_CLASS_OBJECT + 2]
+                = new RGB(0, 255, 0);     // class objects are green
+        mMapLegend[HeapSegmentElement.KIND_CLASS_OBJECT + 2]
+                = "class object";
+
+        colors[HeapSegmentElement.KIND_ARRAY_1 + 2]
+                = new RGB(255, 0, 0);     // byte/bool arrays are red
+        mMapLegend[HeapSegmentElement.KIND_ARRAY_1 + 2]
+                = "1-byte array (byte[], boolean[])";
+
+        colors[HeapSegmentElement.KIND_ARRAY_2 + 2]
+                = new RGB(255, 128, 0);   // short/char arrays are orange
+        mMapLegend[HeapSegmentElement.KIND_ARRAY_2 + 2]
+                = "2-byte array (short[], char[])";
+
+        colors[HeapSegmentElement.KIND_ARRAY_4 + 2]
+                = new RGB(255, 255, 0);   // obj/int/float arrays are yellow
+        mMapLegend[HeapSegmentElement.KIND_ARRAY_4 + 2]
+                = "4-byte array (object[], int[], float[])";
+
+        colors[HeapSegmentElement.KIND_ARRAY_8 + 2]
+                = new RGB(255, 128, 128); // long/double arrays are pink
+        mMapLegend[HeapSegmentElement.KIND_ARRAY_8 + 2]
+                = "8-byte array (long[], double[])";
+
+        colors[HeapSegmentElement.KIND_UNKNOWN + 2]
+                = new RGB(255, 0, 255);   // unknown objects are cyan
+        mMapLegend[HeapSegmentElement.KIND_UNKNOWN + 2]
+                = "unknown object";
+
+        colors[HeapSegmentElement.KIND_NATIVE + 2]
+                = new RGB(64, 64, 64);    // native objects are dark gray
+        mMapLegend[HeapSegmentElement.KIND_NATIVE + 2]
+                = "non-Java object";
+
+        return new PaletteData(colors);
+    }
+
+    /**
+     * Sent when an existing client information changed.
+     * <p/>
+     * This is sent from a non UI thread.
+     * @param client the updated client.
+     * @param changeMask the bit mask describing the changed properties. It can contain
+     * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME}
+     * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE},
+     * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
+     * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
+     *
+     * @see IClientChangeListener#clientChanged(Client, int)
+     */
+    @Override
+    public void clientChanged(final Client client, int changeMask) {
+        if (client == getCurrentClient()) {
+            if ((changeMask & Client.CHANGE_HEAP_MODE) == Client.CHANGE_HEAP_MODE ||
+                    (changeMask & Client.CHANGE_HEAP_DATA) == Client.CHANGE_HEAP_DATA) {
+                try {
+                    mTop.getDisplay().asyncExec(new Runnable() {
+                        @Override
+                        public void run() {
+                            clientSelected();
+                        }
+                    });
+                } catch (SWTException e) {
+                    // display is disposed (app is quitting most likely), we do nothing.
+                }
+            }
+        }
+    }
+
+    /**
+     * Sent when a new device is selected. The new device can be accessed
+     * with {@link #getCurrentDevice()}
+     */
+    @Override
+    public void deviceSelected() {
+        // pass
+    }
+
+    /**
+     * Sent when a new client is selected. The new client can be accessed
+     * with {@link #getCurrentClient()}.
+     */
+    @Override
+    public void clientSelected() {
+        if (mTop.isDisposed())
+            return;
+
+        Client client = getCurrentClient();
+
+        Log.d("ddms", "HeapPanel: changed " + client);
+
+        if (client != null) {
+            ClientData cd = client.getClientData();
+
+            if (client.isHeapUpdateEnabled()) {
+                mGcButton.setEnabled(true);
+                mDisplayMode.setEnabled(true);
+                setUpdateStatus(ENABLED);
+            } else {
+                setUpdateStatus(NOT_ENABLED);
+                mGcButton.setEnabled(false);
+                mDisplayMode.setEnabled(false);
+            }
+
+            fillSummaryTable(cd);
+
+            int mode = mDisplayMode.getSelectionIndex();
+            if (mode == 0) {
+                fillDetailedTable(client, false /* forceRedraw */);
+            } else {
+                if (DISPLAY_HEAP_BITMAP) {
+                    renderHeapData(cd, mode - 1, false /* forceRedraw */);
+                }
+            }
+        } else {
+            mGcButton.setEnabled(false);
+            mDisplayMode.setEnabled(false);
+            fillSummaryTable(null);
+            fillDetailedTable(null, true);
+            setUpdateStatus(NOT_SELECTED);
+        }
+
+        // sizes of things change frequently, so redo layout
+        //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT,
+        //        SWT.DEFAULT));
+        mDisplayBase.layout();
+        //mScrolledComposite.redraw();
+    }
+
+    /**
+     * Create our control(s).
+     */
+    @Override
+    protected Control createControl(Composite parent) {
+        mDisplay = parent.getDisplay();
+
+        GridLayout gl;
+
+        mTop = new Composite(parent, SWT.NONE);
+        mTop.setLayout(new GridLayout(1, false));
+        mTop.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        mUpdateStatus = new Label(mTop, SWT.NONE);
+        setUpdateStatus(NOT_SELECTED);
+
+        Composite summarySection = new Composite(mTop, SWT.NONE);
+        summarySection.setLayout(gl = new GridLayout(2, false));
+        gl.marginHeight = gl.marginWidth = 0;
+
+        mHeapSummary = createSummaryTable(summarySection);
+        mGcButton = new Button(summarySection, SWT.PUSH);
+        mGcButton.setText("Cause GC");
+        mGcButton.setEnabled(false);
+        mGcButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                Client client = getCurrentClient();
+                if (client != null) {
+                    client.executeGarbageCollector();
+                }
+            }
+        });
+
+        Composite comboSection = new Composite(mTop, SWT.NONE);
+        gl = new GridLayout(2, false);
+        gl.marginHeight = gl.marginWidth = 0;
+        comboSection.setLayout(gl);
+
+        Label displayLabel = new Label(comboSection, SWT.NONE);
+        displayLabel.setText("Display: ");
+
+        mDisplayMode = new Combo(comboSection, SWT.READ_ONLY);
+        mDisplayMode.setEnabled(false);
+        mDisplayMode.add("Stats");
+        if (DISPLAY_HEAP_BITMAP) {
+            mDisplayMode.add("Linear");
+            if (DISPLAY_HILBERT_BITMAP) {
+                mDisplayMode.add("Hilbert");
+            }
+        }
+
+        // the base of the displays.
+        mDisplayBase = new Composite(mTop, SWT.NONE);
+        mDisplayBase.setLayoutData(new GridData(GridData.FILL_BOTH));
+        mDisplayStack = new StackLayout();
+        mDisplayBase.setLayout(mDisplayStack);
+
+        // create the statistics display
+        mStatisticsBase = new Composite(mDisplayBase, SWT.NONE);
+        //mStatisticsBase.setLayoutData(new GridData(GridData.FILL_BOTH));
+        mStatisticsBase.setLayout(gl = new GridLayout(1, false));
+        gl.marginHeight = gl.marginWidth = 0;
+        mDisplayStack.topControl = mStatisticsBase;
+
+        mStatisticsTable = createDetailedTable(mStatisticsBase);
+        mStatisticsTable.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        createChart();
+
+        //create the linear composite
+        mLinearBase = new Composite(mDisplayBase, SWT.NONE);
+        //mLinearBase.setLayoutData(new GridData());
+        gl = new GridLayout(1, false);
+        gl.marginHeight = gl.marginWidth = 0;
+        mLinearBase.setLayout(gl);
+
+        {
+            mLinearHeapImage = new Label(mLinearBase, SWT.NONE);
+            mLinearHeapImage.setLayoutData(new GridData());
+            mLinearHeapImage.setImage(ImageLoader.createPlaceHolderArt(mDisplay,
+                    PLACEHOLDER_LINEAR_H_SIZE, PLACEHOLDER_LINEAR_V_SIZE,
+                    mDisplay.getSystemColor(SWT.COLOR_BLUE)));
+
+            // create a composite to contain the bottom part (legend)
+            Composite bottomSection = new Composite(mLinearBase, SWT.NONE);
+            gl = new GridLayout(1, false);
+            gl.marginHeight = gl.marginWidth = 0;
+            bottomSection.setLayout(gl);
+
+            createLegend(bottomSection);
+        }
+
+/*
+        mScrolledComposite = new ScrolledComposite(mTop, SWT.H_SCROLL | SWT.V_SCROLL);
+        mScrolledComposite.setLayoutData(new GridData(GridData.FILL_BOTH));
+        mScrolledComposite.setExpandHorizontal(true);
+        mScrolledComposite.setExpandVertical(true);
+        mScrolledComposite.setContent(mDisplayBase);
+*/
+
+
+        // create the hilbert display.
+        mHilbertBase = new Composite(mDisplayBase, SWT.NONE);
+        //mHilbertBase.setLayoutData(new GridData());
+        gl = new GridLayout(2, false);
+        gl.marginHeight = gl.marginWidth = 0;
+        mHilbertBase.setLayout(gl);
+
+        if (DISPLAY_HILBERT_BITMAP) {
+            mHilbertHeapImage = new Label(mHilbertBase, SWT.NONE);
+            mHilbertHeapImage.setLayoutData(new GridData());
+            mHilbertHeapImage.setImage(ImageLoader.createPlaceHolderArt(mDisplay,
+                    PLACEHOLDER_HILBERT_SIZE, PLACEHOLDER_HILBERT_SIZE,
+                    mDisplay.getSystemColor(SWT.COLOR_BLUE)));
+
+            // create a composite to contain the right part (legend + zoom)
+            Composite rightSection = new Composite(mHilbertBase, SWT.NONE);
+            gl = new GridLayout(1, false);
+            gl.marginHeight = gl.marginWidth = 0;
+            rightSection.setLayout(gl);
+
+            Composite zoomComposite = new Composite(rightSection, SWT.NONE);
+            gl = new GridLayout(2, false);
+            zoomComposite.setLayout(gl);
+
+            Label l = new Label(zoomComposite, SWT.NONE);
+            l.setText("Zoom:");
+            mZoom = new Combo(zoomComposite, SWT.READ_ONLY);
+            for (int z : ZOOMS) {
+                mZoom.add(String.format("%1$d%%", z)); //$NON-NLS-1$
+            }
+
+            mZoom.select(0);
+            mZoom.addSelectionListener(new SelectionAdapter() {
+                @Override
+                public void widgetSelected(SelectionEvent e) {
+                    setLegendText(mZoom.getSelectionIndex());
+                    Client client = getCurrentClient();
+                    if (client != null) {
+                        renderHeapData(client.getClientData(), 1, true);
+                        mTop.pack();
+                    }
+                }
+            });
+
+            createLegend(rightSection);
+        }
+        mHilbertBase.pack();
+
+        mLayout = new Composite[] { mStatisticsBase, mLinearBase, mHilbertBase };
+        mDisplayMode.select(0);
+        mDisplayMode.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                int index = mDisplayMode.getSelectionIndex();
+                Client client = getCurrentClient();
+
+                if (client != null) {
+                    if (index == 0) {
+                        fillDetailedTable(client, true /* forceRedraw */);
+                    } else {
+                        renderHeapData(client.getClientData(), index-1, true /* forceRedraw */);
+                    }
+                }
+
+                mDisplayStack.topControl = mLayout[index];
+                //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT,
+                //        SWT.DEFAULT));
+                mDisplayBase.layout();
+                //mScrolledComposite.redraw();
+            }
+        });
+
+        //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT,
+        //        SWT.DEFAULT));
+        mDisplayBase.layout();
+        //mScrolledComposite.redraw();
+
+        return mTop;
+    }
+
+    /**
+     * Sets the focus to the proper control inside the panel.
+     */
+    @Override
+    public void setFocus() {
+        mHeapSummary.setFocus();
+    }
+
+
+    private Table createSummaryTable(Composite base) {
+        Table tab = new Table(base, SWT.SINGLE | SWT.FULL_SELECTION);
+        tab.setHeaderVisible(true);
+        tab.setLinesVisible(true);
+
+        TableColumn col;
+
+        col = new TableColumn(tab, SWT.RIGHT);
+        col.setText("ID");
+        col.pack();
+
+        col = new TableColumn(tab, SWT.RIGHT);
+        col.setText("000.000WW"); //$NON-NLS-1$
+        col.pack();
+        col.setText("Heap Size");
+
+        col = new TableColumn(tab, SWT.RIGHT);
+        col.setText("000.000WW"); //$NON-NLS-1$
+        col.pack();
+        col.setText("Allocated");
+
+        col = new TableColumn(tab, SWT.RIGHT);
+        col.setText("000.000WW"); //$NON-NLS-1$
+        col.pack();
+        col.setText("Free");
+
+        col = new TableColumn(tab, SWT.RIGHT);
+        col.setText("000.00%"); //$NON-NLS-1$
+        col.pack();
+        col.setText("% Used");
+
+        col = new TableColumn(tab, SWT.RIGHT);
+        col.setText("000,000,000"); //$NON-NLS-1$
+        col.pack();
+        col.setText("# Objects");
+
+        // make sure there is always one empty item so that one table row is always displayed.
+        TableItem item = new TableItem(tab, SWT.NONE);
+        item.setText("");
+
+        return tab;
+    }
+
+    private Table createDetailedTable(Composite base) {
+        IPreferenceStore store = DdmUiPreferences.getStore();
+
+        Table tab = new Table(base, SWT.SINGLE | SWT.FULL_SELECTION);
+        tab.setHeaderVisible(true);
+        tab.setLinesVisible(true);
+
+        TableHelper.createTableColumn(tab, "Type", SWT.LEFT,
+                "4-byte array (object[], int[], float[])", //$NON-NLS-1$
+                PREFS_STATS_COL_TYPE, store);
+
+        TableHelper.createTableColumn(tab, "Count", SWT.RIGHT,
+                "00,000", //$NON-NLS-1$
+                PREFS_STATS_COL_COUNT, store);
+
+        TableHelper.createTableColumn(tab, "Total Size", SWT.RIGHT,
+                "000.000 WW", //$NON-NLS-1$
+                PREFS_STATS_COL_SIZE, store);
+
+        TableHelper.createTableColumn(tab, "Smallest", SWT.RIGHT,
+                "000.000 WW", //$NON-NLS-1$
+                PREFS_STATS_COL_SMALLEST, store);
+
+        TableHelper.createTableColumn(tab, "Largest", SWT.RIGHT,
+                "000.000 WW", //$NON-NLS-1$
+                PREFS_STATS_COL_LARGEST, store);
+
+        TableHelper.createTableColumn(tab, "Median", SWT.RIGHT,
+                "000.000 WW", //$NON-NLS-1$
+                PREFS_STATS_COL_MEDIAN, store);
+
+        TableHelper.createTableColumn(tab, "Average", SWT.RIGHT,
+                "000.000 WW", //$NON-NLS-1$
+                PREFS_STATS_COL_AVERAGE, store);
+
+        tab.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+
+                Client client = getCurrentClient();
+                if (client != null) {
+                    int index = mStatisticsTable.getSelectionIndex();
+                    TableItem item = mStatisticsTable.getItem(index);
+
+                    if (item != null) {
+                        Map<Integer, ArrayList<HeapSegmentElement>> heapMap =
+                            client.getClientData().getVmHeapData().getProcessedHeapMap();
+
+                        ArrayList<HeapSegmentElement> list = heapMap.get(item.getData());
+                        if (list != null) {
+                            showChart(list);
+                        }
+                    }
+                }
+
+            }
+        });
+
+        return tab;
+    }
+
+    /**
+     * Creates the chart below the statistics table
+     */
+    private void createChart() {
+        mAllocCountDataSet = new DefaultCategoryDataset();
+        mChart = ChartFactory.createBarChart(null, "Size", "Count", mAllocCountDataSet,
+                PlotOrientation.VERTICAL, false, true, false);
+
+        // get the font to make a proper title. We need to convert the swt font,
+        // into an awt font.
+        Font f = mStatisticsBase.getFont();
+        FontData[] fData = f.getFontData();
+
+        // event though on Mac OS there could be more than one fontData, we'll only use
+        // the first one.
+        FontData firstFontData = fData[0];
+
+        java.awt.Font awtFont = SWTUtils.toAwtFont(mStatisticsBase.getDisplay(),
+                firstFontData, true /* ensureSameSize */);
+
+        mChart.setTitle(new TextTitle("Allocation count per size", awtFont));
+
+        Plot plot = mChart.getPlot();
+        if (plot instanceof CategoryPlot) {
+            // get the plot
+            CategoryPlot categoryPlot = (CategoryPlot)plot;
+
+            // set the domain axis to draw labels that are displayed even with many values.
+            CategoryAxis domainAxis = categoryPlot.getDomainAxis();
+            domainAxis.setCategoryLabelPositions(CategoryLabelPositions.DOWN_90);
+
+            CategoryItemRenderer renderer = categoryPlot.getRenderer();
+            renderer.setBaseToolTipGenerator(new CategoryToolTipGenerator() {
+                @Override
+                public String generateToolTip(CategoryDataset dataset, int row, int column) {
+                    // get the key for the size of the allocation
+                    ByteLong columnKey = (ByteLong)dataset.getColumnKey(column);
+                    String rowKey = (String)dataset.getRowKey(row);
+                    Number value = dataset.getValue(rowKey, columnKey);
+
+                    return String.format("%1$d %2$s of %3$d bytes", value.intValue(), rowKey,
+                            columnKey.getValue());
+                }
+            });
+        }
+        mChartComposite = new ChartComposite(mStatisticsBase, SWT.BORDER, mChart,
+                ChartComposite.DEFAULT_WIDTH,
+                ChartComposite.DEFAULT_HEIGHT,
+                ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH,
+                ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT,
+                3000, // max draw width. We don't want it to zoom, so we put a big number
+                3000, // max draw height. We don't want it to zoom, so we put a big number
+                true,  // off-screen buffer
+                true,  // properties
+                true,  // save
+                true,  // print
+                false,  // zoom
+                true);   // tooltips
+
+        mChartComposite.setLayoutData(new GridData(GridData.FILL_BOTH));
+    }
+
+    private static String prettyByteCount(long bytes) {
+        double fracBytes = bytes;
+        String units = " B";
+        if (fracBytes < 1024) {
+            return sByteFormatter.format(fracBytes) + units;
+        } else {
+            fracBytes /= 1024;
+            units = " KB";
+        }
+        if (fracBytes >= 1024) {
+            fracBytes /= 1024;
+            units = " MB";
+        }
+        if (fracBytes >= 1024) {
+            fracBytes /= 1024;
+            units = " GB";
+        }
+
+        return sLargeByteFormatter.format(fracBytes) + units;
+    }
+
+    private static String approximateByteCount(long bytes) {
+        double fracBytes = bytes;
+        String units = "";
+        if (fracBytes >= 1024) {
+            fracBytes /= 1024;
+            units = "K";
+        }
+        if (fracBytes >= 1024) {
+            fracBytes /= 1024;
+            units = "M";
+        }
+        if (fracBytes >= 1024) {
+            fracBytes /= 1024;
+            units = "G";
+        }
+
+        return sByteFormatter.format(fracBytes) + units;
+    }
+
+    private static String addCommasToNumber(long num) {
+        return sCountFormatter.format(num);
+    }
+
+    private static String fractionalPercent(long num, long denom) {
+        double val = (double)num / (double)denom;
+        val *= 100;
+
+        NumberFormat nf = NumberFormat.getInstance();
+        nf.setMinimumFractionDigits(2);
+        nf.setMaximumFractionDigits(2);
+        return nf.format(val) + "%";
+    }
+
+    private void fillSummaryTable(ClientData cd) {
+        if (mHeapSummary.isDisposed()) {
+            return;
+        }
+
+        mHeapSummary.setRedraw(false);
+        mHeapSummary.removeAll();
+
+        int numRows = 0;
+        if (cd != null) {
+            synchronized (cd) {
+                Iterator<Integer> iter = cd.getVmHeapIds();
+
+                while (iter.hasNext()) {
+                    numRows++;
+                    Integer id = iter.next();
+                    Map<String, Long> heapInfo = cd.getVmHeapInfo(id);
+                    if (heapInfo == null) {
+                        continue;
+                    }
+                    long sizeInBytes = heapInfo.get(ClientData.HEAP_SIZE_BYTES);
+                    long bytesAllocated = heapInfo.get(ClientData.HEAP_BYTES_ALLOCATED);
+                    long objectsAllocated = heapInfo.get(ClientData.HEAP_OBJECTS_ALLOCATED);
+
+                    TableItem item = new TableItem(mHeapSummary, SWT.NONE);
+                    item.setText(0, id.toString());
+
+                    item.setText(1, prettyByteCount(sizeInBytes));
+                    item.setText(2, prettyByteCount(bytesAllocated));
+                    item.setText(3, prettyByteCount(sizeInBytes - bytesAllocated));
+                    item.setText(4, fractionalPercent(bytesAllocated, sizeInBytes));
+                    item.setText(5, addCommasToNumber(objectsAllocated));
+                }
+            }
+        }
+
+        if (numRows == 0) {
+            // make sure there is always one empty item so that one table row is always displayed.
+            TableItem item = new TableItem(mHeapSummary, SWT.NONE);
+            item.setText("");
+        }
+
+        mHeapSummary.pack();
+        mHeapSummary.setRedraw(true);
+    }
+
+    private void fillDetailedTable(Client client, boolean forceRedraw) {
+        // first check if the client is invalid or heap updates are not enabled.
+        if (client == null || client.isHeapUpdateEnabled() == false) {
+            mStatisticsTable.removeAll();
+            showChart(null);
+            return;
+        }
+
+        ClientData cd = client.getClientData();
+
+        Map<Integer, ArrayList<HeapSegmentElement>> heapMap;
+
+        // Atomically get and clear the heap data.
+        synchronized (cd) {
+            if (serializeHeapData(cd.getVmHeapData()) == false && forceRedraw == false) {
+                // no change, we return.
+                return;
+            }
+
+            heapMap = cd.getVmHeapData().getProcessedHeapMap();
+        }
+
+        // we have new data, lets display it.
+
+        // First, get the current selection, and its key.
+        int index = mStatisticsTable.getSelectionIndex();
+        Integer selectedKey = null;
+        if (index != -1) {
+            selectedKey = (Integer)mStatisticsTable.getItem(index).getData();
+        }
+
+        // disable redraws and remove all from the table.
+        mStatisticsTable.setRedraw(false);
+        mStatisticsTable.removeAll();
+
+        if (heapMap != null) {
+            int selectedIndex = -1;
+            ArrayList<HeapSegmentElement> selectedList = null;
+
+            // get the keys
+            Set<Integer> keys = heapMap.keySet();
+            int iter = 0; // use a manual iter int because Set<?> doesn't have an index
+            // based accessor.
+            for (Integer key : keys) {
+                ArrayList<HeapSegmentElement> list = heapMap.get(key);
+
+                // check if this is the key that is supposed to be selected
+                if (key.equals(selectedKey)) {
+                    selectedIndex = iter;
+                    selectedList = list;
+                }
+                iter++;
+
+                TableItem item = new TableItem(mStatisticsTable, SWT.NONE);
+                item.setData(key);
+
+                // get the type
+                item.setText(0, mMapLegend[key]);
+
+                // set the count, smallest, largest
+                int count = list.size();
+                item.setText(1, addCommasToNumber(count));
+
+                if (count > 0) {
+                    item.setText(3, prettyByteCount(list.get(0).getLength()));
+                    item.setText(4, prettyByteCount(list.get(count-1).getLength()));
+
+                    int median = count / 2;
+                    HeapSegmentElement element = list.get(median);
+                    long size = element.getLength();
+                    item.setText(5, prettyByteCount(size));
+
+                    long totalSize = 0;
+                    for (int i = 0 ; i < count; i++) {
+                        element = list.get(i);
+
+                        size = element.getLength();
+                        totalSize += size;
+                    }
+
+                    // set the average and total
+                    item.setText(2, prettyByteCount(totalSize));
+                    item.setText(6, prettyByteCount(totalSize / count));
+                }
+            }
+
+            mStatisticsTable.setRedraw(true);
+
+            if (selectedIndex != -1) {
+                mStatisticsTable.setSelection(selectedIndex);
+                showChart(selectedList);
+            } else {
+                showChart(null);
+            }
+        } else {
+            mStatisticsTable.setRedraw(true);
+        }
+    }
+
+    private static class ByteLong implements Comparable<ByteLong> {
+        private long mValue;
+
+        private ByteLong(long value) {
+            mValue = value;
+        }
+
+        public long getValue() {
+            return mValue;
+        }
+
+        @Override
+        public String toString() {
+            return approximateByteCount(mValue);
+        }
+
+        @Override
+        public int compareTo(ByteLong other) {
+            if (mValue != other.mValue) {
+                return mValue < other.mValue ? -1 : 1;
+            }
+            return 0;
+        }
+
+    }
+
+    /**
+     * Fills the chart with the content of the list of {@link HeapSegmentElement}.
+     */
+    private void showChart(ArrayList<HeapSegmentElement> list) {
+        mAllocCountDataSet.clear();
+
+        if (list != null) {
+            String rowKey = "Alloc Count";
+
+            long currentSize = -1;
+            int currentCount = 0;
+            for (HeapSegmentElement element : list) {
+                if (element.getLength() != currentSize) {
+                    if (currentSize != -1) {
+                        ByteLong columnKey = new ByteLong(currentSize);
+                        mAllocCountDataSet.addValue(currentCount, rowKey, columnKey);
+                    }
+
+                    currentSize = element.getLength();
+                    currentCount = 1;
+                } else {
+                    currentCount++;
+                }
+            }
+
+            // add the last item
+            if (currentSize != -1) {
+                ByteLong columnKey = new ByteLong(currentSize);
+                mAllocCountDataSet.addValue(currentCount, rowKey, columnKey);
+            }
+        }
+    }
+
+    /*
+     * Add a color legend to the specified table.
+     */
+    private void createLegend(Composite parent) {
+        mLegend = new Group(parent, SWT.NONE);
+        mLegend.setText(getLegendText(0));
+
+        mLegend.setLayout(new GridLayout(2, false));
+
+        RGB[] colors = mMapPalette.colors;
+
+        for (int i = 0; i < NUM_PALETTE_ENTRIES; i++) {
+            Image tmpImage = createColorRect(parent.getDisplay(), colors[i]);
+
+            Label l = new Label(mLegend, SWT.NONE);
+            l.setImage(tmpImage);
+
+            l = new Label(mLegend, SWT.NONE);
+            l.setText(mMapLegend[i]);
+        }
+    }
+
+    private String getLegendText(int level) {
+        int bytes = 8 * (100 / ZOOMS[level]);
+
+        return String.format("Key (1 pixel = %1$d bytes)", bytes);
+    }
+
+    private void setLegendText(int level) {
+        mLegend.setText(getLegendText(level));
+
+    }
+
+    /*
+     * Create a nice rectangle in the specified color.
+     */
+    private Image createColorRect(Display display, RGB color) {
+        int width = 32;
+        int height = 16;
+
+        Image img = new Image(display, width, height);
+        GC gc = new GC(img);
+        gc.setBackground(new Color(display, color));
+        gc.fillRectangle(0, 0, width, height);
+        gc.dispose();
+        return img;
+    }
+
+
+    /*
+     * Are updates enabled?
+     */
+    private void setUpdateStatus(int status) {
+        switch (status) {
+            case NOT_SELECTED:
+                mUpdateStatus.setText("Select a client to see heap updates");
+                break;
+            case NOT_ENABLED:
+                mUpdateStatus.setText("Heap updates are " +
+                                      "NOT ENABLED for this client");
+                break;
+            case ENABLED:
+                mUpdateStatus.setText("Heap updates will happen after " +
+                                      "every GC for this client");
+                break;
+            default:
+                throw new RuntimeException();
+        }
+
+        mUpdateStatus.pack();
+    }
+
+
+    /**
+     * Return the closest power of two greater than or equal to value.
+     *
+     * @param value the return value will be >= value
+     * @return a power of two >= value.  If value > 2^31, 2^31 is returned.
+     */
+//xxx use Integer.highestOneBit() or numberOfLeadingZeros().
+    private int nextPow2(int value) {
+        for (int i = 31; i >= 0; --i) {
+            if ((value & (1<<i)) != 0) {
+                if (i < 31) {
+                    return 1<<(i + 1);
+                } else {
+                    return 1<<31;
+                }
+            }
+        }
+        return 0;
+    }
+
+    private int zOrderData(ImageData id, byte pixData[]) {
+        int maxX = 0;
+        for (int i = 0; i < pixData.length; i++) {
+            /* Tread the pixData index as a z-order curve index and
+             * decompose into Cartesian coordinates.
+             */
+            int x = (i & 1) |
+                    ((i >>> 2) & 1) << 1 |
+                    ((i >>> 4) & 1) << 2 |
+                    ((i >>> 6) & 1) << 3 |
+                    ((i >>> 8) & 1) << 4 |
+                    ((i >>> 10) & 1) << 5 |
+                    ((i >>> 12) & 1) << 6 |
+                    ((i >>> 14) & 1) << 7 |
+                    ((i >>> 16) & 1) << 8 |
+                    ((i >>> 18) & 1) << 9 |
+                    ((i >>> 20) & 1) << 10 |
+                    ((i >>> 22) & 1) << 11 |
+                    ((i >>> 24) & 1) << 12 |
+                    ((i >>> 26) & 1) << 13 |
+                    ((i >>> 28) & 1) << 14 |
+                    ((i >>> 30) & 1) << 15;
+            int y = ((i >>> 1) & 1) << 0 |
+                    ((i >>> 3) & 1) << 1 |
+                    ((i >>> 5) & 1) << 2 |
+                    ((i >>> 7) & 1) << 3 |
+                    ((i >>> 9) & 1) << 4 |
+                    ((i >>> 11) & 1) << 5 |
+                    ((i >>> 13) & 1) << 6 |
+                    ((i >>> 15) & 1) << 7 |
+                    ((i >>> 17) & 1) << 8 |
+                    ((i >>> 19) & 1) << 9 |
+                    ((i >>> 21) & 1) << 10 |
+                    ((i >>> 23) & 1) << 11 |
+                    ((i >>> 25) & 1) << 12 |
+                    ((i >>> 27) & 1) << 13 |
+                    ((i >>> 29) & 1) << 14 |
+                    ((i >>> 31) & 1) << 15;
+            try {
+                id.setPixel(x, y, pixData[i]);
+                if (x > maxX) {
+                    maxX = x;
+                }
+            } catch (IllegalArgumentException ex) {
+                System.out.println("bad pixels: i " + i +
+                        ", w " + id.width +
+                        ", h " + id.height +
+                        ", x " + x +
+                        ", y " + y);
+                throw ex;
+            }
+        }
+        return maxX;
+    }
+
+    private final static int HILBERT_DIR_N = 0;
+    private final static int HILBERT_DIR_S = 1;
+    private final static int HILBERT_DIR_E = 2;
+    private final static int HILBERT_DIR_W = 3;
+
+    private void hilbertWalk(ImageData id, InputStream pixData,
+                             int order, int x, int y, int dir)
+                             throws IOException {
+        if (x >= id.width || y >= id.height) {
+            return;
+        } else if (order == 0) {
+            try {
+                int p = pixData.read();
+                if (p >= 0) {
+                    // flip along x=y axis;  assume width == height
+                    id.setPixel(y, x, p);
+
+                    /* Skanky; use an otherwise-unused ImageData field
+                     * to keep track of the max x,y used. Note that x and y are inverted.
+                     */
+                    if (y > id.x) {
+                        id.x = y;
+                    }
+                    if (x > id.y) {
+                        id.y = x;
+                    }
+                }
+//xxx just give up; don't bother walking the rest of the image
+            } catch (IllegalArgumentException ex) {
+                System.out.println("bad pixels: order " + order +
+                        ", dir " + dir +
+                        ", w " + id.width +
+                        ", h " + id.height +
+                        ", x " + x +
+                        ", y " + y);
+                throw ex;
+            }
+        } else {
+            order--;
+            int delta = 1 << order;
+            int nextX = x + delta;
+            int nextY = y + delta;
+
+            switch (dir) {
+            case HILBERT_DIR_E:
+                hilbertWalk(id, pixData, order,     x,     y, HILBERT_DIR_N);
+                hilbertWalk(id, pixData, order,     x, nextY, HILBERT_DIR_E);
+                hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_E);
+                hilbertWalk(id, pixData, order, nextX,     y, HILBERT_DIR_S);
+                break;
+            case HILBERT_DIR_N:
+                hilbertWalk(id, pixData, order,     x,     y, HILBERT_DIR_E);
+                hilbertWalk(id, pixData, order, nextX,     y, HILBERT_DIR_N);
+                hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_N);
+                hilbertWalk(id, pixData, order,     x, nextY, HILBERT_DIR_W);
+                break;
+            case HILBERT_DIR_S:
+                hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_W);
+                hilbertWalk(id, pixData, order,     x, nextY, HILBERT_DIR_S);
+                hilbertWalk(id, pixData, order,     x,     y, HILBERT_DIR_S);
+                hilbertWalk(id, pixData, order, nextX,     y, HILBERT_DIR_E);
+                break;
+            case HILBERT_DIR_W:
+                hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_S);
+                hilbertWalk(id, pixData, order, nextX,     y, HILBERT_DIR_W);
+                hilbertWalk(id, pixData, order,     x,     y, HILBERT_DIR_W);
+                hilbertWalk(id, pixData, order,     x, nextY, HILBERT_DIR_N);
+                break;
+            default:
+                throw new RuntimeException("Unexpected Hilbert direction " +
+                                           dir);
+            }
+        }
+    }
+
+    private Point hilbertOrderData(ImageData id, byte pixData[]) {
+
+        int order = 0;
+        for (int n = 1; n < id.width; n *= 2) {
+            order++;
+        }
+        /* Skanky; use an otherwise-unused ImageData field
+         * to keep track of maxX.
+         */
+        Point p = new Point(0,0);
+        int oldIdX = id.x;
+        int oldIdY = id.y;
+        id.x = id.y = 0;
+        try {
+            hilbertWalk(id, new ByteArrayInputStream(pixData),
+                        order, 0, 0, HILBERT_DIR_E);
+            p.x = id.x;
+            p.y = id.y;
+        } catch (IOException ex) {
+            System.err.println("Exception during hilbertWalk()");
+            p.x = id.height;
+            p.y = id.width;
+        }
+        id.x = oldIdX;
+        id.y = oldIdY;
+        return p;
+    }
+
+    private ImageData createHilbertHeapImage(byte pixData[]) {
+        int w, h;
+
+        // Pick an image size that the largest of heaps will fit into.
+        w = (int)Math.sqrt(((16 * 1024 * 1024)/8));
+
+        // Space-filling curves require a power-of-2 width.
+        w = nextPow2(w);
+        h = w;
+
+        // Create the heap image.
+        ImageData id = new ImageData(w, h, 8, mMapPalette);
+
+        // Copy the data into the image
+        //int maxX = zOrderData(id, pixData);
+        Point maxP = hilbertOrderData(id, pixData);
+
+        // update the max size to make it a round number once the zoom is applied
+        int factor = 100 / ZOOMS[mZoom.getSelectionIndex()];
+        if (factor != 1) {
+            int tmp = maxP.x % factor;
+            if (tmp != 0) {
+                maxP.x += factor - tmp;
+            }
+
+            tmp = maxP.y % factor;
+            if (tmp != 0) {
+                maxP.y += factor - tmp;
+            }
+        }
+
+        if (maxP.y < id.height) {
+            // Crop the image down to the interesting part.
+            id = new ImageData(id.width, maxP.y, id.depth, id.palette,
+                               id.scanlinePad, id.data);
+        }
+
+        if (maxP.x < id.width) {
+            // crop the image again. A bit trickier this time.
+           ImageData croppedId = new ImageData(maxP.x, id.height, id.depth, id.palette);
+
+           int[] buffer = new int[maxP.x];
+           for (int l = 0 ; l < id.height; l++) {
+               id.getPixels(0, l, maxP.x, buffer, 0);
+               croppedId.setPixels(0, l, maxP.x, buffer, 0);
+           }
+
+           id = croppedId;
+        }
+
+        // apply the zoom
+        if (factor != 1) {
+            id = id.scaledTo(id.width / factor, id.height / factor);
+        }
+
+        return id;
+    }
+
+    /**
+     * Convert the raw heap data to an image.  We know we're running in
+     * the UI thread, so we can issue graphics commands directly.
+     *
+     * http://help.eclipse.org/help31/nftopic/org.eclipse.platform.doc.isv/reference/api/org/eclipse/swt/graphics/GC.html
+     *
+     * @param cd The client data
+     * @param mode The display mode. 0 = linear, 1 = hilbert.
+     * @param forceRedraw
+     */
+    private void renderHeapData(ClientData cd, int mode, boolean forceRedraw) {
+        Image image;
+
+        byte[] pixData;
+
+        // Atomically get and clear the heap data.
+        synchronized (cd) {
+            if (serializeHeapData(cd.getVmHeapData()) == false && forceRedraw == false) {
+                // no change, we return.
+                return;
+            }
+
+            pixData = getSerializedData();
+        }
+
+        if (pixData != null) {
+            ImageData id;
+            if (mode == 1) {
+                id = createHilbertHeapImage(pixData);
+            } else {
+                id = createLinearHeapImage(pixData, 200, mMapPalette);
+            }
+
+            image = new Image(mDisplay, id);
+        } else {
+            // Render a placeholder image.
+            int width, height;
+            if (mode == 1) {
+                width = height = PLACEHOLDER_HILBERT_SIZE;
+            } else {
+                width = PLACEHOLDER_LINEAR_H_SIZE;
+                height = PLACEHOLDER_LINEAR_V_SIZE;
+            }
+            image = new Image(mDisplay, width, height);
+            GC gc = new GC(image);
+            gc.setForeground(mDisplay.getSystemColor(SWT.COLOR_RED));
+            gc.drawLine(0, 0, width-1, height-1);
+            gc.dispose();
+            gc = null;
+        }
+
+        // set the new image
+
+        if (mode == 1) {
+            if (mHilbertImage != null) {
+                mHilbertImage.dispose();
+            }
+
+            mHilbertImage = image;
+            mHilbertHeapImage.setImage(mHilbertImage);
+            mHilbertHeapImage.pack(true);
+            mHilbertBase.layout();
+            mHilbertBase.pack(true);
+        } else {
+            if (mLinearImage != null) {
+                mLinearImage.dispose();
+            }
+
+            mLinearImage = image;
+            mLinearHeapImage.setImage(mLinearImage);
+            mLinearHeapImage.pack(true);
+            mLinearBase.layout();
+            mLinearBase.pack(true);
+        }
+    }
+
+    @Override
+    protected void setTableFocusListener() {
+        addTableToFocusListener(mHeapSummary);
+    }
+}
+
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/IFindTarget.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/IFindTarget.java
new file mode 100644
index 0000000..9aa6943
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/IFindTarget.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2012 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.ddmuilib;
+
+public interface IFindTarget {
+    boolean findAndSelect(String text, boolean isNewSearch, boolean searchForward);
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ITableFocusListener.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ITableFocusListener.java
new file mode 100644
index 0000000..37dd9a0
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ITableFocusListener.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib;
+
+import org.eclipse.swt.dnd.Clipboard;
+
+/**
+ * An object listening to focus change in Table objects.<br>
+ * For application not relying on a RCP to provide menu changes based on focus,
+ * this class allows to get monitor the focus change of several Table widget
+ * and update the menu action accordingly.
+ */
+public interface ITableFocusListener {
+
+    public interface IFocusedTableActivator {
+        public void copy(Clipboard clipboard);
+
+        public void selectAll();
+    }
+
+    public void focusGained(IFocusedTableActivator activator);
+
+    public void focusLost(IFocusedTableActivator activator);
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ImageLoader.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ImageLoader.java
new file mode 100644
index 0000000..fd480f6
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ImageLoader.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib;
+
+import com.android.ddmlib.Log;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+
+import java.io.InputStream;
+import java.net.URL;
+import java.util.HashMap;
+
+/**
+ * Class to load images stored in a jar file.
+ * All images are loaded from /images/<var>filename</var>
+ *
+ * Because Java requires to know the jar file in which to load the image from, a class is required
+ * when getting the instance. Instances are cached and associated to the class passed to
+ * {@link #getLoader(Class)}.
+ *
+ * {@link #getDdmUiLibLoader()} use {@link ImageLoader#getClass()} as the class. This is to be used
+ * to load images from ddmuilib.
+ *
+ * Loaded images are stored so that 2 calls with the same filename will return the same object.
+ * This also means that {@link Image} object returned by the loader should never be disposed.
+ *
+ */
+public class ImageLoader {
+
+    private static final String PATH = "/images/"; //$NON-NLS-1$
+
+    private final HashMap<String, Image> mLoadedImages = new HashMap<String, Image>();
+    private static final HashMap<Class<?>, ImageLoader> mInstances =
+            new HashMap<Class<?>, ImageLoader>();
+    private final Class<?> mClass;
+
+    /**
+     * Private constructor, creating an instance associated with a class.
+     * The class is used to identify which jar file the images are loaded from.
+     */
+    private ImageLoader(Class<?> theClass) {
+        if (theClass == null) {
+            theClass = ImageLoader.class;
+        }
+        mClass = theClass;
+    }
+
+    /**
+     * Returns the {@link ImageLoader} instance to load images from ddmuilib.jar
+     */
+    public static ImageLoader getDdmUiLibLoader() {
+        return getLoader(null);
+    }
+
+    /**
+     * Returns an {@link ImageLoader} to load images based on a given class.
+     *
+     * The loader will load images from the jar from which the class was loaded. using
+     * {@link Class#getResource(String)} and {@link Class#getResourceAsStream(String)}.
+     *
+     * Since all images are loaded using the path /images/<var>filename</var>, any class from the
+     * jar will work. However since the loader is cached and reused when the query provides the same
+     * class instance, and since the loader will also cache the loaded images, it is recommended
+     * to always use the same class for a given Jar file.
+     *
+     */
+    public static ImageLoader getLoader(Class<?> theClass) {
+        ImageLoader instance = mInstances.get(theClass);
+        if (instance == null) {
+            instance = new ImageLoader(theClass);
+            mInstances.put(theClass, instance);
+        }
+
+        return instance;
+    }
+
+    /**
+     * Disposes all images for all instances.
+     * This should only be called when the program exits.
+     */
+    public static void dispose() {
+        for (ImageLoader loader : mInstances.values()) {
+            loader.doDispose();
+        }
+    }
+
+    private synchronized void doDispose() {
+        for (Image image : mLoadedImages.values()) {
+            image.dispose();
+        }
+
+        mLoadedImages.clear();
+    }
+
+    /**
+     * Returns an {@link ImageDescriptor} for a given filename.
+     *
+     * This searches for an image located at /images/<var>filename</var>.
+     *
+     * @param filename the filename of the image to load.
+     */
+    public ImageDescriptor loadDescriptor(String filename) {
+        URL url = mClass.getResource(PATH + filename);
+        // TODO cache in a map
+        return ImageDescriptor.createFromURL(url);
+    }
+
+    /**
+     * Returns an {@link Image} for a given filename.
+     *
+     * This searches for an image located at /images/<var>filename</var>.
+     *
+     * @param filename the filename of the image to load.
+     * @param display the Display object
+     */
+    public synchronized Image loadImage(String filename, Display display) {
+        Image img = mLoadedImages.get(filename);
+        if (img == null) {
+            String tmp = PATH + filename;
+            InputStream imageStream = mClass.getResourceAsStream(tmp);
+
+            if (imageStream != null) {
+                img = new Image(display, imageStream);
+                mLoadedImages.put(filename, img);
+            }
+
+            if (img == null) {
+                throw new RuntimeException("Failed to load " + tmp);
+            }
+        }
+
+        return img;
+    }
+
+    /**
+     * Loads an image from a resource. This method used a class to locate the
+     * resources, and then load the filename from /images inside the resources.<br>
+     * Extra parameters allows for creation of a replacement image of the
+     * loading failed.
+     *
+     * @param display the Display object
+     * @param fileName the file name
+     * @param width optional width to create replacement Image. If -1, null be
+     *            be returned if the loading fails.
+     * @param height optional height to create replacement Image. If -1, null be
+     *            be returned if the loading fails.
+     * @param phColor optional color to create replacement Image. If null, Blue
+     *            color will be used.
+     * @return a new Image or null if the loading failed and the optional
+     *         replacement size was -1
+     */
+    public Image loadImage(Display display, String fileName, int width, int height,
+            Color phColor) {
+
+        Image img = loadImage(fileName, display);
+
+        if (img == null) {
+            Log.w("ddms", "Couldn't load " + fileName);
+            // if we had the extra parameter to create replacement image then we
+            // create and return it.
+            if (width != -1 && height != -1) {
+                return createPlaceHolderArt(display, width, height,
+                        phColor != null ? phColor : display
+                                .getSystemColor(SWT.COLOR_BLUE));
+            }
+
+            // otherwise, just return null
+            return null;
+        }
+
+        return img;
+    }
+
+    /**
+     * Create place-holder art with the specified color.
+     */
+    public static Image createPlaceHolderArt(Display display, int width,
+            int height, Color color) {
+        Image img = new Image(display, width, height);
+        GC gc = new GC(img);
+        gc.setForeground(color);
+        gc.drawLine(0, 0, width, height);
+        gc.drawLine(0, height - 1, width, -1);
+        gc.dispose();
+        return img;
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/InfoPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/InfoPanel.java
new file mode 100644
index 0000000..60dc2c0
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/InfoPanel.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib;
+
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.TableItem;
+
+/**
+ * Display client info in a two-column format.
+ */
+public class InfoPanel extends TablePanel {
+    private Table mTable;
+    private TableColumn mCol2;
+
+    private static final String mLabels[] = {
+        "DDM-aware?",
+        "App description:",
+        "VM version:",
+        "Process ID:",
+        "Supports Profiling Control:",
+        "Supports HPROF Control:",
+    };
+    private static final int ENT_DDM_AWARE          = 0;
+    private static final int ENT_APP_DESCR          = 1;
+    private static final int ENT_VM_VERSION         = 2;
+    private static final int ENT_PROCESS_ID         = 3;
+    private static final int ENT_SUPPORTS_PROFILING = 4;
+    private static final int ENT_SUPPORTS_HPROF     = 5;
+
+    /**
+     * Create our control(s).
+     */
+    @Override
+    protected Control createControl(Composite parent) {
+        mTable = new Table(parent, SWT.MULTI | SWT.FULL_SELECTION);
+        mTable.setHeaderVisible(false);
+        mTable.setLinesVisible(false);
+
+        TableColumn col1 = new TableColumn(mTable, SWT.RIGHT);
+        col1.setText("name");
+        mCol2 = new TableColumn(mTable, SWT.LEFT);
+        mCol2.setText("PlaceHolderContentForWidth");
+
+        TableItem item;
+        for (int i = 0; i < mLabels.length; i++) {
+            item = new TableItem(mTable, SWT.NONE);
+            item.setText(0, mLabels[i]);
+            item.setText(1, "-");
+        }
+
+        col1.pack();
+        mCol2.pack();
+
+        return mTable;
+    }
+
+    /**
+     * Sets the focus to the proper control inside the panel.
+     */
+    @Override
+    public void setFocus() {
+        mTable.setFocus();
+    }
+
+
+    /**
+     * Sent when an existing client information changed.
+     * <p/>
+     * This is sent from a non UI thread.
+     * @param client the updated client.
+     * @param changeMask the bit mask describing the changed properties. It can contain
+     * any of the following values: {@link Client#CHANGE_PORT}, {@link Client#CHANGE_NAME}
+     * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE},
+     * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
+     * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
+     *
+     * @see IClientChangeListener#clientChanged(Client, int)
+     */
+    @Override
+    public void clientChanged(final Client client, int changeMask) {
+        if (client == getCurrentClient()) {
+            if ((changeMask & Client.CHANGE_INFO) == Client.CHANGE_INFO) {
+                if (mTable.isDisposed())
+                    return;
+
+                mTable.getDisplay().asyncExec(new Runnable() {
+                    @Override
+                    public void run() {
+                        clientSelected();
+                    }
+                });
+            }
+        }
+    }
+
+
+    /**
+     * Sent when a new device is selected. The new device can be accessed
+     * with {@link #getCurrentDevice()}
+     */
+    @Override
+    public void deviceSelected() {
+        // pass
+    }
+
+    /**
+     * Sent when a new client is selected. The new client can be accessed
+     * with {@link #getCurrentClient()}
+     */
+    @Override
+    public void clientSelected() {
+        if (mTable.isDisposed())
+            return;
+
+        Client client = getCurrentClient();
+
+        if (client == null) {
+            for (int i = 0; i < mLabels.length; i++) {
+                TableItem item = mTable.getItem(i);
+                item.setText(1, "-");
+            }
+        } else {
+            TableItem item;
+            String clientDescription, vmIdentifier, isDdmAware,
+                pid;
+
+            ClientData cd = client.getClientData();
+            synchronized (cd) {
+                clientDescription = (cd.getClientDescription() != null) ?
+                        cd.getClientDescription() : "?";
+                vmIdentifier = (cd.getVmIdentifier() != null) ?
+                        cd.getVmIdentifier() : "?";
+                isDdmAware = cd.isDdmAware() ?
+                        "yes" : "no";
+                pid = (cd.getPid() != 0) ?
+                        String.valueOf(cd.getPid()) : "?";
+            }
+
+            item = mTable.getItem(ENT_APP_DESCR);
+            item.setText(1, clientDescription);
+            item = mTable.getItem(ENT_VM_VERSION);
+            item.setText(1, vmIdentifier);
+            item = mTable.getItem(ENT_DDM_AWARE);
+            item.setText(1, isDdmAware);
+            item = mTable.getItem(ENT_PROCESS_ID);
+            item.setText(1, pid);
+
+            item = mTable.getItem(ENT_SUPPORTS_PROFILING);
+            if (cd.hasFeature(ClientData.FEATURE_PROFILING_STREAMING)) {
+                item.setText(1, "Yes");
+            } else if (cd.hasFeature(ClientData.FEATURE_PROFILING)) {
+                item.setText(1, "Yes (Application must be able to write on the SD Card)");
+            } else {
+                item.setText(1, "No");
+            }
+
+            item = mTable.getItem(ENT_SUPPORTS_HPROF);
+            if (cd.hasFeature(ClientData.FEATURE_HPROF_STREAMING)) {
+                item.setText(1, "Yes");
+            } else if (cd.hasFeature(ClientData.FEATURE_HPROF)) {
+                item.setText(1, "Yes (Application must be able to write on the SD Card)");
+            } else {
+                item.setText(1, "No");
+            }
+        }
+
+        mCol2.pack();
+
+        //Log.i("ddms", "InfoPanel: changed " + client);
+    }
+
+    @Override
+    protected void setTableFocusListener() {
+        addTableToFocusListener(mTable);
+    }
+}
+
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/NativeHeapPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/NativeHeapPanel.java
new file mode 100644
index 0000000..337bff2
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/NativeHeapPanel.java
@@ -0,0 +1,1648 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib;
+
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData;
+import com.android.ddmlib.HeapSegment.HeapSegmentElement;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.NativeAllocationInfo;
+import com.android.ddmlib.NativeLibraryMapInfo;
+import com.android.ddmlib.NativeStackCallInfo;
+import com.android.ddmuilib.annotation.WorkerThread;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.custom.StackLayout;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.PaletteData;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Sash;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableItem;
+
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Panel with native heap information.
+ */
+public final class NativeHeapPanel extends BaseHeapPanel {
+
+    /** color palette and map legend. NATIVE is the last enum is a 0 based enum list, so we need
+     * Native+1 at least. We also need 2 more entries for free area and expansion area.  */
+    private static final int NUM_PALETTE_ENTRIES = HeapSegmentElement.KIND_NATIVE+2 +1;
+    private static final String[] mMapLegend = new String[NUM_PALETTE_ENTRIES];
+    private static final PaletteData mMapPalette = createPalette();
+
+    private static final int ALLOC_DISPLAY_ALL = 0;
+    private static final int ALLOC_DISPLAY_PRE_ZYGOTE = 1;
+    private static final int ALLOC_DISPLAY_POST_ZYGOTE = 2;
+
+    private Display mDisplay;
+
+    private Composite mBase;
+
+    private Label mUpdateStatus;
+
+    /** combo giving choice of what to display: all, pre-zygote, post-zygote */
+    private Combo mAllocDisplayCombo;
+
+    private Button mFullUpdateButton;
+
+    // see CreateControl()
+    //private Button mDiffUpdateButton;
+
+    private Combo mDisplayModeCombo;
+
+    /** stack composite for mode (1-2) & 3 */
+    private Composite mTopStackComposite;
+
+    private StackLayout mTopStackLayout;
+
+    /** stack composite for mode 1 & 2 */
+    private Composite mAllocationStackComposite;
+
+    private StackLayout mAllocationStackLayout;
+
+    /** top level container for mode 1 & 2 */
+    private Composite mTableModeControl;
+
+    /** top level object for the allocation mode */
+    private Control mAllocationModeTop;
+
+    /** top level for the library mode */
+    private Control mLibraryModeTopControl;
+
+    /** composite for page UI and total memory display */
+    private Composite mPageUIComposite;
+
+    private Label mTotalMemoryLabel;
+
+    private Label mPageLabel;
+
+    private Button mPageNextButton;
+
+    private Button mPagePreviousButton;
+
+    private Table mAllocationTable;
+
+    private Table mLibraryTable;
+
+    private Table mLibraryAllocationTable;
+
+    private Table mDetailTable;
+
+    private Label mImage;
+
+    private int mAllocDisplayMode = ALLOC_DISPLAY_ALL;
+
+    /**
+     * pointer to current stackcall thread computation in order to quit it if
+     * required (new update requested)
+     */
+    private StackCallThread mStackCallThread;
+
+    /** Current Library Allocation table fill thread. killed if selection changes */
+    private FillTableThread mFillTableThread;
+
+    /**
+     * current client data. Used to access the malloc info when switching pages
+     * or selecting allocation to show stack call
+     */
+    private ClientData mClientData;
+
+    /**
+     * client data from a previous display. used when asking for an "update & diff"
+     */
+    private ClientData mBackUpClientData;
+
+    /** list of NativeAllocationInfo objects filled with the list from ClientData */
+    private final ArrayList<NativeAllocationInfo> mAllocations =
+        new ArrayList<NativeAllocationInfo>();
+
+    /** list of the {@link NativeAllocationInfo} being displayed based on the selection
+     * of {@link #mAllocDisplayCombo}.
+     */
+    private final ArrayList<NativeAllocationInfo> mDisplayedAllocations =
+        new ArrayList<NativeAllocationInfo>();
+
+    /** list of NativeAllocationInfo object kept as backup when doing an "update & diff" */
+    private final ArrayList<NativeAllocationInfo> mBackUpAllocations =
+        new ArrayList<NativeAllocationInfo>();
+
+    /** back up of the total memory, used when doing an "update & diff" */
+    private int mBackUpTotalMemory;
+
+    private int mCurrentPage = 0;
+
+    private int mPageCount = 0;
+
+    /**
+     * list of allocation per Library. This is created from the list of
+     * NativeAllocationInfo objects that is stored in the ClientData object. Since we
+     * don't keep this list around, it is recomputed everytime the client
+     * changes.
+     */
+    private final ArrayList<LibraryAllocations> mLibraryAllocations =
+        new ArrayList<LibraryAllocations>();
+
+    /* args to setUpdateStatus() */
+    private static final int NOT_SELECTED = 0;
+
+    private static final int NOT_ENABLED = 1;
+
+    private static final int ENABLED = 2;
+
+    private static final int DISPLAY_PER_PAGE = 20;
+
+    private static final String PREFS_ALLOCATION_SASH = "NHallocSash"; //$NON-NLS-1$
+    private static final String PREFS_LIBRARY_SASH = "NHlibrarySash"; //$NON-NLS-1$
+    private static final String PREFS_DETAIL_ADDRESS = "NHdetailAddress"; //$NON-NLS-1$
+    private static final String PREFS_DETAIL_LIBRARY = "NHdetailLibrary"; //$NON-NLS-1$
+    private static final String PREFS_DETAIL_METHOD = "NHdetailMethod"; //$NON-NLS-1$
+    private static final String PREFS_DETAIL_FILE = "NHdetailFile"; //$NON-NLS-1$
+    private static final String PREFS_DETAIL_LINE = "NHdetailLine"; //$NON-NLS-1$
+    private static final String PREFS_ALLOC_TOTAL = "NHallocTotal"; //$NON-NLS-1$
+    private static final String PREFS_ALLOC_COUNT = "NHallocCount"; //$NON-NLS-1$
+    private static final String PREFS_ALLOC_SIZE = "NHallocSize"; //$NON-NLS-1$
+    private static final String PREFS_ALLOC_LIBRARY = "NHallocLib"; //$NON-NLS-1$
+    private static final String PREFS_ALLOC_METHOD = "NHallocMethod"; //$NON-NLS-1$
+    private static final String PREFS_ALLOC_FILE = "NHallocFile"; //$NON-NLS-1$
+    private static final String PREFS_LIB_LIBRARY = "NHlibLibrary"; //$NON-NLS-1$
+    private static final String PREFS_LIB_SIZE = "NHlibSize"; //$NON-NLS-1$
+    private static final String PREFS_LIB_COUNT = "NHlibCount"; //$NON-NLS-1$
+    private static final String PREFS_LIBALLOC_TOTAL = "NHlibAllocTotal"; //$NON-NLS-1$
+    private static final String PREFS_LIBALLOC_COUNT = "NHlibAllocCount"; //$NON-NLS-1$
+    private static final String PREFS_LIBALLOC_SIZE = "NHlibAllocSize"; //$NON-NLS-1$
+    private static final String PREFS_LIBALLOC_METHOD = "NHlibAllocMethod"; //$NON-NLS-1$
+
+    /** static formatter object to format all numbers as #,### */
+    private static DecimalFormat sFormatter;
+    static {
+        sFormatter = (DecimalFormat)NumberFormat.getInstance();
+        if (sFormatter == null) {
+            sFormatter = new DecimalFormat("#,###");
+        } else {
+            sFormatter.applyPattern("#,###");
+        }
+    }
+
+
+    /**
+     * caching mechanism to avoid recomputing the backtrace for a particular
+     * address several times.
+     */
+    private HashMap<Long, NativeStackCallInfo> mSourceCache =
+        new HashMap<Long, NativeStackCallInfo>();
+    private long mTotalSize;
+    private Button mSaveButton;
+    private Button mSymbolsButton;
+
+    /**
+     * thread class to convert the address call into method, file and line
+     * number in the background.
+     */
+    private class StackCallThread extends BackgroundThread {
+        private ClientData mClientData;
+
+        public StackCallThread(ClientData cd) {
+            mClientData = cd;
+        }
+
+        public ClientData getClientData() {
+            return mClientData;
+        }
+
+        @Override
+        public void run() {
+            // loop through all the NativeAllocationInfo and init them
+            Iterator<NativeAllocationInfo> iter = mAllocations.iterator();
+            int total = mAllocations.size();
+            int count = 0;
+            while (iter.hasNext()) {
+
+                if (isQuitting())
+                    return;
+
+                NativeAllocationInfo info = iter.next();
+                if (info.isStackCallResolved() == false) {
+                    final List<Long> list = info.getStackCallAddresses();
+                    final int size = list.size();
+
+                    ArrayList<NativeStackCallInfo> resolvedStackCall =
+                        new ArrayList<NativeStackCallInfo>();
+
+                    for (int i = 0; i < size; i++) {
+                        long addr = list.get(i);
+
+                        // first check if the addr has already been converted.
+                        NativeStackCallInfo source = mSourceCache.get(addr);
+
+                        // if not we convert it
+                        if (source == null) {
+                            source = sourceForAddr(addr);
+                            mSourceCache.put(addr, source);
+                        }
+
+                        resolvedStackCall.add(source);
+                    }
+
+                    info.setResolvedStackCall(resolvedStackCall);
+                }
+                // after every DISPLAY_PER_PAGE we ask for a ui refresh, unless
+                // we reach total, since we also do it after the loop
+                // (only an issue in case we have a perfect number of page)
+                count++;
+                if ((count % DISPLAY_PER_PAGE) == 0 && count != total) {
+                    if (updateNHAllocationStackCalls(mClientData, count) == false) {
+                        // looks like the app is quitting, so we just
+                        // stopped the thread
+                        return;
+                    }
+                }
+            }
+
+            updateNHAllocationStackCalls(mClientData, count);
+        }
+
+        private NativeStackCallInfo sourceForAddr(long addr) {
+            NativeLibraryMapInfo library = getLibraryFor(addr);
+
+            if (library != null) {
+
+                Addr2Line process = Addr2Line.getProcess(library);
+                if (process != null) {
+                    // remove the base of the library address
+                    NativeStackCallInfo info = process.getAddress(addr);
+                    if (info != null) {
+                        return info;
+                    }
+                }
+            }
+
+            return new NativeStackCallInfo(addr,
+                    library != null ? library.getLibraryName() : null,
+                    Long.toHexString(addr),
+                    "");
+        }
+
+        private NativeLibraryMapInfo getLibraryFor(long addr) {
+            for (NativeLibraryMapInfo info : mClientData.getMappedNativeLibraries()) {
+                if (info.isWithinLibrary(addr)) {
+                    return info;
+                }
+            }
+
+            Log.d("ddm-nativeheap", "Failed finding Library for " + Long.toHexString(addr));
+            return null;
+        }
+
+        /**
+         * update the Native Heap panel with the amount of allocation for which the
+         * stack call has been computed. This is called from a non UI thread, but
+         * will be executed in the UI thread.
+         *
+         * @param count the amount of allocation
+         * @return false if the display was disposed and the update couldn't happen
+         */
+        private boolean updateNHAllocationStackCalls(final ClientData clientData, final int count) {
+            if (mDisplay.isDisposed() == false) {
+                mDisplay.asyncExec(new Runnable() {
+                    @Override
+                    public void run() {
+                        updateAllocationStackCalls(clientData, count);
+                    }
+                });
+                return true;
+            }
+            return false;
+        }
+    }
+
+    private class FillTableThread extends BackgroundThread {
+        private LibraryAllocations mLibAlloc;
+
+        private int mMax;
+
+        public FillTableThread(LibraryAllocations liballoc, int m) {
+            mLibAlloc = liballoc;
+            mMax = m;
+        }
+
+        @Override
+        public void run() {
+            for (int i = mMax; i > 0 && isQuitting() == false; i -= 10) {
+                updateNHLibraryAllocationTable(mLibAlloc, mMax - i, mMax - i + 10);
+            }
+        }
+
+        /**
+         * updates the library allocation table in the Native Heap panel. This is
+         * called from a non UI thread, but will be executed in the UI thread.
+         *
+         * @param liballoc the current library allocation object being displayed
+         * @param start start index of items that need to be displayed
+         * @param end end index of the items that need to be displayed
+         */
+        private void updateNHLibraryAllocationTable(final LibraryAllocations libAlloc,
+                final int start, final int end) {
+            if (mDisplay.isDisposed() == false) {
+                mDisplay.asyncExec(new Runnable() {
+                    @Override
+                    public void run() {
+                        updateLibraryAllocationTable(libAlloc, start, end);
+                    }
+                });
+            }
+
+        }
+    }
+
+    /** class to aggregate allocations per library */
+    public static class LibraryAllocations {
+        private String mLibrary;
+
+        private final ArrayList<NativeAllocationInfo> mLibAllocations =
+            new ArrayList<NativeAllocationInfo>();
+
+        private int mSize;
+
+        private int mCount;
+
+        /** construct the aggregate object for a library */
+        public LibraryAllocations(final String lib) {
+            mLibrary = lib;
+        }
+
+        /** get the library name */
+        public String getLibrary() {
+            return mLibrary;
+        }
+
+        /** add a NativeAllocationInfo object to this aggregate object */
+        public void addAllocation(NativeAllocationInfo info) {
+            mLibAllocations.add(info);
+        }
+
+        /** get an iterator on the NativeAllocationInfo objects */
+        public Iterator<NativeAllocationInfo> getAllocations() {
+            return mLibAllocations.iterator();
+        }
+
+        /** get a NativeAllocationInfo object by index */
+        public NativeAllocationInfo getAllocation(int index) {
+            return mLibAllocations.get(index);
+        }
+
+        /** returns the NativeAllocationInfo object count */
+        public int getAllocationSize() {
+            return mLibAllocations.size();
+        }
+
+        /** returns the total allocation size */
+        public int getSize() {
+            return mSize;
+        }
+
+        /** returns the number of allocations */
+        public int getCount() {
+            return mCount;
+        }
+
+        /**
+         * compute the allocation count and size for allocation objects added
+         * through <code>addAllocation()</code>, and sort the objects by
+         * total allocation size.
+         */
+        public void computeAllocationSizeAndCount() {
+            mSize = 0;
+            mCount = 0;
+            for (NativeAllocationInfo info : mLibAllocations) {
+                mCount += info.getAllocationCount();
+                mSize += info.getAllocationCount() * info.getSize();
+            }
+            Collections.sort(mLibAllocations, new Comparator<NativeAllocationInfo>() {
+                @Override
+                public int compare(NativeAllocationInfo o1, NativeAllocationInfo o2) {
+                    return o2.getAllocationCount() * o2.getSize() -
+                        o1.getAllocationCount() * o1.getSize();
+                }
+            });
+        }
+    }
+
+    /**
+     * Create our control(s).
+     */
+    @Override
+    protected Control createControl(Composite parent) {
+
+        mDisplay = parent.getDisplay();
+
+        mBase = new Composite(parent, SWT.NONE);
+        GridLayout gl = new GridLayout(1, false);
+        gl.horizontalSpacing = 0;
+        gl.verticalSpacing = 0;
+        mBase.setLayout(gl);
+        mBase.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        // composite for <update btn> <status>
+        Composite tmp = new Composite(mBase, SWT.NONE);
+        tmp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        tmp.setLayout(gl = new GridLayout(2, false));
+        gl.marginWidth = gl.marginHeight = 0;
+
+        mFullUpdateButton = new Button(tmp, SWT.NONE);
+        mFullUpdateButton.setText("Full Update");
+        mFullUpdateButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                mBackUpClientData = null;
+                mDisplayModeCombo.setEnabled(false);
+                mSaveButton.setEnabled(false);
+                emptyTables();
+                // if we already have a stack call computation for this
+                // client
+                // we stop it
+                if (mStackCallThread != null &&
+                        mStackCallThread.getClientData() == mClientData) {
+                    mStackCallThread.quit();
+                    mStackCallThread = null;
+                }
+                mLibraryAllocations.clear();
+                Client client = getCurrentClient();
+                if (client != null) {
+                    client.requestNativeHeapInformation();
+                }
+            }
+        });
+
+        mUpdateStatus = new Label(tmp, SWT.NONE);
+        mUpdateStatus.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        // top layout for the combos and oter controls on the right.
+        Composite top_layout = new Composite(mBase, SWT.NONE);
+        top_layout.setLayout(gl = new GridLayout(4, false));
+        gl.marginWidth = gl.marginHeight = 0;
+
+        new Label(top_layout, SWT.NONE).setText("Show:");
+
+        mAllocDisplayCombo = new Combo(top_layout, SWT.DROP_DOWN | SWT.READ_ONLY);
+        mAllocDisplayCombo.setLayoutData(new GridData(
+                GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
+        mAllocDisplayCombo.add("All Allocations");
+        mAllocDisplayCombo.add("Pre-Zygote Allocations");
+        mAllocDisplayCombo.add("Zygote Child Allocations (Z)");
+        mAllocDisplayCombo.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                onAllocDisplayChange();
+            }
+        });
+        mAllocDisplayCombo.select(0);
+
+        // separator
+        Label separator = new Label(top_layout, SWT.SEPARATOR | SWT.VERTICAL);
+        GridData gd;
+        separator.setLayoutData(gd = new GridData(
+                GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL));
+        gd.heightHint = 0;
+        gd.verticalSpan = 2;
+
+        mSaveButton = new Button(top_layout, SWT.PUSH);
+        mSaveButton.setText("Save...");
+        mSaveButton.setEnabled(false);
+        mSaveButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                FileDialog fileDialog = new FileDialog(mBase.getShell(), SWT.SAVE);
+
+                fileDialog.setText("Save Allocations");
+                fileDialog.setFileName("allocations.txt");
+
+                String fileName = fileDialog.open();
+                if (fileName != null) {
+                    saveAllocations(fileName);
+                }
+            }
+        });
+
+        /*
+         * TODO: either fix the diff mechanism or remove it altogether.
+        mDiffUpdateButton = new Button(top_layout, SWT.NONE);
+        mDiffUpdateButton.setText("Update && Diff");
+        mDiffUpdateButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                // since this is an update and diff, we need to store the
+                // current list
+                // of mallocs
+                mBackUpAllocations.clear();
+                mBackUpAllocations.addAll(mAllocations);
+                mBackUpClientData = mClientData;
+                mBackUpTotalMemory = mClientData.getTotalNativeMemory();
+
+                mDisplayModeCombo.setEnabled(false);
+                emptyTables();
+                // if we already have a stack call computation for this
+                // client
+                // we stop it
+                if (mStackCallThread != null &&
+                        mStackCallThread.getClientData() == mClientData) {
+                    mStackCallThread.quit();
+                    mStackCallThread = null;
+                }
+                mLibraryAllocations.clear();
+                Client client = getCurrentClient();
+                if (client != null) {
+                    client.requestNativeHeapInformation();
+                }
+            }
+        });
+        */
+
+        Label l = new Label(top_layout, SWT.NONE);
+        l.setText("Display:");
+
+        mDisplayModeCombo = new Combo(top_layout, SWT.DROP_DOWN | SWT.READ_ONLY);
+        mDisplayModeCombo.setLayoutData(new GridData(
+                GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
+        mDisplayModeCombo.setItems(new String[] { "Allocation List", "By Libraries" });
+        mDisplayModeCombo.select(0);
+        mDisplayModeCombo.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                switchDisplayMode();
+            }
+        });
+        mDisplayModeCombo.setEnabled(false);
+
+        mSymbolsButton = new Button(top_layout, SWT.PUSH);
+        mSymbolsButton.setText("Load Symbols");
+        mSymbolsButton.setEnabled(false);
+
+
+        // create a composite that will contains the actual content composites,
+        // in stack mode layout.
+        // This top level composite contains 2 other composites.
+        // * one for both Allocations and Libraries mode
+        // * one for flat mode (which is gone for now)
+
+        mTopStackComposite = new Composite(mBase, SWT.NONE);
+        mTopStackComposite.setLayout(mTopStackLayout = new StackLayout());
+        mTopStackComposite.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        // create 1st and 2nd modes
+        createTableDisplay(mTopStackComposite);
+
+        mTopStackLayout.topControl = mTableModeControl;
+        mTopStackComposite.layout();
+
+        setUpdateStatus(NOT_SELECTED);
+
+        // Work in progress
+        // TODO add image display of native heap.
+        //mImage = new Label(mBase, SWT.NONE);
+
+        mBase.pack();
+
+        return mBase;
+    }
+
+    /**
+     * Sets the focus to the proper control inside the panel.
+     */
+    @Override
+    public void setFocus() {
+        // TODO
+    }
+
+
+    /**
+     * Sent when an existing client information changed.
+     * <p/>
+     * This is sent from a non UI thread.
+     * @param client the updated client.
+     * @param changeMask the bit mask describing the changed properties. It can contain
+     * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME}
+     * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE},
+     * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
+     * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
+     *
+     * @see IClientChangeListener#clientChanged(Client, int)
+     */
+    @Override
+    public void clientChanged(final Client client, int changeMask) {
+        if (client == getCurrentClient()) {
+            if ((changeMask & Client.CHANGE_NATIVE_HEAP_DATA) == Client.CHANGE_NATIVE_HEAP_DATA) {
+                if (mBase.isDisposed())
+                    return;
+
+                mBase.getDisplay().asyncExec(new Runnable() {
+                    @Override
+                    public void run() {
+                        clientSelected();
+                    }
+                });
+            }
+        }
+    }
+
+    /**
+     * Sent when a new device is selected. The new device can be accessed
+     * with {@link #getCurrentDevice()}.
+     */
+    @Override
+    public void deviceSelected() {
+        // pass
+    }
+
+    /**
+     * Sent when a new client is selected. The new client can be accessed
+     * with {@link #getCurrentClient()}.
+     */
+    @Override
+    public void clientSelected() {
+        if (mBase.isDisposed())
+            return;
+
+        Client client = getCurrentClient();
+
+        mDisplayModeCombo.setEnabled(false);
+        emptyTables();
+
+        Log.d("ddms", "NativeHeapPanel: changed " + client);
+
+        if (client != null) {
+            ClientData cd = client.getClientData();
+            mClientData = cd;
+
+            // if (cd.getShowHeapUpdates())
+            setUpdateStatus(ENABLED);
+            // else
+            // setUpdateStatus(NOT_ENABLED);
+
+            initAllocationDisplay();
+
+            //renderBitmap(cd);
+        } else {
+            mClientData = null;
+            setUpdateStatus(NOT_SELECTED);
+        }
+
+        mBase.pack();
+    }
+
+    /**
+     * Update the UI with the newly compute stack calls, unless the UI switched
+     * to a different client.
+     *
+     * @param cd the ClientData for which the stack call are being computed.
+     * @param count the current count of allocations for which the stack calls
+     *            have been computed.
+     */
+    @WorkerThread
+    public void updateAllocationStackCalls(ClientData cd, int count) {
+        // we have to check that the panel still shows the same clientdata than
+        // the thread is computing for.
+        if (cd == mClientData) {
+
+            int total = mAllocations.size();
+
+            if (count == total) {
+                // we're done: do something
+                mDisplayModeCombo.setEnabled(true);
+                mSaveButton.setEnabled(true);
+
+                mStackCallThread = null;
+            } else {
+                // work in progress, update the progress bar.
+//                mUiThread.setStatusLine("Computing stack call: " + count
+//                        + "/" + total);
+            }
+
+            // FIXME: attempt to only update when needed.
+            // Because the number of pages is not related to mAllocations.size() anymore
+            // due to pre-zygote/post-zygote display, update all the time.
+            // At some point we should remove the pages anyway, since it's getting computed
+            // really fast now.
+//            if ((mCurrentPage + 1) * DISPLAY_PER_PAGE == count
+//                    || (count == total && mCurrentPage == mPageCount - 1)) {
+            try {
+                // get the current selection of the allocation
+                int index = mAllocationTable.getSelectionIndex();
+                NativeAllocationInfo info = null;
+
+                if (index != -1) {
+                    info = (NativeAllocationInfo)mAllocationTable.getItem(index).getData();
+                }
+
+                // empty the table
+                emptyTables();
+
+                // fill it again
+                fillAllocationTable();
+
+                // reselect
+                mAllocationTable.setSelection(index);
+
+                // display detail table if needed
+                if (info != null) {
+                    fillDetailTable(info);
+                }
+            } catch (SWTException e) {
+                if (mAllocationTable.isDisposed()) {
+                    // looks like the table is disposed. Let's ignore it.
+                } else {
+                    throw e;
+                }
+            }
+
+        } else {
+            // old client still running. doesn't really matter.
+        }
+    }
+
+    @Override
+    protected void setTableFocusListener() {
+        addTableToFocusListener(mAllocationTable);
+        addTableToFocusListener(mLibraryTable);
+        addTableToFocusListener(mLibraryAllocationTable);
+        addTableToFocusListener(mDetailTable);
+    }
+
+    protected void onAllocDisplayChange() {
+        mAllocDisplayMode = mAllocDisplayCombo.getSelectionIndex();
+
+        // create the new list
+        updateAllocDisplayList();
+
+        updateTotalMemoryDisplay();
+
+        // reset the ui.
+        mCurrentPage = 0;
+        updatePageUI();
+        switchDisplayMode();
+    }
+
+    private void updateAllocDisplayList() {
+        mTotalSize = 0;
+        mDisplayedAllocations.clear();
+        for (NativeAllocationInfo info : mAllocations) {
+            if (mAllocDisplayMode == ALLOC_DISPLAY_ALL ||
+                    (mAllocDisplayMode == ALLOC_DISPLAY_PRE_ZYGOTE ^ info.isZygoteChild())) {
+                mDisplayedAllocations.add(info);
+                mTotalSize += info.getSize() * info.getAllocationCount();
+            } else {
+                // skip this item
+                continue;
+            }
+        }
+
+        int count = mDisplayedAllocations.size();
+
+        mPageCount = count / DISPLAY_PER_PAGE;
+
+        // need to add a page for the rest of the div
+        if ((count % DISPLAY_PER_PAGE) > 0) {
+            mPageCount++;
+        }
+    }
+
+    private void updateTotalMemoryDisplay() {
+        switch (mAllocDisplayMode) {
+            case ALLOC_DISPLAY_ALL:
+                mTotalMemoryLabel.setText(String.format("Total Memory: %1$s Bytes",
+                        sFormatter.format(mTotalSize)));
+                break;
+            case ALLOC_DISPLAY_PRE_ZYGOTE:
+                mTotalMemoryLabel.setText(String.format("Zygote Memory: %1$s Bytes",
+                        sFormatter.format(mTotalSize)));
+                break;
+            case ALLOC_DISPLAY_POST_ZYGOTE:
+                mTotalMemoryLabel.setText(String.format("Post-zygote Memory: %1$s Bytes",
+                        sFormatter.format(mTotalSize)));
+                break;
+        }
+    }
+
+
+    private void switchDisplayMode() {
+        switch (mDisplayModeCombo.getSelectionIndex()) {
+            case 0: {// allocations
+                mTopStackLayout.topControl = mTableModeControl;
+                mAllocationStackLayout.topControl = mAllocationModeTop;
+                mAllocationStackComposite.layout();
+                mTopStackComposite.layout();
+                emptyTables();
+                fillAllocationTable();
+            }
+                break;
+            case 1: {// libraries
+                mTopStackLayout.topControl = mTableModeControl;
+                mAllocationStackLayout.topControl = mLibraryModeTopControl;
+                mAllocationStackComposite.layout();
+                mTopStackComposite.layout();
+                emptyTables();
+                fillLibraryTable();
+            }
+                break;
+        }
+    }
+
+    private void initAllocationDisplay() {
+        if (mStackCallThread != null) {
+            mStackCallThread.quit();
+        }
+
+        mAllocations.clear();
+        mAllocations.addAll(mClientData.getNativeAllocationList());
+
+        updateAllocDisplayList();
+
+        // if we have a previous clientdata and it matches the current one. we
+        // do a diff between the new list and the old one.
+        if (mBackUpClientData != null && mBackUpClientData == mClientData) {
+
+            ArrayList<NativeAllocationInfo> add = new ArrayList<NativeAllocationInfo>();
+
+            // we go through the list of NativeAllocationInfo in the new list and check if
+            // there's one with the same exact data (size, allocation, count and
+            // stackcall addresses) in the old list.
+            // if we don't find any, we add it to the "add" list
+            for (NativeAllocationInfo mi : mAllocations) {
+                boolean found = false;
+                for (NativeAllocationInfo old_mi : mBackUpAllocations) {
+                    if (mi.equals(old_mi)) {
+                        found = true;
+                        break;
+                    }
+                }
+                if (found == false) {
+                    add.add(mi);
+                }
+            }
+
+            // put the result in mAllocations
+            mAllocations.clear();
+            mAllocations.addAll(add);
+
+            // display the difference in memory usage. This is computed
+            // calculating the memory usage of the objects in mAllocations.
+            int count = 0;
+            for (NativeAllocationInfo allocInfo : mAllocations) {
+                count += allocInfo.getSize() * allocInfo.getAllocationCount();
+            }
+
+            mTotalMemoryLabel.setText(String.format("Memory Difference: %1$s Bytes",
+                    sFormatter.format(count)));
+        }
+        else {
+            // display the full memory usage
+            updateTotalMemoryDisplay();
+            //mDiffUpdateButton.setEnabled(mClientData.getTotalNativeMemory() > 0);
+        }
+        mTotalMemoryLabel.pack();
+
+        // update the page ui
+        mDisplayModeCombo.select(0);
+
+        mLibraryAllocations.clear();
+
+        // reset to first page
+        mCurrentPage = 0;
+
+        // update the label
+        updatePageUI();
+
+        // now fill the allocation Table with the current page
+        switchDisplayMode();
+
+        // start the thread to compute the stack calls
+        if (mAllocations.size() > 0) {
+            mStackCallThread = new StackCallThread(mClientData);
+            mStackCallThread.start();
+        }
+    }
+
+    private void updatePageUI() {
+
+        // set the label and pack to update the layout, otherwise
+        // the label will be cut off if the new size is bigger
+        if (mPageCount == 0) {
+            mPageLabel.setText("0 of 0 allocations.");
+        } else {
+            StringBuffer buffer = new StringBuffer();
+            // get our starting index
+            int start = (mCurrentPage * DISPLAY_PER_PAGE) + 1;
+            // end index, taking into account the last page can be half full
+            int count = mDisplayedAllocations.size();
+            int end = Math.min(start + DISPLAY_PER_PAGE - 1, count);
+            buffer.append(sFormatter.format(start));
+            buffer.append(" - ");
+            buffer.append(sFormatter.format(end));
+            buffer.append(" of ");
+            buffer.append(sFormatter.format(count));
+            buffer.append(" allocations.");
+            mPageLabel.setText(buffer.toString());
+        }
+
+        // handle the button enabled state.
+        mPagePreviousButton.setEnabled(mCurrentPage > 0);
+        // reminder: mCurrentPage starts at 0.
+        mPageNextButton.setEnabled(mCurrentPage < mPageCount - 1);
+
+        mPageLabel.pack();
+        mPageUIComposite.pack();
+
+    }
+
+    private void fillAllocationTable() {
+        // get the count
+        int count = mDisplayedAllocations.size();
+
+        // get our starting index
+        int start = mCurrentPage * DISPLAY_PER_PAGE;
+
+        // loop for DISPLAY_PER_PAGE or till we reach count
+        int end = start + DISPLAY_PER_PAGE;
+
+        for (int i = start; i < end && i < count; i++) {
+            NativeAllocationInfo info = mDisplayedAllocations.get(i);
+
+            TableItem item = null;
+
+            if (mAllocDisplayMode == ALLOC_DISPLAY_ALL)  {
+                item = new TableItem(mAllocationTable, SWT.NONE);
+                item.setText(0, (info.isZygoteChild() ? "Z " : "") +
+                        sFormatter.format(info.getSize() * info.getAllocationCount()));
+                item.setText(1, sFormatter.format(info.getAllocationCount()));
+                item.setText(2, sFormatter.format(info.getSize()));
+            } else if (mAllocDisplayMode == ALLOC_DISPLAY_PRE_ZYGOTE ^ info.isZygoteChild()) {
+                item = new TableItem(mAllocationTable, SWT.NONE);
+                item.setText(0, sFormatter.format(info.getSize() * info.getAllocationCount()));
+                item.setText(1, sFormatter.format(info.getAllocationCount()));
+                item.setText(2, sFormatter.format(info.getSize()));
+            } else {
+                // skip this item
+                continue;
+            }
+
+            item.setData(info);
+
+            NativeStackCallInfo bti = info.getRelevantStackCallInfo();
+            if (bti != null) {
+                String lib = bti.getLibraryName();
+                String method = bti.getMethodName();
+                String source = bti.getSourceFile();
+                if (lib != null)
+                    item.setText(3, lib);
+                if (method != null)
+                    item.setText(4, method);
+                if (source != null)
+                    item.setText(5, source);
+            }
+        }
+    }
+
+    private void fillLibraryTable() {
+        // fill the library table
+        sortAllocationsPerLibrary();
+
+        for (LibraryAllocations liballoc : mLibraryAllocations) {
+            if (liballoc != null) {
+                TableItem item = new TableItem(mLibraryTable, SWT.NONE);
+                String lib = liballoc.getLibrary();
+                item.setText(0, lib != null ? lib : "");
+                item.setText(1, sFormatter.format(liballoc.getSize()));
+                item.setText(2, sFormatter.format(liballoc.getCount()));
+            }
+        }
+    }
+
+    private void fillLibraryAllocationTable() {
+        mLibraryAllocationTable.removeAll();
+        mDetailTable.removeAll();
+        int index = mLibraryTable.getSelectionIndex();
+        if (index != -1) {
+            LibraryAllocations liballoc = mLibraryAllocations.get(index);
+            // start a thread that will fill table 10 at a time to keep the ui
+            // responsive, but first we kill the previous one if there was one
+            if (mFillTableThread != null) {
+                mFillTableThread.quit();
+            }
+            mFillTableThread = new FillTableThread(liballoc,
+                    liballoc.getAllocationSize());
+            mFillTableThread.start();
+        }
+    }
+
+    public void updateLibraryAllocationTable(LibraryAllocations liballoc,
+            int start, int end) {
+        try {
+            if (mLibraryTable.isDisposed() == false) {
+                int index = mLibraryTable.getSelectionIndex();
+                if (index != -1) {
+                    LibraryAllocations newliballoc = mLibraryAllocations.get(
+                            index);
+                    if (newliballoc == liballoc) {
+                        int count = liballoc.getAllocationSize();
+                        for (int i = start; i < end && i < count; i++) {
+                            NativeAllocationInfo info = liballoc.getAllocation(i);
+
+                            TableItem item = new TableItem(
+                                    mLibraryAllocationTable, SWT.NONE);
+                            item.setText(0, sFormatter.format(
+                                    info.getSize() * info.getAllocationCount()));
+                            item.setText(1, sFormatter.format(info.getAllocationCount()));
+                            item.setText(2, sFormatter.format(info.getSize()));
+
+                            NativeStackCallInfo stackCallInfo = info.getRelevantStackCallInfo();
+                            if (stackCallInfo != null) {
+                                item.setText(3, stackCallInfo.getMethodName());
+                            }
+                        }
+                    } else {
+                        // we should quit the thread
+                        if (mFillTableThread != null) {
+                            mFillTableThread.quit();
+                            mFillTableThread = null;
+                        }
+                    }
+                }
+            }
+        } catch (SWTException e) {
+            Log.e("ddms", "error when updating the library allocation table");
+        }
+    }
+
+    private void fillDetailTable(final NativeAllocationInfo mi) {
+        mDetailTable.removeAll();
+        mDetailTable.setRedraw(false);
+
+        try {
+            // populate the detail Table with the back trace
+            List<Long> addresses = mi.getStackCallAddresses();
+            List<NativeStackCallInfo> resolvedStackCall = mi.getResolvedStackCall();
+
+            if (resolvedStackCall == null) {
+                return;
+            }
+
+            for (int i = 0 ; i < resolvedStackCall.size(); i++) {
+                if (addresses.get(i) == null || addresses.get(i).longValue() == 0) {
+                    continue;
+                }
+
+                long addr = addresses.get(i).longValue();
+                NativeStackCallInfo source = resolvedStackCall.get(i);
+
+                TableItem item = new TableItem(mDetailTable, SWT.NONE);
+                item.setText(0, String.format("%08x", addr)); //$NON-NLS-1$
+
+                String libraryName = source.getLibraryName();
+                String methodName = source.getMethodName();
+                String sourceFile = source.getSourceFile();
+                int lineNumber = source.getLineNumber();
+
+                if (libraryName != null)
+                    item.setText(1, libraryName);
+                if (methodName != null)
+                    item.setText(2, methodName);
+                if (sourceFile != null)
+                    item.setText(3, sourceFile);
+                if (lineNumber != -1)
+                    item.setText(4, Integer.toString(lineNumber));
+            }
+        } finally {
+            mDetailTable.setRedraw(true);
+        }
+    }
+
+    /*
+     * Are updates enabled?
+     */
+    private void setUpdateStatus(int status) {
+        switch (status) {
+            case NOT_SELECTED:
+                mUpdateStatus.setText("Select a client to see heap info");
+                mAllocDisplayCombo.setEnabled(false);
+                mFullUpdateButton.setEnabled(false);
+                //mDiffUpdateButton.setEnabled(false);
+                break;
+            case NOT_ENABLED:
+                mUpdateStatus.setText("Heap updates are " + "NOT ENABLED for this client");
+                mAllocDisplayCombo.setEnabled(false);
+                mFullUpdateButton.setEnabled(false);
+                //mDiffUpdateButton.setEnabled(false);
+                break;
+            case ENABLED:
+                mUpdateStatus.setText("Press 'Full Update' to retrieve " + "latest data");
+                mAllocDisplayCombo.setEnabled(true);
+                mFullUpdateButton.setEnabled(true);
+                //mDiffUpdateButton.setEnabled(true);
+                break;
+            default:
+                throw new RuntimeException();
+        }
+
+        mUpdateStatus.pack();
+    }
+
+    /**
+     * Create the Table display. This includes a "detail" Table in the bottom
+     * half and 2 modes in the top half: allocation Table and
+     * library+allocations Tables.
+     *
+     * @param base the top parent to create the display into
+     */
+    private void createTableDisplay(Composite base) {
+        final int minPanelWidth = 60;
+
+        final IPreferenceStore prefs = DdmUiPreferences.getStore();
+
+        // top level composite for mode 1 & 2
+        mTableModeControl = new Composite(base, SWT.NONE);
+        GridLayout gl = new GridLayout(1, false);
+        gl.marginLeft = gl.marginRight = gl.marginTop = gl.marginBottom = 0;
+        mTableModeControl.setLayout(gl);
+        mTableModeControl.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        mTotalMemoryLabel = new Label(mTableModeControl, SWT.NONE);
+        mTotalMemoryLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mTotalMemoryLabel.setText("Total Memory: 0 Bytes");
+
+        // the top half of these modes is dynamic
+
+        final Composite sash_composite = new Composite(mTableModeControl,
+                SWT.NONE);
+        sash_composite.setLayout(new FormLayout());
+        sash_composite.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        // create the stacked composite
+        mAllocationStackComposite = new Composite(sash_composite, SWT.NONE);
+        mAllocationStackLayout = new StackLayout();
+        mAllocationStackComposite.setLayout(mAllocationStackLayout);
+        mAllocationStackComposite.setLayoutData(new GridData(
+                GridData.FILL_BOTH));
+
+        // create the top half for mode 1
+        createAllocationTopHalf(mAllocationStackComposite);
+
+        // create the top half for mode 2
+        createLibraryTopHalf(mAllocationStackComposite);
+
+        final Sash sash = new Sash(sash_composite, SWT.HORIZONTAL);
+
+        // bottom half of these modes is the same: detail table
+        createDetailTable(sash_composite);
+
+        // init value for stack
+        mAllocationStackLayout.topControl = mAllocationModeTop;
+
+        // form layout data
+        FormData data = new FormData();
+        data.top = new FormAttachment(mTotalMemoryLabel, 0);
+        data.bottom = new FormAttachment(sash, 0);
+        data.left = new FormAttachment(0, 0);
+        data.right = new FormAttachment(100, 0);
+        mAllocationStackComposite.setLayoutData(data);
+
+        final FormData sashData = new FormData();
+        if (prefs != null && prefs.contains(PREFS_ALLOCATION_SASH)) {
+            sashData.top = new FormAttachment(0,
+                    prefs.getInt(PREFS_ALLOCATION_SASH));
+        } else {
+            sashData.top = new FormAttachment(50, 0); // 50% across
+        }
+        sashData.left = new FormAttachment(0, 0);
+        sashData.right = new FormAttachment(100, 0);
+        sash.setLayoutData(sashData);
+
+        data = new FormData();
+        data.top = new FormAttachment(sash, 0);
+        data.bottom = new FormAttachment(100, 0);
+        data.left = new FormAttachment(0, 0);
+        data.right = new FormAttachment(100, 0);
+        mDetailTable.setLayoutData(data);
+
+        // allow resizes, but cap at minPanelWidth
+        sash.addListener(SWT.Selection, new Listener() {
+            @Override
+            public void handleEvent(Event e) {
+                Rectangle sashRect = sash.getBounds();
+                Rectangle panelRect = sash_composite.getClientArea();
+                int bottom = panelRect.height - sashRect.height - minPanelWidth;
+                e.y = Math.max(Math.min(e.y, bottom), minPanelWidth);
+                if (e.y != sashRect.y) {
+                    sashData.top = new FormAttachment(0, e.y);
+                    prefs.setValue(PREFS_ALLOCATION_SASH, e.y);
+                    sash_composite.layout();
+                }
+            }
+        });
+    }
+
+    private void createDetailTable(Composite base) {
+
+        final IPreferenceStore prefs = DdmUiPreferences.getStore();
+
+        mDetailTable = new Table(base, SWT.MULTI | SWT.FULL_SELECTION);
+        mDetailTable.setLayoutData(new GridData(GridData.FILL_BOTH));
+        mDetailTable.setHeaderVisible(true);
+        mDetailTable.setLinesVisible(true);
+
+        TableHelper.createTableColumn(mDetailTable, "Address", SWT.RIGHT,
+                "00000000", PREFS_DETAIL_ADDRESS, prefs); //$NON-NLS-1$
+        TableHelper.createTableColumn(mDetailTable, "Library", SWT.LEFT,
+                "abcdefghijklmnopqrst", PREFS_DETAIL_LIBRARY, prefs); //$NON-NLS-1$
+        TableHelper.createTableColumn(mDetailTable, "Method", SWT.LEFT,
+                "abcdefghijklmnopqrst", PREFS_DETAIL_METHOD, prefs); //$NON-NLS-1$
+        TableHelper.createTableColumn(mDetailTable, "File", SWT.LEFT,
+                "abcdefghijklmnopqrstuvwxyz", PREFS_DETAIL_FILE, prefs); //$NON-NLS-1$
+        TableHelper.createTableColumn(mDetailTable, "Line", SWT.RIGHT,
+                "9,999", PREFS_DETAIL_LINE, prefs); //$NON-NLS-1$
+    }
+
+    private void createAllocationTopHalf(Composite b) {
+        final IPreferenceStore prefs = DdmUiPreferences.getStore();
+
+        Composite base = new Composite(b, SWT.NONE);
+        mAllocationModeTop = base;
+        GridLayout gl = new GridLayout(1, false);
+        gl.marginLeft = gl.marginRight = gl.marginTop = gl.marginBottom = 0;
+        gl.verticalSpacing = 0;
+        base.setLayout(gl);
+        base.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        // horizontal layout for memory total and pages UI
+        mPageUIComposite = new Composite(base, SWT.NONE);
+        mPageUIComposite.setLayoutData(new GridData(
+                GridData.HORIZONTAL_ALIGN_BEGINNING));
+        gl = new GridLayout(3, false);
+        gl.marginLeft = gl.marginRight = gl.marginTop = gl.marginBottom = 0;
+        gl.horizontalSpacing = 0;
+        mPageUIComposite.setLayout(gl);
+
+        // Page UI
+        mPagePreviousButton = new Button(mPageUIComposite, SWT.NONE);
+        mPagePreviousButton.setText("<");
+        mPagePreviousButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                mCurrentPage--;
+                updatePageUI();
+                emptyTables();
+                fillAllocationTable();
+            }
+        });
+
+        mPageNextButton = new Button(mPageUIComposite, SWT.NONE);
+        mPageNextButton.setText(">");
+        mPageNextButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                mCurrentPage++;
+                updatePageUI();
+                emptyTables();
+                fillAllocationTable();
+            }
+        });
+
+        mPageLabel = new Label(mPageUIComposite, SWT.NONE);
+        mPageLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        updatePageUI();
+
+        mAllocationTable = new Table(base, SWT.MULTI | SWT.FULL_SELECTION);
+        mAllocationTable.setLayoutData(new GridData(GridData.FILL_BOTH));
+        mAllocationTable.setHeaderVisible(true);
+        mAllocationTable.setLinesVisible(true);
+
+        TableHelper.createTableColumn(mAllocationTable, "Total", SWT.RIGHT,
+                "9,999,999", PREFS_ALLOC_TOTAL, prefs); //$NON-NLS-1$
+        TableHelper.createTableColumn(mAllocationTable, "Count", SWT.RIGHT,
+                "9,999", PREFS_ALLOC_COUNT, prefs); //$NON-NLS-1$
+        TableHelper.createTableColumn(mAllocationTable, "Size", SWT.RIGHT,
+                "999,999", PREFS_ALLOC_SIZE, prefs); //$NON-NLS-1$
+        TableHelper.createTableColumn(mAllocationTable, "Library", SWT.LEFT,
+                "abcdefghijklmnopqrst", PREFS_ALLOC_LIBRARY, prefs); //$NON-NLS-1$
+        TableHelper.createTableColumn(mAllocationTable, "Method", SWT.LEFT,
+                "abcdefghijklmnopqrst", PREFS_ALLOC_METHOD, prefs); //$NON-NLS-1$
+        TableHelper.createTableColumn(mAllocationTable, "File", SWT.LEFT,
+                "abcdefghijklmnopqrstuvwxyz", PREFS_ALLOC_FILE, prefs); //$NON-NLS-1$
+
+        mAllocationTable.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                // get the selection index
+                int index = mAllocationTable.getSelectionIndex();
+                if (index >= 0 && index < mAllocationTable.getItemCount()) {
+                    TableItem item = mAllocationTable.getItem(index);
+                    if (item != null && item.getData() instanceof NativeAllocationInfo) {
+                        fillDetailTable((NativeAllocationInfo)item.getData());
+                    }
+                }
+            }
+        });
+    }
+
+    private void createLibraryTopHalf(Composite base) {
+        final int minPanelWidth = 60;
+
+        final IPreferenceStore prefs = DdmUiPreferences.getStore();
+
+        // create a composite that'll contain 2 tables horizontally
+        final Composite top = new Composite(base, SWT.NONE);
+        mLibraryModeTopControl = top;
+        top.setLayout(new FormLayout());
+        top.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        // first table: library
+        mLibraryTable = new Table(top, SWT.MULTI | SWT.FULL_SELECTION);
+        mLibraryTable.setLayoutData(new GridData(GridData.FILL_BOTH));
+        mLibraryTable.setHeaderVisible(true);
+        mLibraryTable.setLinesVisible(true);
+
+        TableHelper.createTableColumn(mLibraryTable, "Library", SWT.LEFT,
+                "abcdefghijklmnopqrstuvwxyz", PREFS_LIB_LIBRARY, prefs); //$NON-NLS-1$
+        TableHelper.createTableColumn(mLibraryTable, "Size", SWT.RIGHT,
+                "9,999,999", PREFS_LIB_SIZE, prefs); //$NON-NLS-1$
+        TableHelper.createTableColumn(mLibraryTable, "Count", SWT.RIGHT,
+                "9,999", PREFS_LIB_COUNT, prefs); //$NON-NLS-1$
+
+        mLibraryTable.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                fillLibraryAllocationTable();
+            }
+        });
+
+        final Sash sash = new Sash(top, SWT.VERTICAL);
+
+        // 2nd table: allocation per library
+        mLibraryAllocationTable = new Table(top, SWT.MULTI | SWT.FULL_SELECTION);
+        mLibraryAllocationTable.setLayoutData(new GridData(GridData.FILL_BOTH));
+        mLibraryAllocationTable.setHeaderVisible(true);
+        mLibraryAllocationTable.setLinesVisible(true);
+
+        TableHelper.createTableColumn(mLibraryAllocationTable, "Total",
+                SWT.RIGHT, "9,999,999", PREFS_LIBALLOC_TOTAL, prefs); //$NON-NLS-1$
+        TableHelper.createTableColumn(mLibraryAllocationTable, "Count",
+                SWT.RIGHT, "9,999", PREFS_LIBALLOC_COUNT, prefs); //$NON-NLS-1$
+        TableHelper.createTableColumn(mLibraryAllocationTable, "Size",
+                SWT.RIGHT, "999,999", PREFS_LIBALLOC_SIZE, prefs); //$NON-NLS-1$
+        TableHelper.createTableColumn(mLibraryAllocationTable, "Method",
+                SWT.LEFT, "abcdefghijklmnopqrst", PREFS_LIBALLOC_METHOD, prefs); //$NON-NLS-1$
+
+        mLibraryAllocationTable.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                // get the index of the selection in the library table
+                int index1 = mLibraryTable.getSelectionIndex();
+                // get the index in the library allocation table
+                int index2 = mLibraryAllocationTable.getSelectionIndex();
+                // get the MallocInfo object
+                if (index1 != -1 && index2 != -1) {
+                    LibraryAllocations liballoc = mLibraryAllocations.get(index1);
+                    NativeAllocationInfo info = liballoc.getAllocation(index2);
+                    fillDetailTable(info);
+                }
+            }
+        });
+
+        // form layout data
+        FormData data = new FormData();
+        data.top = new FormAttachment(0, 0);
+        data.bottom = new FormAttachment(100, 0);
+        data.left = new FormAttachment(0, 0);
+        data.right = new FormAttachment(sash, 0);
+        mLibraryTable.setLayoutData(data);
+
+        final FormData sashData = new FormData();
+        if (prefs != null && prefs.contains(PREFS_LIBRARY_SASH)) {
+            sashData.left = new FormAttachment(0,
+                    prefs.getInt(PREFS_LIBRARY_SASH));
+        } else {
+            sashData.left = new FormAttachment(50, 0);
+        }
+        sashData.bottom = new FormAttachment(100, 0);
+        sashData.top = new FormAttachment(0, 0); // 50% across
+        sash.setLayoutData(sashData);
+
+        data = new FormData();
+        data.top = new FormAttachment(0, 0);
+        data.bottom = new FormAttachment(100, 0);
+        data.left = new FormAttachment(sash, 0);
+        data.right = new FormAttachment(100, 0);
+        mLibraryAllocationTable.setLayoutData(data);
+
+        // allow resizes, but cap at minPanelWidth
+        sash.addListener(SWT.Selection, new Listener() {
+            @Override
+            public void handleEvent(Event e) {
+                Rectangle sashRect = sash.getBounds();
+                Rectangle panelRect = top.getClientArea();
+                int right = panelRect.width - sashRect.width - minPanelWidth;
+                e.x = Math.max(Math.min(e.x, right), minPanelWidth);
+                if (e.x != sashRect.x) {
+                    sashData.left = new FormAttachment(0, e.x);
+                    prefs.setValue(PREFS_LIBRARY_SASH, e.y);
+                    top.layout();
+                }
+            }
+        });
+    }
+
+    private void emptyTables() {
+        mAllocationTable.removeAll();
+        mLibraryTable.removeAll();
+        mLibraryAllocationTable.removeAll();
+        mDetailTable.removeAll();
+    }
+
+    private void sortAllocationsPerLibrary() {
+        if (mClientData != null) {
+            mLibraryAllocations.clear();
+
+            // create a hash map of LibraryAllocations to access aggregate
+            // objects already created
+            HashMap<String, LibraryAllocations> libcache =
+                new HashMap<String, LibraryAllocations>();
+
+            // get the allocation count
+            int count = mDisplayedAllocations.size();
+            for (int i = 0; i < count; i++) {
+                NativeAllocationInfo allocInfo = mDisplayedAllocations.get(i);
+
+                NativeStackCallInfo stackCallInfo = allocInfo.getRelevantStackCallInfo();
+                if (stackCallInfo != null) {
+                    String libraryName = stackCallInfo.getLibraryName();
+                    LibraryAllocations liballoc = libcache.get(libraryName);
+                    if (liballoc == null) {
+                        // didn't find a library allocation object already
+                        // created so we create one
+                        liballoc = new LibraryAllocations(libraryName);
+                        // add it to the cache
+                        libcache.put(libraryName, liballoc);
+                        // add it to the list
+                        mLibraryAllocations.add(liballoc);
+                    }
+                    // add the MallocInfo object to it.
+                    liballoc.addAllocation(allocInfo);
+                }
+            }
+            // now that the list is created, we need to compute the size and
+            // sort it by size. This will also sort the MallocInfo objects
+            // inside each LibraryAllocation objects.
+            for (LibraryAllocations liballoc : mLibraryAllocations) {
+                liballoc.computeAllocationSizeAndCount();
+            }
+
+            // now we sort it
+            Collections.sort(mLibraryAllocations,
+                    new Comparator<LibraryAllocations>() {
+                @Override
+                public int compare(LibraryAllocations o1,
+                        LibraryAllocations o2) {
+                    return o2.getSize() - o1.getSize();
+                }
+            });
+        }
+    }
+
+    private void renderBitmap(ClientData cd) {
+        byte[] pixData;
+
+        // Atomically get and clear the heap data.
+        synchronized (cd) {
+            if (serializeHeapData(cd.getVmHeapData()) == false) {
+                // no change, we return.
+                return;
+            }
+
+            pixData = getSerializedData();
+
+            ImageData id = createLinearHeapImage(pixData, 200, mMapPalette);
+            Image image = new Image(mBase.getDisplay(), id);
+            mImage.setImage(image);
+            mImage.pack(true);
+        }
+    }
+
+    /*
+     * Create color palette for map.  Set up titles for legend.
+     */
+    private static PaletteData createPalette() {
+        RGB colors[] = new RGB[NUM_PALETTE_ENTRIES];
+        colors[0]
+                = new RGB(192, 192, 192); // non-heap pixels are gray
+        mMapLegend[0]
+                = "(heap expansion area)";
+
+        colors[1]
+                = new RGB(0, 0, 0);       // free chunks are black
+        mMapLegend[1]
+                = "free";
+
+        colors[HeapSegmentElement.KIND_OBJECT + 2]
+                = new RGB(0, 0, 255);     // objects are blue
+        mMapLegend[HeapSegmentElement.KIND_OBJECT + 2]
+                = "data object";
+
+        colors[HeapSegmentElement.KIND_CLASS_OBJECT + 2]
+                = new RGB(0, 255, 0);     // class objects are green
+        mMapLegend[HeapSegmentElement.KIND_CLASS_OBJECT + 2]
+                = "class object";
+
+        colors[HeapSegmentElement.KIND_ARRAY_1 + 2]
+                = new RGB(255, 0, 0);     // byte/bool arrays are red
+        mMapLegend[HeapSegmentElement.KIND_ARRAY_1 + 2]
+                = "1-byte array (byte[], boolean[])";
+
+        colors[HeapSegmentElement.KIND_ARRAY_2 + 2]
+                = new RGB(255, 128, 0);   // short/char arrays are orange
+        mMapLegend[HeapSegmentElement.KIND_ARRAY_2 + 2]
+                = "2-byte array (short[], char[])";
+
+        colors[HeapSegmentElement.KIND_ARRAY_4 + 2]
+                = new RGB(255, 255, 0);   // obj/int/float arrays are yellow
+        mMapLegend[HeapSegmentElement.KIND_ARRAY_4 + 2]
+                = "4-byte array (object[], int[], float[])";
+
+        colors[HeapSegmentElement.KIND_ARRAY_8 + 2]
+                = new RGB(255, 128, 128); // long/double arrays are pink
+        mMapLegend[HeapSegmentElement.KIND_ARRAY_8 + 2]
+                = "8-byte array (long[], double[])";
+
+        colors[HeapSegmentElement.KIND_UNKNOWN + 2]
+                = new RGB(255, 0, 255);   // unknown objects are cyan
+        mMapLegend[HeapSegmentElement.KIND_UNKNOWN + 2]
+                = "unknown object";
+
+        colors[HeapSegmentElement.KIND_NATIVE + 2]
+                = new RGB(64, 64, 64);    // native objects are dark gray
+        mMapLegend[HeapSegmentElement.KIND_NATIVE + 2]
+                = "non-Java object";
+
+        return new PaletteData(colors);
+    }
+
+    private void saveAllocations(String fileName) {
+        try {
+            PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(fileName)));
+
+            for (NativeAllocationInfo alloc : mAllocations) {
+                out.println(alloc.toString());
+            }
+            out.close();
+        } catch (IOException e) {
+            Log.e("Native", e);
+        }
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/Panel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/Panel.java
new file mode 100644
index 0000000..d910cc7
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/Panel.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib;
+
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+
+/**
+ * Base class for our information panels.
+ */
+public abstract class Panel {
+
+    public final Control createPanel(Composite parent) {
+        Control panelControl = createControl(parent);
+
+        postCreation();
+
+        return panelControl;
+    }
+
+    protected abstract void postCreation();
+
+    /**
+     * Creates a control capable of displaying some information.  This is
+     * called once, when the application is initializing, from the UI thread.
+     */
+    protected abstract Control createControl(Composite parent);
+    
+    /**
+     * Sets the focus to the proper control inside the panel.
+     */
+    public abstract void setFocus();
+}
+
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/PortFieldEditor.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/PortFieldEditor.java
new file mode 100644
index 0000000..533372e
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/PortFieldEditor.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib;
+
+import org.eclipse.jface.preference.IntegerFieldEditor;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * Edit an integer field, validating it as a port number.
+ */
+public class PortFieldEditor extends IntegerFieldEditor {
+
+    public boolean mRecursiveCheck = false;
+
+    public PortFieldEditor(String name, String label, Composite parent) {
+        super(name, label, parent);
+        setValidateStrategy(VALIDATE_ON_KEY_STROKE);
+    }
+
+    /*
+     * Get the current value of the field, as an integer.
+     */
+    public int getCurrentValue() {
+        int val;
+        try {
+            val = Integer.parseInt(getStringValue());
+        }
+        catch (NumberFormatException nfe) {
+            val = -1;
+        }
+        return val;
+    }
+
+    /*
+     * Check the validity of the field.
+     */
+    @Override
+    protected boolean checkState() {
+        if (super.checkState() == false) {
+            return false;
+        }
+        //Log.i("ddms", "check state " + getStringValue());
+        boolean err = false;
+        int val = getCurrentValue();
+        if (val < 1024 || val > 32767) {
+            setErrorMessage("Port must be between 1024 and 32767");
+            err = true;
+        } else {
+            setErrorMessage(null);
+            err = false;
+        }
+        showErrorMessage();
+        return !err;
+    }
+
+    protected void updateCheckState(PortFieldEditor pfe) {
+        pfe.refreshValidState();
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ScreenShotDialog.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ScreenShotDialog.java
new file mode 100644
index 0000000..b0f885a
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ScreenShotDialog.java
@@ -0,0 +1,350 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib;
+
+import com.android.ddmlib.AdbCommandRejectedException;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.RawImage;
+import com.android.ddmlib.TimeoutException;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.ImageTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.PaletteData;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Calendar;
+
+
+/**
+ * Gather a screen shot from the device and save it to a file.
+ */
+public class ScreenShotDialog extends Dialog {
+
+    private Label mBusyLabel;
+    private Label mImageLabel;
+    private Button mSave;
+    private IDevice mDevice;
+    private RawImage mRawImage;
+    private Clipboard mClipboard;
+
+    /** Number of 90 degree rotations applied to the current image */
+    private int mRotateCount = 0;
+
+    /**
+     * Create with default style.
+     */
+    public ScreenShotDialog(Shell parent) {
+        this(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL);
+        mClipboard = new Clipboard(parent.getDisplay());
+    }
+
+    /**
+     * Create with app-defined style.
+     */
+    public ScreenShotDialog(Shell parent, int style) {
+        super(parent, style);
+    }
+
+    /**
+     * Prepare and display the dialog.
+     * @param device The {@link IDevice} from which to get the screenshot.
+     */
+    public void open(IDevice device) {
+        mDevice = device;
+
+        Shell parent = getParent();
+        Shell shell = new Shell(parent, getStyle());
+        shell.setText("Device Screen Capture");
+
+        createContents(shell);
+        shell.pack();
+        shell.open();
+
+        updateDeviceImage(shell);
+
+        Display display = parent.getDisplay();
+        while (!shell.isDisposed()) {
+            if (!display.readAndDispatch())
+                display.sleep();
+        }
+
+    }
+
+    /*
+     * Create the screen capture dialog contents.
+     */
+    private void createContents(final Shell shell) {
+        GridData data;
+
+        final int colCount = 5;
+
+        shell.setLayout(new GridLayout(colCount, true));
+
+        // "refresh" button
+        Button refresh = new Button(shell, SWT.PUSH);
+        refresh.setText("Refresh");
+        data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
+        data.widthHint = 80;
+        refresh.setLayoutData(data);
+        refresh.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                updateDeviceImage(shell);
+                // RawImage only allows us to rotate the image 90 degrees at the time,
+                // so to preserve the current rotation we must call getRotated()
+                // the same number of times the user has done it manually.
+                // TODO: improve the RawImage class.
+                for (int i=0; i < mRotateCount; i++) {
+                    mRawImage = mRawImage.getRotated();
+                }
+                updateImageDisplay(shell);
+            }
+        });
+
+        // "rotate" button
+        Button rotate = new Button(shell, SWT.PUSH);
+        rotate.setText("Rotate");
+        data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
+        data.widthHint = 80;
+        rotate.setLayoutData(data);
+        rotate.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                if (mRawImage != null) {
+                    mRotateCount = (mRotateCount + 1) % 4;
+                    mRawImage = mRawImage.getRotated();
+                    updateImageDisplay(shell);
+                }
+            }
+        });
+
+        // "save" button
+        mSave = new Button(shell, SWT.PUSH);
+        mSave.setText("Save");
+        data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
+        data.widthHint = 80;
+        mSave.setLayoutData(data);
+        mSave.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                saveImage(shell);
+            }
+        });
+
+        Button copy = new Button(shell, SWT.PUSH);
+        copy.setText("Copy");
+        copy.setToolTipText("Copy the screenshot to the clipboard");
+        data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
+        data.widthHint = 80;
+        copy.setLayoutData(data);
+        copy.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                copy();
+            }
+        });
+
+
+        // "done" button
+        Button done = new Button(shell, SWT.PUSH);
+        done.setText("Done");
+        data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
+        data.widthHint = 80;
+        done.setLayoutData(data);
+        done.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                shell.close();
+            }
+        });
+
+        // title/"capturing" label
+        mBusyLabel = new Label(shell, SWT.NONE);
+        mBusyLabel.setText("Preparing...");
+        data = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING);
+        data.horizontalSpan = colCount;
+        mBusyLabel.setLayoutData(data);
+
+        // space for the image
+        mImageLabel = new Label(shell, SWT.BORDER);
+        data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
+        data.horizontalSpan = colCount;
+        mImageLabel.setLayoutData(data);
+        Display display = shell.getDisplay();
+        mImageLabel.setImage(ImageLoader.createPlaceHolderArt(
+                display, 50, 50, display.getSystemColor(SWT.COLOR_BLUE)));
+
+
+        shell.setDefaultButton(done);
+    }
+
+    /**
+     * Copies the content of {@link #mImageLabel} to the clipboard.
+     */
+    private void copy() {
+        mClipboard.setContents(
+                new Object[] {
+                        mImageLabel.getImage().getImageData()
+                }, new Transfer[] {
+                        ImageTransfer.getInstance()
+                });
+    }
+
+    /**
+     * Captures a new image from the device, and display it.
+     */
+    private void updateDeviceImage(Shell shell) {
+        mBusyLabel.setText("Capturing...");     // no effect
+
+        shell.setCursor(shell.getDisplay().getSystemCursor(SWT.CURSOR_WAIT));
+
+        mRawImage = getDeviceImage();
+
+        updateImageDisplay(shell);
+    }
+
+    /**
+     * Updates the display with {@link #mRawImage}.
+     * @param shell
+     */
+    private void updateImageDisplay(Shell shell) {
+        Image image;
+        if (mRawImage == null) {
+            Display display = shell.getDisplay();
+            image = ImageLoader.createPlaceHolderArt(
+                    display, 320, 240, display.getSystemColor(SWT.COLOR_BLUE));
+
+            mSave.setEnabled(false);
+            mBusyLabel.setText("Screen not available");
+        } else {
+            // convert raw data to an Image.
+            PaletteData palette = new PaletteData(
+                    mRawImage.getRedMask(),
+                    mRawImage.getGreenMask(),
+                    mRawImage.getBlueMask());
+
+            ImageData imageData = new ImageData(mRawImage.width, mRawImage.height,
+                    mRawImage.bpp, palette, 1, mRawImage.data);
+            image = new Image(getParent().getDisplay(), imageData);
+
+            mSave.setEnabled(true);
+            mBusyLabel.setText("Captured image:");
+        }
+
+        mImageLabel.setImage(image);
+        mImageLabel.pack();
+        shell.pack();
+
+        // there's no way to restore old cursor; assume it's ARROW
+        shell.setCursor(shell.getDisplay().getSystemCursor(SWT.CURSOR_ARROW));
+    }
+
+    /**
+     * Grabs an image from an ADB-connected device and returns it as a {@link RawImage}.
+     */
+    private RawImage getDeviceImage() {
+        try {
+            return mDevice.getScreenshot();
+        }
+        catch (IOException ioe) {
+            Log.w("ddms", "Unable to get frame buffer: " + ioe.getMessage());
+            return null;
+        } catch (TimeoutException e) {
+            Log.w("ddms", "Unable to get frame buffer: timeout ");
+            return null;
+        } catch (AdbCommandRejectedException e) {
+            Log.w("ddms", "Unable to get frame buffer: " + e.getMessage());
+            return null;
+        }
+    }
+
+    /*
+     * Prompt the user to save the image to disk.
+     */
+    private void saveImage(Shell shell) {
+        FileDialog dlg = new FileDialog(shell, SWT.SAVE);
+
+        Calendar now = Calendar.getInstance();
+        String fileName = String.format("device-%tF-%tH%tM%tS.png",
+                now, now, now, now);
+
+        dlg.setText("Save image...");
+        dlg.setFileName(fileName);
+
+        String lastDir = DdmUiPreferences.getStore().getString("lastImageSaveDir");
+        if (lastDir.length() == 0) {
+            lastDir = DdmUiPreferences.getStore().getString("imageSaveDir");
+        }
+        dlg.setFilterPath(lastDir);
+        dlg.setFilterNames(new String[] {
+            "PNG Files (*.png)"
+        });
+        dlg.setFilterExtensions(new String[] {
+            "*.png" //$NON-NLS-1$
+        });
+
+        fileName = dlg.open();
+        if (fileName != null) {
+            // FileDialog.getFilterPath() does NOT always return the current
+            // directory of the FileDialog; on the Mac it sometimes just returns
+            // the value the dialog was initialized with. It does however return
+            // the full path as its return value, so just pick the path from
+            // there.
+            if (!fileName.endsWith(".png")) {
+                fileName = fileName + ".png";
+            }
+
+            String saveDir = new File(fileName).getParent();
+            if (saveDir != null) {
+                DdmUiPreferences.getStore().setValue("lastImageSaveDir", saveDir);
+            }
+
+            Log.d("ddms", "Saving image to " + fileName);
+            ImageData imageData = mImageLabel.getImage().getImageData();
+
+            try {
+                org.eclipse.swt.graphics.ImageLoader loader =
+                        new org.eclipse.swt.graphics.ImageLoader();
+
+                loader.data = new ImageData[] { imageData };
+                loader.save(fileName, SWT.IMAGE_PNG);
+            }
+            catch (SWTException e) {
+                Log.w("ddms", "Unable to save " + fileName + ": " + e.getMessage());
+            }
+        }
+    }
+
+}
+
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SelectionDependentPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SelectionDependentPanel.java
new file mode 100644
index 0000000..e6d2211
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SelectionDependentPanel.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.IDevice;
+
+/**
+ * A Panel that requires {@link Device}/{@link Client} selection notifications.
+ */
+public abstract class SelectionDependentPanel extends Panel {
+    private IDevice mCurrentDevice = null;
+    private Client mCurrentClient = null;
+
+    /**
+     * Returns the current {@link Device}.
+     * @return the current device or null if none are selected.
+     */
+    protected final IDevice getCurrentDevice() {
+        return mCurrentDevice;
+    }
+
+    /**
+     * Returns the current {@link Client}.
+     * @return the current client or null if none are selected.
+     */
+    protected final Client getCurrentClient() {
+        return mCurrentClient;
+    }
+
+    /**
+     * Sent when a new device is selected.
+     * @param selectedDevice the selected device.
+     */
+    public final void deviceSelected(IDevice selectedDevice) {
+        if (selectedDevice != mCurrentDevice) {
+            mCurrentDevice = selectedDevice;
+            deviceSelected();
+        }
+    }
+
+    /**
+     * Sent when a new client is selected.
+     * @param selectedClient the selected client.
+     */
+    public final void clientSelected(Client selectedClient) {
+        if (selectedClient != mCurrentClient) {
+            mCurrentClient = selectedClient;
+            clientSelected();
+        }
+    }
+
+    /**
+     * Sent when a new device is selected. The new device can be accessed
+     * with {@link #getCurrentDevice()}.
+     */
+    public abstract void deviceSelected();
+
+    /**
+     * Sent when a new client is selected. The new client can be accessed
+     * with {@link #getCurrentClient()}.
+     */
+    public abstract void clientSelected();
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/StackTracePanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/StackTracePanel.java
new file mode 100644
index 0000000..b00120b
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/StackTracePanel.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2008 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.ddmuilib;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.IStackTraceInfo;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Table;
+
+/**
+ * Stack Trace Panel.
+ * <p/>This is not a panel in the regular sense. Instead this is just an object around the creation
+ * and management of a Stack Trace display.
+ * <p/>UI creation is done through
+ * {@link #createPanel(Composite, String, IPreferenceStore)}.
+ *
+ */
+public final class StackTracePanel {
+
+    private static ISourceRevealer sSourceRevealer;
+
+    private Table mStackTraceTable;
+    private TableViewer mStackTraceViewer;
+
+    private Client mCurrentClient;
+
+
+    /**
+     * Content Provider to display the stack trace of a thread.
+     * Expected input is a {@link IStackTraceInfo} object.
+     */
+    private static class StackTraceContentProvider implements IStructuredContentProvider {
+        @Override
+        public Object[] getElements(Object inputElement) {
+            if (inputElement instanceof IStackTraceInfo) {
+                // getElement cannot return null, so we return an empty array
+                // if there's no stack trace
+                StackTraceElement trace[] = ((IStackTraceInfo)inputElement).getStackTrace();
+                if (trace != null) {
+                    return trace;
+                }
+            }
+
+            return new Object[0];
+        }
+
+        @Override
+        public void dispose() {
+            // pass
+        }
+
+        @Override
+        public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+            // pass
+        }
+    }
+
+
+    /**
+     * A Label Provider to use with {@link StackTraceContentProvider}. It expects the elements to be
+     * of type {@link StackTraceElement}.
+     */
+    private static class StackTraceLabelProvider implements ITableLabelProvider {
+
+        @Override
+        public Image getColumnImage(Object element, int columnIndex) {
+            return null;
+        }
+
+        @Override
+        public String getColumnText(Object element, int columnIndex) {
+            if (element instanceof StackTraceElement && columnIndex == 0) {
+                StackTraceElement traceElement = (StackTraceElement) element;
+                return "  at " + traceElement.toString();
+            }
+            return null;
+        }
+
+        @Override
+        public void addListener(ILabelProviderListener listener) {
+            // pass
+        }
+
+        @Override
+        public void dispose() {
+            // pass
+        }
+
+        @Override
+        public boolean isLabelProperty(Object element, String property) {
+            // pass
+            return false;
+        }
+
+        @Override
+        public void removeListener(ILabelProviderListener listener) {
+            // pass
+        }
+    }
+
+    /**
+     * Classes which implement this interface provide a method that is able to reveal a method
+     * in a source editor
+     */
+    public interface ISourceRevealer {
+        /**
+         * Sent to reveal a particular line in a source editor
+         * @param applicationName the name of the application running the source.
+         * @param className the fully qualified class name
+         * @param line the line to reveal
+         */
+        public void reveal(String applicationName, String className, int line);
+    }
+
+
+    /**
+     * Sets the {@link ISourceRevealer} object able to reveal source code in a source editor.
+     * @param revealer
+     */
+    public static void setSourceRevealer(ISourceRevealer revealer) {
+        sSourceRevealer = revealer;
+    }
+
+    /**
+     * Creates the controls for the StrackTrace display.
+     * <p/>This method will set the parent {@link Composite} to use a {@link GridLayout} with
+     * 2 columns.
+     * @param parent the parent composite.
+     * @param prefs_stack_column
+     * @param store
+     */
+    public Table createPanel(Composite parent, String prefs_stack_column,
+            IPreferenceStore store) {
+
+        mStackTraceTable = new Table(parent, SWT.MULTI | SWT.FULL_SELECTION);
+        mStackTraceTable.setHeaderVisible(false);
+        mStackTraceTable.setLinesVisible(false);
+
+        TableHelper.createTableColumn(
+                mStackTraceTable,
+                "Info",
+                SWT.LEFT,
+                "SomeLongClassName.method(android/somepackage/someotherpackage/somefile.java:99999)", //$NON-NLS-1$
+                prefs_stack_column, store);
+
+        mStackTraceViewer = new TableViewer(mStackTraceTable);
+        mStackTraceViewer.setContentProvider(new StackTraceContentProvider());
+        mStackTraceViewer.setLabelProvider(new StackTraceLabelProvider());
+
+        mStackTraceViewer.addDoubleClickListener(new IDoubleClickListener() {
+            @Override
+            public void doubleClick(DoubleClickEvent event) {
+                if (sSourceRevealer != null && mCurrentClient != null) {
+                    // get the selected stack trace element
+                    ISelection selection = mStackTraceViewer.getSelection();
+
+                    if (selection instanceof IStructuredSelection) {
+                        IStructuredSelection structuredSelection = (IStructuredSelection)selection;
+                        Object object = structuredSelection.getFirstElement();
+                        if (object instanceof StackTraceElement) {
+                            StackTraceElement traceElement = (StackTraceElement)object;
+
+                            if (traceElement.isNativeMethod() == false) {
+                                sSourceRevealer.reveal(
+                                        mCurrentClient.getClientData().getClientDescription(),
+                                        traceElement.getClassName(),
+                                        traceElement.getLineNumber());
+                            }
+                        }
+                    }
+                }
+            }
+        });
+
+        return mStackTraceTable;
+    }
+
+    /**
+     * Sets the input for the {@link TableViewer}.
+     * @param input the {@link IStackTraceInfo} that will provide the viewer with the list of
+     * {@link StackTraceElement}
+     */
+    public void setViewerInput(IStackTraceInfo input) {
+        mStackTraceViewer.setInput(input);
+        mStackTraceViewer.refresh();
+    }
+
+    /**
+     * Sets the current client running the stack trace.
+     * @param currentClient the {@link Client}.
+     */
+    public void setCurrentClient(Client currentClient) {
+        mCurrentClient = currentClient;
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressHelper.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressHelper.java
new file mode 100644
index 0000000..732de59
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressHelper.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2010 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.ddmuilib;
+
+import com.android.ddmlib.SyncException;
+import com.android.ddmlib.SyncService;
+import com.android.ddmlib.SyncService.ISyncProgressMonitor;
+import com.android.ddmlib.TimeoutException;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.jface.dialogs.ProgressMonitorDialog;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.swt.widgets.Shell;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * Helper class to run a Sync in a {@link ProgressMonitorDialog}.
+ */
+public class SyncProgressHelper {
+
+    /**
+     * a runnable class run with an {@link ISyncProgressMonitor}.
+     */
+    public interface SyncRunnable {
+        /** Runs the sync action */
+        void run(ISyncProgressMonitor monitor) throws SyncException, IOException, TimeoutException;
+        /** close the {@link SyncService} */
+        void close();
+    }
+
+    /**
+     * Runs a {@link SyncRunnable} in a {@link ProgressMonitorDialog}.
+     * @param runnable The {@link SyncRunnable} to run.
+     * @param progressMessage the message to display in the progress dialog
+     * @param parentShell the parent shell for the progress dialog.
+     *
+     * @throws InvocationTargetException
+     * @throws InterruptedException
+     * @throws SyncException if an error happens during the push of the package on the device.
+     * @throws IOException
+     * @throws TimeoutException
+     */
+    public static void run(final SyncRunnable runnable, final String progressMessage,
+            final Shell parentShell)
+            throws InvocationTargetException, InterruptedException, SyncException, IOException,
+            TimeoutException {
+
+        final Exception[] result = new Exception[1];
+        new ProgressMonitorDialog(parentShell).run(true, true, new IRunnableWithProgress() {
+            @Override
+            public void run(IProgressMonitor monitor) {
+                try {
+                    runnable.run(new SyncProgressMonitor(monitor, progressMessage));
+                } catch (Exception e) {
+                    result[0] = e;
+                } finally {
+                    runnable.close();
+                }
+            }
+        });
+
+        if (result[0] instanceof SyncException) {
+            SyncException se = (SyncException)result[0];
+            if (se.wasCanceled()) {
+                // no need to throw this
+                return;
+            }
+            throw se;
+        }
+
+        // just do some casting so that the method declaration matches what's thrown.
+        if (result[0] instanceof TimeoutException) {
+            throw (TimeoutException)result[0];
+        }
+
+        if (result[0] instanceof IOException) {
+            throw (IOException)result[0];
+        }
+
+        if (result[0] instanceof RuntimeException) {
+            throw (RuntimeException)result[0];
+        }
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressMonitor.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressMonitor.java
new file mode 100644
index 0000000..4254f67
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressMonitor.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2009 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.ddmuilib;
+
+import com.android.ddmlib.SyncService.ISyncProgressMonitor;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+
+/**
+ * Implementation of the {@link ISyncProgressMonitor} wrapping an Eclipse {@link IProgressMonitor}.
+ */
+public class SyncProgressMonitor implements ISyncProgressMonitor {
+
+    private IProgressMonitor mMonitor;
+    private String mName;
+
+    public SyncProgressMonitor(IProgressMonitor monitor, String name) {
+        mMonitor = monitor;
+        mName = name;
+    }
+
+    @Override
+    public void start(int totalWork) {
+        mMonitor.beginTask(mName, totalWork);
+    }
+
+    @Override
+    public void stop() {
+        mMonitor.done();
+    }
+
+    @Override
+    public void advance(int work) {
+        mMonitor.worked(work);
+    }
+
+    @Override
+    public boolean isCanceled() {
+        return mMonitor.isCanceled();
+    }
+
+    @Override
+    public void startSubTask(String name) {
+        mMonitor.subTask(name);
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SysinfoPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SysinfoPanel.java
new file mode 100644
index 0000000..8ba2171
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SysinfoPanel.java
@@ -0,0 +1,907 @@
+/*
+ * Copyright (C) 2008 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.ddmuilib;
+
+import com.android.ddmlib.AdbCommandRejectedException;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.IShellOutputReceiver;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.NullOutputReceiver;
+import com.android.ddmlib.ShellCommandUnresponsiveException;
+import com.android.ddmlib.TimeoutException;
+import com.android.ddmuilib.SysinfoPanel.BugReportParser.GfxProfileData;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Lists;
+
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.StackLayout;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.data.category.DefaultCategoryDataset;
+import org.jfree.data.general.DefaultPieDataset;
+import org.jfree.experimental.chart.swt.ChartComposite;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Displays system information graphs obtained from a bugreport file or device.
+ */
+public class SysinfoPanel extends TablePanel implements IShellOutputReceiver {
+
+    // UI components
+    private Label mLabel;
+    private Button mFetchButton;
+    private Combo mDisplayMode;
+
+    private DefaultPieDataset mDataset;
+    private DefaultCategoryDataset mBarDataSet;
+
+    private StackLayout mStackLayout;
+    private Composite mChartComposite;
+    private Composite mPieChartComposite;
+    private Composite mStackedBarComposite;
+
+    // The bugreport file to process
+    private File mDataFile;
+
+    // To get output from adb commands
+    private FileOutputStream mTempStream;
+
+    // Selects the current display: MODE_CPU, etc.
+    private int mMode = 0;
+    private String mGfxPackageName;
+
+    private static final int MODE_CPU = 0;
+    private static final int MODE_MEMINFO = 1;
+    private static final int MODE_GFXINFO = 2;
+
+    // argument to dumpsys; section in the bugreport holding the data
+    private static final String DUMP_COMMAND[] = {
+        "dumpsys cpuinfo",
+        "cat /proc/meminfo ; procrank",
+        "dumpsys gfxinfo",
+    };
+
+    private static final String CAPTIONS[] = {
+        "CPU load",
+        "Memory usage",
+        "Frame Render Time",
+    };
+
+    /** Shell property that controls whether graphics profiling is enabled or not. */
+    private static final String PROP_GFX_PROFILING = "debug.hwui.profile"; //$NON-NLS-1$
+
+    /**
+     * Generates the dataset to display.
+     *
+     * @param file The bugreport file to process.
+     */
+    private void generateDataset(File file) {
+        if (file == null) {
+            return;
+        }
+        try {
+            BufferedReader br = getBugreportReader(file);
+            if (mMode == MODE_CPU) {
+                readCpuDataset(br);
+            } else if (mMode == MODE_MEMINFO) {
+                readMeminfoDataset(br);
+            } else if (mMode == MODE_GFXINFO) {
+                readGfxInfoDataset(br);
+            }
+            br.close();
+        } catch (IOException e) {
+            Log.e("DDMS", e);
+        }
+    }
+
+    /**
+     * Sent when a new device is selected. The new device can be accessed with
+     * {@link #getCurrentDevice()}
+     */
+    @Override
+    public void deviceSelected() {
+        if (getCurrentDevice() != null) {
+            mFetchButton.setEnabled(true);
+            loadFromDevice();
+        } else {
+            mFetchButton.setEnabled(false);
+        }
+    }
+
+    /**
+     * Sent when a new client is selected. The new client can be accessed with
+     * {@link #getCurrentClient()}.
+     */
+    @Override
+    public void clientSelected() {
+    }
+
+    /**
+     * Sets the focus to the proper control inside the panel.
+     */
+    @Override
+    public void setFocus() {
+        mDisplayMode.setFocus();
+    }
+
+    /**
+     * Fetches a new bugreport from the device and updates the display.
+     * Fetching is asynchronous.  See also addOutput, flush, and isCancelled.
+     */
+    private void loadFromDevice() {
+        clearDataSet();
+
+        if (mMode == MODE_GFXINFO) {
+            boolean en = isGfxProfilingEnabled();
+            if (!en) {
+                if (enableGfxProfiling()) {
+                    MessageDialog.openInformation(Display.getCurrent().getActiveShell(),
+                            "DDMS",
+                            "Graphics profiling was enabled on the device.\n" +
+                            "It may be necessary to relaunch your application to see profile information.");
+                } else {
+                    MessageDialog.openError(Display.getCurrent().getActiveShell(),
+                            "DDMS",
+                            "Unexpected error enabling graphics profiling on device.\n");
+                    return;
+                }
+            }
+        }
+
+        final String command = getDumpsysCommand(mMode);
+        if (command == null) {
+            return;
+        }
+
+        Thread t = new Thread(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    initShellOutputBuffer();
+                    if (mMode == MODE_MEMINFO) {
+                        // Hack to add bugreport-style section header for meminfo
+                        mTempStream.write("------ MEMORY INFO ------\n".getBytes());
+                    }
+                    getCurrentDevice().executeShellCommand(command, SysinfoPanel.this);
+                } catch (IOException e) {
+                    Log.e("DDMS", e);
+                } catch (TimeoutException e) {
+                    Log.e("DDMS", e);
+                } catch (AdbCommandRejectedException e) {
+                    Log.e("DDMS", e);
+                } catch (ShellCommandUnresponsiveException e) {
+                    Log.e("DDMS", e);
+                }
+            }
+        }, "Sysinfo Output Collector");
+        t.start();
+    }
+
+    private boolean isGfxProfilingEnabled() {
+        IDevice device = getCurrentDevice();
+        if (device == null) {
+            return false;
+        }
+
+        String prop;
+        try {
+            prop = device.getPropertySync(PROP_GFX_PROFILING);
+            return Boolean.valueOf(prop);
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    private boolean enableGfxProfiling() {
+        IDevice device = getCurrentDevice();
+        if (device == null) {
+            return false;
+        }
+
+        try {
+            device.executeShellCommand("setprop " + PROP_GFX_PROFILING + " true",
+                    new NullOutputReceiver());
+        } catch (Exception e) {
+            return false;
+        }
+
+        return true;
+    }
+
+    private String getDumpsysCommand(int mode) {
+        if (mode == MODE_GFXINFO) {
+            Client c = getCurrentClient();
+            if (c == null) {
+                return null;
+            }
+
+            ClientData cd = c.getClientData();
+            if (cd == null) {
+                return null;
+            }
+
+            mGfxPackageName = cd.getClientDescription();
+            if (mGfxPackageName == null) {
+                return null;
+            }
+
+            return "dumpsys gfxinfo " + mGfxPackageName;
+        } else if (mode < DUMP_COMMAND.length) {
+            return DUMP_COMMAND[mode];
+        }
+
+        return null;
+    }
+
+    /**
+     * Initializes temporary output file for executeShellCommand().
+     *
+     * @throws IOException on file error
+     */
+    void initShellOutputBuffer() throws IOException {
+        mDataFile = File.createTempFile("ddmsfile", ".txt");
+        mDataFile.deleteOnExit();
+        mTempStream = new FileOutputStream(mDataFile);
+    }
+
+    /**
+     * Adds output to the temp file. IShellOutputReceiver method. Called by
+     * executeShellCommand().
+     */
+    @Override
+    public void addOutput(byte[] data, int offset, int length) {
+        try {
+            mTempStream.write(data, offset, length);
+        } catch (IOException e) {
+            Log.e("DDMS", e);
+        }
+    }
+
+    /**
+     * Processes output from shell command. IShellOutputReceiver method. The
+     * output is passed to generateDataset(). Called by executeShellCommand() on
+     * completion.
+     */
+    @Override
+    public void flush() {
+        if (mTempStream != null) {
+            try {
+                mTempStream.close();
+                generateDataset(mDataFile);
+                mTempStream = null;
+                mDataFile = null;
+            } catch (IOException e) {
+                Log.e("DDMS", e);
+            }
+        }
+    }
+
+    /**
+     * IShellOutputReceiver method.
+     *
+     * @return false - don't cancel
+     */
+    @Override
+    public boolean isCancelled() {
+        return false;
+    }
+
+    /**
+     * Create our controls for the UI panel.
+     */
+    @Override
+    protected Control createControl(Composite parent) {
+        Composite top = new Composite(parent, SWT.NONE);
+        top.setLayout(new GridLayout(1, false));
+        top.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        Composite buttons = new Composite(top, SWT.NONE);
+        buttons.setLayout(new RowLayout());
+
+        mDisplayMode = new Combo(buttons, SWT.PUSH);
+        for (String mode : CAPTIONS) {
+            mDisplayMode.add(mode);
+        }
+        mDisplayMode.select(mMode);
+        mDisplayMode.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                mMode = mDisplayMode.getSelectionIndex();
+                if (mDataFile != null) {
+                    generateDataset(mDataFile);
+                } else if (getCurrentDevice() != null) {
+                    loadFromDevice();
+                }
+            }
+        });
+
+        mFetchButton = new Button(buttons, SWT.PUSH);
+        mFetchButton.setText("Update from Device");
+        mFetchButton.setEnabled(false);
+        mFetchButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                loadFromDevice();
+            }
+        });
+
+        mLabel = new Label(top, SWT.NONE);
+        mLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        mChartComposite = new Composite(top, SWT.NONE);
+        mChartComposite.setLayoutData(new GridData(GridData.FILL_BOTH));
+        mStackLayout = new StackLayout();
+        mChartComposite.setLayout(mStackLayout);
+
+        mPieChartComposite = createPieChartComposite(mChartComposite);
+        mStackedBarComposite = createStackedBarComposite(mChartComposite);
+
+        mStackLayout.topControl = mPieChartComposite;
+
+        return top;
+    }
+
+    private Composite createStackedBarComposite(Composite chartComposite) {
+        mBarDataSet = new DefaultCategoryDataset();
+        JFreeChart chart = ChartFactory.createStackedBarChart("Per Frame Rendering Time",
+                "Frame #", "Time (ms)", mBarDataSet, PlotOrientation.VERTICAL,
+                true /* legend */, true /* tooltips */, false /* urls */);
+
+        ChartComposite c = newChartComposite(chart, chartComposite);
+        c.setLayoutData(new GridData(GridData.FILL_BOTH));
+        return c;
+    }
+
+    private Composite createPieChartComposite(Composite chartComposite) {
+        mDataset = new DefaultPieDataset();
+        JFreeChart chart = ChartFactory.createPieChart("", mDataset, false
+                /* legend */, true/* tooltips */, false /* urls */);
+
+        ChartComposite c = newChartComposite(chart, chartComposite);
+        c.setLayoutData(new GridData(GridData.FILL_BOTH));
+        return c;
+    }
+
+    private ChartComposite newChartComposite(JFreeChart chart, Composite parent) {
+        return new ChartComposite(parent,
+                SWT.BORDER, chart,
+                ChartComposite.DEFAULT_HEIGHT,
+                ChartComposite.DEFAULT_HEIGHT,
+                ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH,
+                ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT,
+                3000,
+                // max draw width. We don't want it to zoom, so we put a big number
+                3000,
+                // max draw height. We don't want it to zoom, so we put a big number
+                true,  // off-screen buffer
+                true,  // properties
+                true,  // save
+                true,  // print
+                false,  // zoom
+                true);
+    }
+
+    @Override
+    public void clientChanged(final Client client, int changeMask) {
+        // Don't care
+    }
+
+    /**
+     * Helper to open a bugreport and skip to the specified section.
+     *
+     * @param file File to open
+     * @return Reader to bugreport file
+     * @throws java.io.IOException on file error
+     */
+    private BufferedReader getBugreportReader(File file) throws
+            IOException {
+        return new BufferedReader(new FileReader(file));
+    }
+
+    /**
+     * Parse the time string generated by BatteryStats.
+     * A typical new-format string is "11d 13h 45m 39s 999ms".
+     * A typical old-format string is "12.3 sec".
+     * @return time in ms
+     */
+    private static long parseTimeMs(String s) {
+        long total = 0;
+        // Matches a single component e.g. "12.3 sec" or "45ms"
+        Pattern p = Pattern.compile("([\\d\\.]+)\\s*([a-z]+)");
+        Matcher m = p.matcher(s);
+        while (m.find()) {
+            String label = m.group(2);
+            if ("sec".equals(label)) {
+                // Backwards compatibility with old time format
+                total += (long) (Double.parseDouble(m.group(1)) * 1000);
+                continue;
+            }
+            long value = Integer.parseInt(m.group(1));
+            if ("d".equals(label)) {
+                total += value * 24 * 60 * 60 * 1000;
+            } else if ("h".equals(label)) {
+                total += value * 60 * 60 * 1000;
+            } else if ("m".equals(label)) {
+                total += value * 60 * 1000;
+            } else if ("s".equals(label)) {
+                total += value * 1000;
+            } else if ("ms".equals(label)) {
+                total += value;
+            }
+        }
+        return total;
+    }
+
+    public static final class BugReportParser {
+        public static final class DataValue {
+            final String name;
+            final double value;
+
+            public DataValue(String n, double v) {
+                name = n;
+                value = v;
+            }
+        };
+
+        /** Components of the time it takes to draw a single frame. */
+        public static final class GfxProfileData {
+            /** draw time (time spent building display lists) in ms */
+            final double draw;
+
+            /** process time (time spent by Android's 2D renderer to execute display lists) (ms) */
+            final double process;
+
+            /** execute time (time spent to send frame to the compositor) in ms */
+            final double execute;
+
+            public GfxProfileData(double draw, double process, double execute) {
+                this.draw = draw;
+                this.process = process;
+                this.execute = execute;
+            }
+        }
+
+        public static List<GfxProfileData> parseGfxInfo(BufferedReader br) throws IOException {
+            Pattern headerPattern = Pattern.compile("\\s+Draw\\s+Process\\s+Execute");
+
+            String line = null;
+            while ((line = br.readLine()) != null) {
+                Matcher m = headerPattern.matcher(line);
+                if (m.find()) {
+                    break;
+                }
+            }
+
+            if (line == null) {
+                return Collections.emptyList();
+            }
+
+            // parse something like: "  0.85    1.10    0.61\n", 3 doubles basically
+            Pattern dataPattern =
+                    Pattern.compile("(\\d*\\.\\d+)\\s+(\\d*\\.\\d+)\\s+(\\d*\\.\\d+)");
+
+            List<GfxProfileData> data = new ArrayList<BugReportParser.GfxProfileData>(128);
+            while ((line = br.readLine()) != null) {
+                Matcher m = dataPattern.matcher(line);
+                if (!m.find()) {
+                    break;
+                }
+
+                double draw = safeParseDouble(m.group(1));
+                double process = safeParseDouble(m.group(2));
+                double execute = safeParseDouble(m.group(3));
+
+                data.add(new GfxProfileData(draw, process, execute));
+            }
+
+            return data;
+        }
+
+        /**
+         * Processes wakelock information from bugreport. Updates mDataset with the
+         * new data.
+         *
+         * @param br Reader providing the content
+         * @throws IOException if error reading file
+         */
+        public static List<DataValue> readWakelockDataset(BufferedReader br) throws IOException {
+            List<DataValue> results = new ArrayList<DataValue>();
+
+            Pattern lockPattern = Pattern.compile("Wake lock (\\S+): (.+) partial");
+            Pattern totalPattern = Pattern.compile("Total: (.+) uptime");
+            double total = 0;
+            boolean inCurrent = false;
+
+            while (true) {
+                String line = br.readLine();
+                if (line == null || line.startsWith("DUMP OF SERVICE")) {
+                    // Done, or moved on to the next service
+                    break;
+                }
+                if (line.startsWith("Current Battery Usage Statistics")) {
+                    inCurrent = true;
+                } else if (inCurrent) {
+                    Matcher m = lockPattern.matcher(line);
+                    if (m.find()) {
+                        double value = parseTimeMs(m.group(2)) / 1000.;
+                        results.add(new DataValue(m.group(1), value));
+                        total -= value;
+                    } else {
+                        m = totalPattern.matcher(line);
+                        if (m.find()) {
+                            total += parseTimeMs(m.group(1)) / 1000.;
+                        }
+                    }
+                }
+            }
+            if (total > 0) {
+                results.add(new DataValue("Unlocked", total));
+            }
+
+            return results;
+        }
+
+        /**
+         * Processes alarm information from bugreport. Updates mDataset with the new
+         * data.
+         *
+         * @param br Reader providing the content
+         * @throws IOException if error reading file
+         */
+        public static List<DataValue> readAlarmDataset(BufferedReader br) throws IOException {
+            List<DataValue> results = new ArrayList<DataValue>();
+            Pattern pattern = Pattern.compile("(\\d+) alarms: Intent .*\\.([^. ]+) flags");
+
+            while (true) {
+                String line = br.readLine();
+                if (line == null || line.startsWith("DUMP OF SERVICE")) {
+                    // Done, or moved on to the next service
+                    break;
+                }
+                Matcher m = pattern.matcher(line);
+                if (m.find()) {
+                    long count = Long.parseLong(m.group(1));
+                    String name = m.group(2);
+                    results.add(new DataValue(name, count));
+                }
+            }
+
+            return results;
+        }
+
+        /**
+         * Processes cpu load information from bugreport. Updates mDataset with the
+         * new data.
+         *
+         * @param br Reader providing the content
+         * @throws IOException if error reading file
+         */
+        public static List<DataValue> readCpuDataset(BufferedReader br) throws IOException {
+            List<DataValue> results = new ArrayList<DataValue>();
+            Pattern pattern1 = Pattern.compile("(\\S+): (\\S+)% = (.+)% user . (.+)% kernel");
+            Pattern pattern2 = Pattern.compile("(\\S+)% (\\S+): (.+)% user . (.+)% kernel");
+
+            while (true) {
+                String line = br.readLine();
+                if (line == null) {
+                    break;
+                }
+                line = line.trim();
+
+                if (line.startsWith("Load:")) {
+                    continue;
+                }
+
+                String name = "";
+                double user = 0, kernel = 0, both = 0;
+                boolean found = false;
+
+                // try pattern1
+                Matcher m = pattern1.matcher(line);
+                if (m.find()) {
+                    found = true;
+                    name = m.group(1);
+                    both = safeParseLong(m.group(2));
+                    user = safeParseLong(m.group(3));
+                    kernel = safeParseLong(m.group(4));
+                }
+
+                // try pattern2
+                m = pattern2.matcher(line);
+                if (m.find()) {
+                    found = true;
+                    name = m.group(2);
+                    both = safeParseDouble(m.group(1));
+                    user = safeParseDouble(m.group(3));
+                    kernel = safeParseDouble(m.group(4));
+                }
+
+                if (!found) {
+                    continue;
+                }
+
+                if ("TOTAL".equals(name)) {
+                    if (both < 100) {
+                        results.add(new DataValue("Idle", (100 - both)));
+                    }
+                } else {
+                    // Try to make graphs more useful even with rounding;
+                    // log often has 0% user + 0% kernel = 1% total
+                    // We arbitrarily give extra to kernel
+                    if (user > 0) {
+                        results.add(new DataValue(name + " (user)", user));
+                    }
+                    if (kernel > 0) {
+                        results.add(new DataValue(name + " (kernel)" , both - user));
+                    }
+                    if (user == 0 && kernel == 0 && both > 0) {
+                        results.add(new DataValue(name, both));
+                    }
+                }
+
+            }
+
+            return results;
+        }
+
+        private static long safeParseLong(String s) {
+            try {
+                return Long.parseLong(s);
+            } catch (NumberFormatException e) {
+                return 0;
+            }
+        }
+
+        private static double safeParseDouble(String s) {
+            try {
+                return Double.parseDouble(s);
+            } catch (NumberFormatException e) {
+                return 0;
+            }
+        }
+
+        /**
+         * Processes meminfo information from bugreport. Updates mDataset with the
+         * new data.
+         *
+         * @param br Reader providing the content
+         * @throws IOException if error reading file
+         */
+        public static List<DataValue> readMeminfoDataset(BufferedReader br) throws IOException {
+            List<DataValue> results = new ArrayList<DataValue>();
+            Pattern valuePattern = Pattern.compile("(\\d+) kB");
+            long total = 0;
+            long other = 0;
+
+            // Scan meminfo
+            String line = null;
+            while ((line = br.readLine()) != null) {
+                if (line.contains("----")) {
+                    continue;
+                }
+
+                Matcher m = valuePattern.matcher(line);
+                if (m.find()) {
+                    long kb = Long.parseLong(m.group(1));
+                    if (line.startsWith("MemTotal")) {
+                        total = kb;
+                    } else if (line.startsWith("MemFree")) {
+                        results.add(new DataValue("Free", kb));
+                        total -= kb;
+                    } else if (line.startsWith("Slab")) {
+                        results.add(new DataValue("Slab", kb));
+                        total -= kb;
+                    } else if (line.startsWith("PageTables")) {
+                        results.add(new DataValue("PageTables", kb));
+                        total -= kb;
+                    } else if (line.startsWith("Buffers") && kb > 0) {
+                        results.add(new DataValue("Buffers", kb));
+                        total -= kb;
+                    } else if (line.startsWith("Inactive")) {
+                        results.add(new DataValue("Inactive", kb));
+                        total -= kb;
+                    } else if (line.startsWith("MemFree")) {
+                        results.add(new DataValue("Free", kb));
+                        total -= kb;
+                    }
+                } else {
+                    break;
+                }
+            }
+
+            List<DataValue> procRankResults = readProcRankDataset(br, line);
+            for (DataValue procRank : procRankResults) {
+                if (procRank.value > 2000) { // only show processes using > 2000K in memory
+                    results.add(procRank);
+                } else {
+                    other += procRank.value;
+                }
+
+                total -= procRank.value;
+            }
+
+            if (other > 0) {
+                results.add(new DataValue("Other", other));
+            }
+
+            // The Pss calculation is not necessarily accurate as accounting memory to
+            // a process is not accurate. So only if there really is unaccounted for memory do we
+            // add it to the pie.
+            if (total > 0) {
+                results.add(new DataValue("Unknown", total));
+            }
+
+            return results;
+        }
+
+        static List<DataValue> readProcRankDataset(BufferedReader br, String header)
+                throws IOException {
+            List<DataValue> results = new ArrayList<DataValue>();
+
+            if (header == null || !header.contains("PID")) {
+                return results;
+            }
+
+            Splitter PROCRANK_SPLITTER = Splitter.on(' ').omitEmptyStrings().trimResults();
+            List<String> fields = Lists.newArrayList(PROCRANK_SPLITTER.split(header));
+            int pssIndex = fields.indexOf("Pss");
+            int cmdIndex = fields.indexOf("cmdline");
+
+            if (pssIndex == -1 || cmdIndex == -1) {
+                return results;
+            }
+
+            String line;
+            while ((line = br.readLine()) != null) {
+                // Extract pss field from procrank output
+                fields = Lists.newArrayList(PROCRANK_SPLITTER.split(line));
+
+                if (fields.size() < cmdIndex) {
+                    break;
+                }
+
+                String cmdline = fields.get(cmdIndex).replace("/system/bin/", "");
+                String pssInK = fields.get(pssIndex);
+                if (pssInK.endsWith("K")) {
+                    pssInK = pssInK.substring(0, pssInK.length() - 1);
+                }
+                long pss = safeParseLong(pssInK);
+                results.add(new DataValue(cmdline, pss));
+            }
+
+            return results;
+        }
+
+        /**
+         * Processes sync information from bugreport. Updates mDataset with the new
+         * data.
+         *
+         * @param br Reader providing the content
+         * @throws IOException if error reading file
+         */
+        public static List<DataValue> readSyncDataset(BufferedReader br) throws IOException {
+            List<DataValue> results = new ArrayList<DataValue>();
+
+            while (true) {
+                String line = br.readLine();
+                if (line == null || line.startsWith("DUMP OF SERVICE")) {
+                    // Done, or moved on to the next service
+                    break;
+                }
+                if (line.startsWith(" |") && line.length() > 70) {
+                    String authority = line.substring(3, 18).trim();
+                    String duration = line.substring(61, 70).trim();
+                    // Duration is MM:SS or HH:MM:SS (DateUtils.formatElapsedTime)
+                    String durParts[] = duration.split(":");
+                    if (durParts.length == 2) {
+                        long dur = Long.parseLong(durParts[0]) * 60 + Long
+                                .parseLong(durParts[1]);
+                        results.add(new DataValue(authority, dur));
+                    } else if (duration.length() == 3) {
+                        long dur = Long.parseLong(durParts[0]) * 3600
+                                + Long.parseLong(durParts[1]) * 60 + Long
+                                .parseLong(durParts[2]);
+                        results.add(new DataValue(authority, dur));
+                    }
+                }
+            }
+
+            return results;
+        }
+    }
+
+    private void readCpuDataset(BufferedReader br) throws IOException {
+        updatePieDataSet(BugReportParser.readCpuDataset(br), "");
+    }
+
+    private void readMeminfoDataset(BufferedReader br) throws IOException {
+        updatePieDataSet(BugReportParser.readMeminfoDataset(br), "PSS in kB");
+    }
+
+    private void readGfxInfoDataset(BufferedReader br) throws IOException {
+        updateBarChartDataSet(BugReportParser.parseGfxInfo(br),
+                mGfxPackageName == null ? "" : mGfxPackageName);
+    }
+
+    private void clearDataSet() {
+        mLabel.setText("");
+        mDataset.clear();
+        mBarDataSet.clear();
+    }
+
+    private void updatePieDataSet(final List<BugReportParser.DataValue> data, final String label) {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                mLabel.setText(label);
+                mStackLayout.topControl = mPieChartComposite;
+                mChartComposite.layout();
+
+                for (BugReportParser.DataValue d : data) {
+                    mDataset.setValue(d.name, d.value);
+                }
+            }
+        });
+    }
+
+    private void updateBarChartDataSet(final List<GfxProfileData> gfxProfileData,
+            final String label) {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                mLabel.setText(label);
+                mStackLayout.topControl = mStackedBarComposite;
+                mChartComposite.layout();
+
+                for (int i = 0; i < gfxProfileData.size(); i++) {
+                    GfxProfileData d = gfxProfileData.get(i);
+                    String frameNumber = Integer.toString(i);
+
+                    mBarDataSet.addValue(d.draw, "Draw", frameNumber);
+                    mBarDataSet.addValue(d.process, "Process", frameNumber);
+                    mBarDataSet.addValue(d.execute, "Execute", frameNumber);
+                }
+            }
+        });
+    }
+
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/TableHelper.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/TableHelper.java
new file mode 100644
index 0000000..66dcc0a
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/TableHelper.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.ControlListener;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeColumn;
+
+/**
+ * Utility class to help using Table objects.
+ *
+ */
+public final class TableHelper {
+    /**
+     * Create a TableColumn with the specified parameters. If a
+     * <code>PreferenceStore</code> object and a preference entry name String
+     * object are provided then the column will listen to change in its width
+     * and update the preference store accordingly.
+     *
+     * @param parent The Table parent object
+     * @param header The header string
+     * @param style The column style
+     * @param sample_text A sample text to figure out column width if preference
+     *            value is missing
+     * @param pref_name The preference entry name for column width
+     * @param prefs The preference store
+     * @return The TableColumn object that was created
+     */
+    public static TableColumn createTableColumn(Table parent, String header,
+            int style, String sample_text, final String pref_name,
+            final IPreferenceStore prefs) {
+
+        // create the column
+        TableColumn col = new TableColumn(parent, style);
+
+        // if there is no pref store or the entry is missing, we use the sample
+        // text and pack the column.
+        // Otherwise we just read the width from the prefs and apply it.
+        if (prefs == null || prefs.contains(pref_name) == false) {
+            col.setText(sample_text);
+            col.pack();
+
+            // init the prefs store with the current value
+            if (prefs != null) {
+                prefs.setValue(pref_name, col.getWidth());
+            }
+        } else {
+            col.setWidth(prefs.getInt(pref_name));
+        }
+
+        // set the header
+        col.setText(header);
+
+        // if there is a pref store and a pref entry name, then we setup a
+        // listener to catch column resize to put store the new width value.
+        if (prefs != null && pref_name != null) {
+            col.addControlListener(new ControlListener() {
+                @Override
+                public void controlMoved(ControlEvent e) {
+                }
+
+                @Override
+                public void controlResized(ControlEvent e) {
+                    // get the new width
+                    int w = ((TableColumn)e.widget).getWidth();
+
+                    // store in pref store
+                    prefs.setValue(pref_name, w);
+                }
+            });
+        }
+
+        return col;
+    }
+
+    /**
+     * Create a TreeColumn with the specified parameters. If a
+     * <code>PreferenceStore</code> object and a preference entry name String
+     * object are provided then the column will listen to change in its width
+     * and update the preference store accordingly.
+     *
+     * @param parent The Table parent object
+     * @param header The header string
+     * @param style The column style
+     * @param sample_text A sample text to figure out column width if preference
+     *            value is missing
+     * @param pref_name The preference entry name for column width
+     * @param prefs The preference store
+     */
+    public static void createTreeColumn(Tree parent, String header, int style,
+            String sample_text, final String pref_name,
+            final IPreferenceStore prefs) {
+
+        // create the column
+        TreeColumn col = new TreeColumn(parent, style);
+
+        // if there is no pref store or the entry is missing, we use the sample
+        // text and pack the column.
+        // Otherwise we just read the width from the prefs and apply it.
+        if (prefs == null || prefs.contains(pref_name) == false) {
+            col.setText(sample_text);
+            col.pack();
+
+            // init the prefs store with the current value
+            if (prefs != null) {
+                prefs.setValue(pref_name, col.getWidth());
+            }
+        } else {
+            col.setWidth(prefs.getInt(pref_name));
+        }
+
+        // set the header
+        col.setText(header);
+
+        // if there is a pref store and a pref entry name, then we setup a
+        // listener to catch column resize to put store the new width value.
+        if (prefs != null && pref_name != null) {
+            col.addControlListener(new ControlListener() {
+                @Override
+                public void controlMoved(ControlEvent e) {
+                }
+
+                @Override
+                public void controlResized(ControlEvent e) {
+                    // get the new width
+                    int w = ((TreeColumn)e.widget).getWidth();
+
+                    // store in pref store
+                    prefs.setValue(pref_name, w);
+                }
+            });
+        }
+    }
+
+    /**
+     * Create a TreeColumn with the specified parameters. If a
+     * <code>PreferenceStore</code> object and a preference entry name String
+     * object are provided then the column will listen to change in its width
+     * and update the preference store accordingly.
+     *
+     * @param parent The Table parent object
+     * @param header The header string
+     * @param style The column style
+     * @param width the width of the column if the preference value is missing
+     * @param pref_name The preference entry name for column width
+     * @param prefs The preference store
+     */
+    public static void createTreeColumn(Tree parent, String header, int style,
+            int width, final String pref_name,
+            final IPreferenceStore prefs) {
+
+        // create the column
+        TreeColumn col = new TreeColumn(parent, style);
+
+        // if there is no pref store or the entry is missing, we use the sample
+        // text and pack the column.
+        // Otherwise we just read the width from the prefs and apply it.
+        if (prefs == null || prefs.contains(pref_name) == false) {
+            col.setWidth(width);
+
+            // init the prefs store with the current value
+            if (prefs != null) {
+                prefs.setValue(pref_name, width);
+            }
+        } else {
+            col.setWidth(prefs.getInt(pref_name));
+        }
+
+        // set the header
+        col.setText(header);
+
+        // if there is a pref store and a pref entry name, then we setup a
+        // listener to catch column resize to put store the new width value.
+        if (prefs != null && pref_name != null) {
+            col.addControlListener(new ControlListener() {
+                @Override
+                public void controlMoved(ControlEvent e) {
+                }
+
+                @Override
+                public void controlResized(ControlEvent e) {
+                    // get the new width
+                    int w = ((TreeColumn)e.widget).getWidth();
+
+                    // store in pref store
+                    prefs.setValue(pref_name, w);
+                }
+            });
+        }
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/TablePanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/TablePanel.java
new file mode 100644
index 0000000..c1eb7f6
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/TablePanel.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib;
+
+import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator;
+
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableItem;
+
+import java.util.Arrays;
+
+/**
+ * Base class for panel containing Table that need to support copy-paste-selectAll
+ */
+public abstract class TablePanel extends ClientDisplayPanel {
+    private ITableFocusListener mGlobalListener;
+
+    /**
+     * Sets a TableFocusListener which will be notified when one of the tables
+     * gets or loses focus.
+     *
+     * @param listener
+     */
+    public void setTableFocusListener(ITableFocusListener listener) {
+        // record the global listener, to make sure table created after
+        // this call will still be setup.
+        mGlobalListener = listener;
+
+        setTableFocusListener();
+    }
+
+    /**
+     * Sets up the Table of object of the panel to work with the global listener.<br>
+     * Default implementation does nothing.
+     */
+    protected void setTableFocusListener() {
+
+    }
+
+    /**
+     * Sets up a Table object to notify the global Table Focus listener when it
+     * gets or loses the focus.
+     *
+     * @param table the Table object.
+     * @param colStart
+     * @param colEnd
+     */
+    protected final void addTableToFocusListener(final Table table,
+            final int colStart, final int colEnd) {
+        // create the activator for this table
+        final IFocusedTableActivator activator = new IFocusedTableActivator() {
+            @Override
+            public void copy(Clipboard clipboard) {
+                int[] selection = table.getSelectionIndices();
+
+                // we need to sort the items to be sure.
+                Arrays.sort(selection);
+
+                // all lines must be concatenated.
+                StringBuilder sb = new StringBuilder();
+
+                // loop on the selection and output the file.
+                for (int i : selection) {
+                    TableItem item = table.getItem(i);
+                    for (int c = colStart ; c <= colEnd ; c++) {
+                        sb.append(item.getText(c));
+                        sb.append('\t');
+                    }
+                    sb.append('\n');
+                }
+
+                // now add that to the clipboard if the string has content
+                String data = sb.toString();
+                if (data != null && data.length() > 0) {
+                    clipboard.setContents(
+                            new Object[] { data },
+                            new Transfer[] { TextTransfer.getInstance() });
+                }
+            }
+
+            @Override
+            public void selectAll() {
+                table.selectAll();
+            }
+        };
+
+        // add the focus listener on the table to notify the global listener
+        table.addFocusListener(new FocusListener() {
+            @Override
+            public void focusGained(FocusEvent e) {
+                mGlobalListener.focusGained(activator);
+            }
+
+            @Override
+            public void focusLost(FocusEvent e) {
+                mGlobalListener.focusLost(activator);
+            }
+        });
+    }
+
+    /**
+     * Sets up a Table object to notify the global Table Focus listener when it
+     * gets or loses the focus.<br>
+     * When the copy method is invoked, all columns are put in the clipboard, separated
+     * by tabs
+     *
+     * @param table the Table object.
+     */
+    protected final void addTableToFocusListener(final Table table) {
+        addTableToFocusListener(table, 0, table.getColumnCount()-1);
+    }
+
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ThreadPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ThreadPanel.java
new file mode 100644
index 0000000..81e245d
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ThreadPanel.java
@@ -0,0 +1,573 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib;
+
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ThreadInfo;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.custom.StackLayout;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Sash;
+import org.eclipse.swt.widgets.Table;
+
+import java.util.Date;
+
+/**
+ * Base class for our information panels.
+ */
+public class ThreadPanel extends TablePanel {
+
+    private final static String PREFS_THREAD_COL_ID = "threadPanel.Col0"; //$NON-NLS-1$
+    private final static String PREFS_THREAD_COL_TID = "threadPanel.Col1"; //$NON-NLS-1$
+    private final static String PREFS_THREAD_COL_STATUS = "threadPanel.Col2"; //$NON-NLS-1$
+    private final static String PREFS_THREAD_COL_UTIME = "threadPanel.Col3"; //$NON-NLS-1$
+    private final static String PREFS_THREAD_COL_STIME = "threadPanel.Col4"; //$NON-NLS-1$
+    private final static String PREFS_THREAD_COL_NAME = "threadPanel.Col5"; //$NON-NLS-1$
+
+    private final static String PREFS_THREAD_SASH = "threadPanel.sash"; //$NON-NLS-1$
+
+    private static final String PREFS_STACK_COLUMN = "threadPanel.stack.col0"; //$NON-NLS-1$
+
+    private Display mDisplay;
+    private Composite mBase;
+    private Label mNotEnabled;
+    private Label mNotSelected;
+
+    private Composite mThreadBase;
+    private Table mThreadTable;
+    private TableViewer mThreadViewer;
+
+    private Composite mStackTraceBase;
+    private Button mRefreshStackTraceButton;
+    private Label mStackTraceTimeLabel;
+    private StackTracePanel mStackTracePanel;
+    private Table mStackTraceTable;
+
+    /** Indicates if a timer-based Runnable is current requesting thread updates regularly. */
+    private boolean mMustStopRecurringThreadUpdate = false;
+    /** Flag to tell the recurring thread update to stop running */
+    private boolean mRecurringThreadUpdateRunning = false;
+
+    private Object mLock = new Object();
+
+    private static final String[] THREAD_STATUS = {
+        "Zombie", "Runnable", "TimedWait", "Monitor",
+        "Wait", "Initializing", "Starting", "Native", "VmWait",
+        "Suspended"
+    };
+
+    /**
+     * Content Provider to display the threads of a client.
+     * Expected input is a {@link Client} object.
+     */
+    private static class ThreadContentProvider implements IStructuredContentProvider {
+        @Override
+        public Object[] getElements(Object inputElement) {
+            if (inputElement instanceof Client) {
+                return ((Client)inputElement).getClientData().getThreads();
+            }
+
+            return new Object[0];
+        }
+
+        @Override
+        public void dispose() {
+            // pass
+        }
+
+        @Override
+        public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+            // pass
+        }
+    }
+
+
+    /**
+     * A Label Provider to use with {@link ThreadContentProvider}. It expects the elements to be
+     * of type {@link ThreadInfo}.
+     */
+    private static class ThreadLabelProvider implements ITableLabelProvider {
+
+        @Override
+        public Image getColumnImage(Object element, int columnIndex) {
+            return null;
+        }
+
+        @Override
+        public String getColumnText(Object element, int columnIndex) {
+            if (element instanceof ThreadInfo) {
+                ThreadInfo thread = (ThreadInfo)element;
+                switch (columnIndex) {
+                    case 0:
+                        return (thread.isDaemon() ? "*" : "") + //$NON-NLS-1$ //$NON-NLS-2$
+                            String.valueOf(thread.getThreadId());
+                    case 1:
+                        return String.valueOf(thread.getTid());
+                    case 2:
+                        if (thread.getStatus() >= 0 && thread.getStatus() < THREAD_STATUS.length)
+                            return THREAD_STATUS[thread.getStatus()];
+                        return "unknown";
+                    case 3:
+                        return String.valueOf(thread.getUtime());
+                    case 4:
+                        return String.valueOf(thread.getStime());
+                    case 5:
+                        return thread.getThreadName();
+                }
+            }
+
+            return null;
+        }
+
+        @Override
+        public void addListener(ILabelProviderListener listener) {
+            // pass
+        }
+
+        @Override
+        public void dispose() {
+            // pass
+        }
+
+        @Override
+        public boolean isLabelProperty(Object element, String property) {
+            // pass
+            return false;
+        }
+
+        @Override
+        public void removeListener(ILabelProviderListener listener) {
+            // pass
+        }
+    }
+
+    /**
+     * Create our control(s).
+     */
+    @Override
+    protected Control createControl(Composite parent) {
+        mDisplay = parent.getDisplay();
+
+        final IPreferenceStore store = DdmUiPreferences.getStore();
+
+        mBase = new Composite(parent, SWT.NONE);
+        mBase.setLayout(new StackLayout());
+
+        // UI for thread not enabled
+        mNotEnabled = new Label(mBase, SWT.CENTER | SWT.WRAP);
+        mNotEnabled.setText("Thread updates not enabled for selected client\n"
+            + "(use toolbar button to enable)");
+
+        // UI for not client selected
+        mNotSelected = new Label(mBase, SWT.CENTER | SWT.WRAP);
+        mNotSelected.setText("no client is selected");
+
+        // base composite for selected client with enabled thread update.
+        mThreadBase = new Composite(mBase, SWT.NONE);
+        mThreadBase.setLayout(new FormLayout());
+
+        // table above the sash
+        mThreadTable = new Table(mThreadBase, SWT.MULTI | SWT.FULL_SELECTION);
+        mThreadTable.setHeaderVisible(true);
+        mThreadTable.setLinesVisible(true);
+
+        TableHelper.createTableColumn(
+                mThreadTable,
+                "ID",
+                SWT.RIGHT,
+                "888", //$NON-NLS-1$
+                PREFS_THREAD_COL_ID, store);
+
+        TableHelper.createTableColumn(
+                mThreadTable,
+                "Tid",
+                SWT.RIGHT,
+                "88888", //$NON-NLS-1$
+                PREFS_THREAD_COL_TID, store);
+
+        TableHelper.createTableColumn(
+                mThreadTable,
+                "Status",
+                SWT.LEFT,
+                "timed-wait", //$NON-NLS-1$
+                PREFS_THREAD_COL_STATUS, store);
+
+        TableHelper.createTableColumn(
+                mThreadTable,
+                "utime",
+                SWT.RIGHT,
+                "utime", //$NON-NLS-1$
+                PREFS_THREAD_COL_UTIME, store);
+
+        TableHelper.createTableColumn(
+                mThreadTable,
+                "stime",
+                SWT.RIGHT,
+                "utime", //$NON-NLS-1$
+                PREFS_THREAD_COL_STIME, store);
+
+        TableHelper.createTableColumn(
+                mThreadTable,
+                "Name",
+                SWT.LEFT,
+                "android.class.ReallyLongClassName.MethodName", //$NON-NLS-1$
+                PREFS_THREAD_COL_NAME, store);
+
+        mThreadViewer = new TableViewer(mThreadTable);
+        mThreadViewer.setContentProvider(new ThreadContentProvider());
+        mThreadViewer.setLabelProvider(new ThreadLabelProvider());
+
+        mThreadViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+            @Override
+            public void selectionChanged(SelectionChangedEvent event) {
+                requestThreadStackTrace(getThreadSelection(event.getSelection()));
+            }
+        });
+        mThreadViewer.addDoubleClickListener(new IDoubleClickListener() {
+            @Override
+            public void doubleClick(DoubleClickEvent event) {
+                requestThreadStackTrace(getThreadSelection(event.getSelection()));
+            }
+        });
+
+        // the separating sash
+        final Sash sash = new Sash(mThreadBase, SWT.HORIZONTAL);
+        Color darkGray = parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY);
+        sash.setBackground(darkGray);
+
+        // the UI below the sash
+        mStackTraceBase = new Composite(mThreadBase, SWT.NONE);
+        mStackTraceBase.setLayout(new GridLayout(2, false));
+
+        mRefreshStackTraceButton = new Button(mStackTraceBase, SWT.PUSH);
+        mRefreshStackTraceButton.setText("Refresh");
+        mRefreshStackTraceButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                requestThreadStackTrace(getThreadSelection(null));
+            }
+        });
+
+        mStackTraceTimeLabel = new Label(mStackTraceBase, SWT.NONE);
+        mStackTraceTimeLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        mStackTracePanel = new StackTracePanel();
+        mStackTraceTable = mStackTracePanel.createPanel(mStackTraceBase, PREFS_STACK_COLUMN, store);
+
+        GridData gd;
+        mStackTraceTable.setLayoutData(gd = new GridData(GridData.FILL_BOTH));
+        gd.horizontalSpan = 2;
+
+        // now setup the sash.
+        // form layout data
+        FormData data = new FormData();
+        data.top = new FormAttachment(0, 0);
+        data.bottom = new FormAttachment(sash, 0);
+        data.left = new FormAttachment(0, 0);
+        data.right = new FormAttachment(100, 0);
+        mThreadTable.setLayoutData(data);
+
+        final FormData sashData = new FormData();
+        if (store != null && store.contains(PREFS_THREAD_SASH)) {
+            sashData.top = new FormAttachment(0, store.getInt(PREFS_THREAD_SASH));
+        } else {
+            sashData.top = new FormAttachment(50,0); // 50% across
+        }
+        sashData.left = new FormAttachment(0, 0);
+        sashData.right = new FormAttachment(100, 0);
+        sash.setLayoutData(sashData);
+
+        data = new FormData();
+        data.top = new FormAttachment(sash, 0);
+        data.bottom = new FormAttachment(100, 0);
+        data.left = new FormAttachment(0, 0);
+        data.right = new FormAttachment(100, 0);
+        mStackTraceBase.setLayoutData(data);
+
+        // allow resizes, but cap at minPanelWidth
+        sash.addListener(SWT.Selection, new Listener() {
+            @Override
+            public void handleEvent(Event e) {
+                Rectangle sashRect = sash.getBounds();
+                Rectangle panelRect = mThreadBase.getClientArea();
+                int bottom = panelRect.height - sashRect.height - 100;
+                e.y = Math.max(Math.min(e.y, bottom), 100);
+                if (e.y != sashRect.y) {
+                    sashData.top = new FormAttachment(0, e.y);
+                    store.setValue(PREFS_THREAD_SASH, e.y);
+                    mThreadBase.layout();
+                }
+            }
+        });
+
+        ((StackLayout)mBase.getLayout()).topControl = mNotSelected;
+
+        return mBase;
+    }
+
+    /**
+     * Sets the focus to the proper control inside the panel.
+     */
+    @Override
+    public void setFocus() {
+        mThreadTable.setFocus();
+    }
+
+    /**
+     * Sent when an existing client information changed.
+     * <p/>
+     * This is sent from a non UI thread.
+     * @param client the updated client.
+     * @param changeMask the bit mask describing the changed properties. It can contain
+     * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME}
+     * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE},
+     * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
+     * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
+     *
+     * @see IClientChangeListener#clientChanged(Client, int)
+     */
+    @Override
+    public void clientChanged(final Client client, int changeMask) {
+        if (client == getCurrentClient()) {
+            if ((changeMask & Client.CHANGE_THREAD_MODE) != 0 ||
+                    (changeMask & Client.CHANGE_THREAD_DATA) != 0) {
+                try {
+                    mThreadTable.getDisplay().asyncExec(new Runnable() {
+                        @Override
+                        public void run() {
+                            clientSelected();
+                        }
+                    });
+                } catch (SWTException e) {
+                    // widget is disposed, we do nothing
+                }
+            } else if ((changeMask & Client.CHANGE_THREAD_STACKTRACE) != 0) {
+                try {
+                    mThreadTable.getDisplay().asyncExec(new Runnable() {
+                        @Override
+                        public void run() {
+                            updateThreadStackCall();
+                        }
+                    });
+                } catch (SWTException e) {
+                    // widget is disposed, we do nothing
+                }
+            }
+        }
+    }
+
+    /**
+     * Sent when a new device is selected. The new device can be accessed
+     * with {@link #getCurrentDevice()}.
+     */
+    @Override
+    public void deviceSelected() {
+        // pass
+    }
+
+    /**
+     * Sent when a new client is selected. The new client can be accessed
+     * with {@link #getCurrentClient()}.
+     */
+    @Override
+    public void clientSelected() {
+        if (mThreadTable.isDisposed()) {
+            return;
+        }
+
+        Client client = getCurrentClient();
+
+        mStackTracePanel.setCurrentClient(client);
+
+        if (client != null) {
+            if (!client.isThreadUpdateEnabled()) {
+                ((StackLayout)mBase.getLayout()).topControl = mNotEnabled;
+                mThreadViewer.setInput(null);
+
+                // if we are currently updating the thread, stop doing it.
+                mMustStopRecurringThreadUpdate = true;
+            } else {
+                ((StackLayout)mBase.getLayout()).topControl = mThreadBase;
+                mThreadViewer.setInput(client);
+
+                synchronized (mLock) {
+                    // if we're not updating we start the process
+                    if (mRecurringThreadUpdateRunning == false) {
+                        startRecurringThreadUpdate();
+                    } else if (mMustStopRecurringThreadUpdate) {
+                        // else if there's a runnable that's still going to get called, lets
+                        // simply cancel the stop, and keep going
+                        mMustStopRecurringThreadUpdate = false;
+                    }
+                }
+            }
+        } else {
+            ((StackLayout)mBase.getLayout()).topControl = mNotSelected;
+            mThreadViewer.setInput(null);
+        }
+
+        mBase.layout();
+    }
+
+    private void requestThreadStackTrace(ThreadInfo selectedThread) {
+        if (selectedThread != null) {
+            Client client = (Client) mThreadViewer.getInput();
+            if (client != null) {
+                client.requestThreadStackTrace(selectedThread.getThreadId());
+            }
+        }
+    }
+
+    /**
+     * Updates the stack call of the currently selected thread.
+     * <p/>
+     * This <b>must</b> be called from the UI thread.
+     */
+    private void updateThreadStackCall() {
+        Client client = getCurrentClient();
+        if (client != null) {
+            // get the current selection in the ThreadTable
+            ThreadInfo selectedThread = getThreadSelection(null);
+
+            if (selectedThread != null) {
+                updateThreadStackTrace(selectedThread);
+            } else {
+                updateThreadStackTrace(null);
+            }
+        }
+    }
+
+    /**
+     * updates the stackcall of the specified thread. If <code>null</code> the UI is emptied
+     * of current data.
+     * @param thread
+     */
+    private void updateThreadStackTrace(ThreadInfo thread) {
+        mStackTracePanel.setViewerInput(thread);
+
+        if (thread != null) {
+            mRefreshStackTraceButton.setEnabled(true);
+            long stackcallTime = thread.getStackCallTime();
+            if (stackcallTime != 0) {
+                String label = new Date(stackcallTime).toString();
+                mStackTraceTimeLabel.setText(label);
+            } else {
+                mStackTraceTimeLabel.setText(""); //$NON-NLS-1$
+            }
+        } else {
+            mRefreshStackTraceButton.setEnabled(true);
+            mStackTraceTimeLabel.setText(""); //$NON-NLS-1$
+        }
+    }
+
+    @Override
+    protected void setTableFocusListener() {
+        addTableToFocusListener(mThreadTable);
+        addTableToFocusListener(mStackTraceTable);
+    }
+
+    /**
+     * Initiate recurring events. We use a shorter "initialWait" so we do the
+     * first execution sooner. We don't do it immediately because we want to
+     * give the clients a chance to get set up.
+     */
+    private void startRecurringThreadUpdate() {
+        mRecurringThreadUpdateRunning = true;
+        int initialWait = 1000;
+
+        mDisplay.timerExec(initialWait, new Runnable() {
+            @Override
+            public void run() {
+                synchronized (mLock) {
+                    // lets check we still want updates.
+                    if (mMustStopRecurringThreadUpdate == false) {
+                        Client client = getCurrentClient();
+                        if (client != null) {
+                            client.requestThreadUpdate();
+
+                            mDisplay.timerExec(
+                                    DdmUiPreferences.getThreadRefreshInterval() * 1000, this);
+                        } else {
+                            // we don't have a Client, which means the runnable is not
+                            // going to be called through the timer. We reset the running flag.
+                            mRecurringThreadUpdateRunning = false;
+                        }
+                    } else {
+                        // else actually stops (don't call the timerExec) and reset the flags.
+                        mRecurringThreadUpdateRunning = false;
+                        mMustStopRecurringThreadUpdate = false;
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * Returns the current thread selection or <code>null</code> if none is found.
+     * If a {@link ISelection} object is specified, the first {@link ThreadInfo} from this selection
+     * is returned, otherwise, the <code>ISelection</code> returned by
+     * {@link TableViewer#getSelection()} is used.
+     * @param selection the {@link ISelection} to use, or <code>null</code>
+     */
+    private ThreadInfo getThreadSelection(ISelection selection) {
+        if (selection == null) {
+            selection = mThreadViewer.getSelection();
+        }
+
+        if (selection instanceof IStructuredSelection) {
+            IStructuredSelection structuredSelection = (IStructuredSelection)selection;
+            Object object = structuredSelection.getFirstElement();
+            if (object instanceof ThreadInfo) {
+                return (ThreadInfo)object;
+            }
+        }
+
+        return null;
+    }
+
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/actions/ICommonAction.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/actions/ICommonAction.java
new file mode 100644
index 0000000..856b874
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/actions/ICommonAction.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib.actions;
+
+/**
+ * Common interface for basic action handling. This allows the common ui
+ * components to access ToolItem or Action the same way.
+ */
+public interface ICommonAction {
+    /**
+     * Sets the enabled state of this action.
+     * @param enabled <code>true</code> to enable, and
+     *   <code>false</code> to disable
+     */
+    public void setEnabled(boolean enabled);
+
+    /**
+     * Sets the checked status of this action.
+     * @param checked the new checked status
+     */
+    public void setChecked(boolean checked);
+    
+    /**
+     * Sets the {@link Runnable} that will be executed when the action is triggered.
+     */
+    public void setRunnable(Runnable runnable);
+}
+
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/actions/ToolItemAction.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/actions/ToolItemAction.java
new file mode 100644
index 0000000..c7fef32
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/actions/ToolItemAction.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib.actions;
+
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.swt.widgets.ToolItem;
+
+/**
+ * Wrapper around {@link ToolItem} to implement {@link ICommonAction}
+ */
+public class ToolItemAction implements ICommonAction {
+    public ToolItem item;
+
+    public ToolItemAction(ToolBar parent, int style) {
+        item = new ToolItem(parent, style);
+    }
+
+    /**
+     * Sets the enabled state of this action.
+     * @param enabled <code>true</code> to enable, and
+     *   <code>false</code> to disable
+     * @see ICommonAction#setChecked(boolean)
+     */
+    @Override
+    public void setChecked(boolean checked) {
+        item.setSelection(checked);
+    }
+
+    /**
+     * Sets the enabled state of this action.
+     * @param enabled <code>true</code> to enable, and
+     *   <code>false</code> to disable
+     * @see ICommonAction#setEnabled(boolean)
+     */
+    @Override
+    public void setEnabled(boolean enabled) {
+        item.setEnabled(enabled);
+    }
+
+    /**
+     * Sets the {@link Runnable} that will be executed when the action is triggered (through
+     * {@link SelectionListener#widgetSelected(SelectionEvent)} on the wrapped {@link ToolItem}).
+     * @see ICommonAction#setRunnable(Runnable)
+     */
+    @Override
+    public void setRunnable(final Runnable runnable) {
+        item.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                runnable.run();
+            }
+        });
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/annotation/UiThread.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/annotation/UiThread.java
new file mode 100644
index 0000000..8e9e11b
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/annotation/UiThread.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2008 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.ddmuilib.annotation;
+
+import java.lang.annotation.Target;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Simple utility annotation used only to mark methods that are executed on the UI thread.
+ * This annotation's sole purpose is to help reading the source code. It has no additional effect.
+ */
+ at Target({ ElementType.METHOD })
+ at Retention(RetentionPolicy.SOURCE)
+public @interface UiThread {
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/annotation/WorkerThread.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/annotation/WorkerThread.java
new file mode 100644
index 0000000..e767eda
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/annotation/WorkerThread.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2008 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.ddmuilib.annotation;
+
+import java.lang.annotation.Target;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Simple utility annotation used only to mark methods that are not executed on the UI thread.
+ * This annotation's sole purpose is to help reading the source code. It has no additional effect.
+ */
+ at Target({ ElementType.METHOD })
+ at Retention(RetentionPolicy.SOURCE)
+public @interface WorkerThread {
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/console/DdmConsole.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/console/DdmConsole.java
new file mode 100644
index 0000000..4df4376
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/console/DdmConsole.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib.console;
+
+
+/**
+ * Static Console used to ouput messages. By default outputs the message to System.out and
+ * System.err, but can receive a IDdmConsole object which will actually do something.
+ */
+public class DdmConsole {
+
+    private static IDdmConsole mConsole;
+
+    /**
+     * Prints a message to the android console.
+     * @param message the message to print
+     * @param forceDisplay if true, this force the console to be displayed.
+     */
+    public static void printErrorToConsole(String message) {
+        if (mConsole != null) {
+            mConsole.printErrorToConsole(message);
+        } else {
+            System.err.println(message);
+        }
+    }
+
+    /**
+     * Prints several messages to the android console.
+     * @param messages the messages to print
+     * @param forceDisplay if true, this force the console to be displayed.
+     */
+    public static void printErrorToConsole(String[] messages) {
+        if (mConsole != null) {
+            mConsole.printErrorToConsole(messages);
+        } else {
+            for (String message : messages) {
+                System.err.println(message);
+            }
+        }
+    }
+
+    /**
+     * Prints a message to the android console.
+     * @param message the message to print
+     * @param forceDisplay if true, this force the console to be displayed.
+     */
+    public static void printToConsole(String message) {
+        if (mConsole != null) {
+            mConsole.printToConsole(message);
+        } else {
+            System.out.println(message);
+        }
+    }
+
+    /**
+     * Prints several messages to the android console.
+     * @param messages the messages to print
+     * @param forceDisplay if true, this force the console to be displayed.
+     */
+    public static void printToConsole(String[] messages) {
+        if (mConsole != null) {
+            mConsole.printToConsole(messages);
+        } else {
+            for (String message : messages) {
+                System.out.println(message);
+            }
+        }
+    }
+
+    /**
+     * Sets a IDdmConsole to override the default behavior of the console
+     * @param console The new IDdmConsole
+     * **/
+    public static void setConsole(IDdmConsole console) {
+        mConsole = console;
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/console/IDdmConsole.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/console/IDdmConsole.java
new file mode 100644
index 0000000..3679d41
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/console/IDdmConsole.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib.console;
+
+
+/**
+ * DDMS console interface.
+ */
+public interface IDdmConsole {
+    /**
+     * Prints a message to the android console.
+     * @param message the message to print
+     */
+    public void printErrorToConsole(String message);
+
+    /**
+     * Prints several messages to the android console.
+     * @param messages the messages to print
+     */
+    public void printErrorToConsole(String[] messages);
+
+    /**
+     * Prints a message to the android console.
+     * @param message the message to print
+     */
+    public void printToConsole(String message);
+
+    /**
+     * Prints several messages to the android console.
+     * @param messages the messages to print
+     */
+    public void printToConsole(String[] messages);
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceContentProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceContentProvider.java
new file mode 100644
index 0000000..062d4f0
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceContentProvider.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib.explorer;
+
+import com.android.ddmlib.FileListingService;
+import com.android.ddmlib.FileListingService.FileEntry;
+import com.android.ddmlib.FileListingService.IListingReceiver;
+
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Tree;
+
+/**
+ * Content provider class for device Explorer.
+ */
+class DeviceContentProvider implements ITreeContentProvider {
+
+    private TreeViewer mViewer;
+    private FileListingService mFileListingService;
+    private FileEntry mRootEntry;
+
+    private IListingReceiver sListingReceiver = new IListingReceiver() {
+        @Override
+        public void setChildren(final FileEntry entry, FileEntry[] children) {
+            final Tree t = mViewer.getTree();
+            if (t != null && t.isDisposed() == false) {
+                Display display = t.getDisplay();
+                if (display.isDisposed() == false) {
+                    display.asyncExec(new Runnable() {
+                        @Override
+                        public void run() {
+                            if (t.isDisposed() == false) {
+                                // refresh the entry.
+                                mViewer.refresh(entry);
+
+                                // force it open, since on linux and windows
+                                // when getChildren() returns null, the node is
+                                // not considered expanded.
+                                mViewer.setExpandedState(entry, true);
+                            }
+                        }
+                    });
+                }
+            }
+        }
+
+        @Override
+        public void refreshEntry(final FileEntry entry) {
+            final Tree t = mViewer.getTree();
+            if (t != null && t.isDisposed() == false) {
+                Display display = t.getDisplay();
+                if (display.isDisposed() == false) {
+                    display.asyncExec(new Runnable() {
+                        @Override
+                        public void run() {
+                            if (t.isDisposed() == false) {
+                                // refresh the entry.
+                                mViewer.refresh(entry);
+                            }
+                        }
+                    });
+                }
+            }
+        }
+    };
+
+    /**
+     *
+     */
+    public DeviceContentProvider() {
+    }
+
+    public void setListingService(FileListingService fls) {
+        mFileListingService = fls;
+    }
+
+    /* (non-Javadoc)
+     * @see org.eclipse.jface.viewers.ITreeContentProvider#getChildren(java.lang.Object)
+     */
+    @Override
+    public Object[] getChildren(Object parentElement) {
+        if (parentElement instanceof FileEntry) {
+            FileEntry parentEntry = (FileEntry)parentElement;
+
+            Object[] oldEntries = parentEntry.getCachedChildren();
+            Object[] newEntries = mFileListingService.getChildren(parentEntry,
+                    true, sListingReceiver);
+
+            if (newEntries != null) {
+                return newEntries;
+            } else {
+                // if null was returned, this means the cache was not valid,
+                // and a thread was launched for ls. sListingReceiver will be
+                // notified with the new entries.
+                return oldEntries;
+            }
+        }
+        return new Object[0];
+    }
+
+    /* (non-Javadoc)
+     * @see org.eclipse.jface.viewers.ITreeContentProvider#getParent(java.lang.Object)
+     */
+    @Override
+    public Object getParent(Object element) {
+        if (element instanceof FileEntry) {
+            FileEntry entry = (FileEntry)element;
+
+            return entry.getParent();
+        }
+        return null;
+    }
+
+    /* (non-Javadoc)
+     * @see org.eclipse.jface.viewers.ITreeContentProvider#hasChildren(java.lang.Object)
+     */
+    @Override
+    public boolean hasChildren(Object element) {
+        if (element instanceof FileEntry) {
+            FileEntry entry = (FileEntry)element;
+
+            return entry.getType() == FileListingService.TYPE_DIRECTORY;
+        }
+        return false;
+    }
+
+    /* (non-Javadoc)
+     * @see org.eclipse.jface.viewers.IStructuredContentProvider#getElements(java.lang.Object)
+     */
+    @Override
+    public Object[] getElements(Object inputElement) {
+        if (inputElement instanceof FileEntry) {
+            FileEntry entry = (FileEntry)inputElement;
+            if (entry.isRoot()) {
+                return getChildren(mRootEntry);
+            }
+        }
+
+        return null;
+    }
+
+    /* (non-Javadoc)
+     * @see org.eclipse.jface.viewers.IContentProvider#dispose()
+     */
+    @Override
+    public void dispose() {
+    }
+
+    /* (non-Javadoc)
+     * @see org.eclipse.jface.viewers.IContentProvider#inputChanged(org.eclipse.jface.viewers.Viewer, java.lang.Object, java.lang.Object)
+     */
+    @Override
+    public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+        if (viewer instanceof TreeViewer) {
+            mViewer = (TreeViewer)viewer;
+        }
+        if (newInput instanceof FileEntry) {
+            mRootEntry = (FileEntry)newInput;
+        }
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceExplorer.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceExplorer.java
new file mode 100644
index 0000000..b69d3b5
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceExplorer.java
@@ -0,0 +1,922 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib.explorer;
+
+import com.android.ddmlib.AdbCommandRejectedException;
+import com.android.ddmlib.DdmConstants;
+import com.android.ddmlib.FileListingService;
+import com.android.ddmlib.FileListingService.FileEntry;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.IShellOutputReceiver;
+import com.android.ddmlib.ShellCommandUnresponsiveException;
+import com.android.ddmlib.SyncException;
+import com.android.ddmlib.SyncService;
+import com.android.ddmlib.SyncService.ISyncProgressMonitor;
+import com.android.ddmlib.TimeoutException;
+import com.android.ddmuilib.DdmUiPreferences;
+import com.android.ddmuilib.ImageLoader;
+import com.android.ddmuilib.Panel;
+import com.android.ddmuilib.SyncProgressHelper;
+import com.android.ddmuilib.SyncProgressHelper.SyncRunnable;
+import com.android.ddmuilib.TableHelper;
+import com.android.ddmuilib.actions.ICommonAction;
+import com.android.ddmuilib.console.DdmConsole;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.dialogs.ErrorDialog;
+import org.eclipse.jface.dialogs.IInputValidator;
+import org.eclipse.jface.dialogs.InputDialog;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.ViewerDropAdapter;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.FileTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.dnd.TransferData;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.DirectoryDialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeItem;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Device filesystem explorer class.
+ */
+public class DeviceExplorer extends Panel {
+
+    private final static String TRACE_KEY_EXT = ".key"; // $NON-NLS-1S
+    private final static String TRACE_DATA_EXT = ".data"; // $NON-NLS-1S
+
+    private static Pattern mKeyFilePattern = Pattern.compile(
+            "(.+)\\" + TRACE_KEY_EXT); // $NON-NLS-1S
+    private static Pattern mDataFilePattern = Pattern.compile(
+            "(.+)\\" + TRACE_DATA_EXT); // $NON-NLS-1S
+
+    public static String COLUMN_NAME = "android.explorer.name"; //$NON-NLS-1S
+    public static String COLUMN_SIZE = "android.explorer.size"; //$NON-NLS-1S
+    public static String COLUMN_DATE = "android.explorer.data"; //$NON-NLS-1S
+    public static String COLUMN_TIME = "android.explorer.time"; //$NON-NLS-1S
+    public static String COLUMN_PERMISSIONS = "android.explorer.permissions"; // $NON-NLS-1S
+    public static String COLUMN_INFO = "android.explorer.info"; // $NON-NLS-1S
+
+    private Composite mParent;
+    private TreeViewer mTreeViewer;
+    private Tree mTree;
+    private DeviceContentProvider mContentProvider;
+
+    private ICommonAction mPushAction;
+    private ICommonAction mPullAction;
+    private ICommonAction mDeleteAction;
+    private ICommonAction mCreateNewFolderAction;
+
+    private Image mFileImage;
+    private Image mFolderImage;
+    private Image mPackageImage;
+    private Image mOtherImage;
+
+    private IDevice mCurrentDevice;
+
+    private String mDefaultSave;
+
+    public DeviceExplorer() {
+    }
+
+    /**
+     * Sets custom images for the device explorer. If none are set then defaults are used.
+     * This can be useful to set platform-specific explorer icons.
+     *
+     * This should be called before {@link #createControl(Composite)}.
+     *
+     * @param fileImage the icon to represent a file.
+     * @param folderImage the icon to represent a folder.
+     * @param packageImage the icon to represent an apk.
+     * @param otherImage the icon to represent other types of files.
+     */
+    public void setCustomImages(Image fileImage, Image folderImage, Image packageImage,
+            Image otherImage) {
+        mFileImage = fileImage;
+        mFolderImage = folderImage;
+        mPackageImage = packageImage;
+        mOtherImage = otherImage;
+    }
+
+    /**
+     * Sets the actions so that the device explorer can enable/disable them based on the current
+     * selection
+     * @param pushAction
+     * @param pullAction
+     * @param deleteAction
+     * @param createNewFolderAction
+     */
+    public void setActions(ICommonAction pushAction, ICommonAction pullAction,
+            ICommonAction deleteAction, ICommonAction createNewFolderAction) {
+        mPushAction = pushAction;
+        mPullAction = pullAction;
+        mDeleteAction = deleteAction;
+        mCreateNewFolderAction = createNewFolderAction;
+    }
+
+    /**
+     * Creates a control capable of displaying some information.  This is
+     * called once, when the application is initializing, from the UI thread.
+     */
+    @Override
+    protected Control createControl(Composite parent) {
+        mParent = parent;
+        parent.setLayout(new FillLayout());
+
+        ImageLoader loader = ImageLoader.getDdmUiLibLoader();
+        if (mFileImage == null) {
+            mFileImage = loader.loadImage("file.png", mParent.getDisplay());
+        }
+        if (mFolderImage == null) {
+            mFolderImage = loader.loadImage("folder.png", mParent.getDisplay());
+        }
+        if (mPackageImage == null) {
+            mPackageImage = loader.loadImage("android.png", mParent.getDisplay());
+        }
+        if (mOtherImage == null) {
+            // TODO: find a default image for other.
+        }
+
+        mTree = new Tree(parent, SWT.MULTI | SWT.FULL_SELECTION | SWT.VIRTUAL);
+        mTree.setHeaderVisible(true);
+
+        IPreferenceStore store = DdmUiPreferences.getStore();
+
+        // create columns
+        TableHelper.createTreeColumn(mTree, "Name", SWT.LEFT,
+                "0000drwxrwxrwx", COLUMN_NAME, store); //$NON-NLS-1$
+        TableHelper.createTreeColumn(mTree, "Size", SWT.RIGHT,
+                "000000", COLUMN_SIZE, store); //$NON-NLS-1$
+        TableHelper.createTreeColumn(mTree, "Date", SWT.LEFT,
+                "2007-08-14", COLUMN_DATE, store); //$NON-NLS-1$
+        TableHelper.createTreeColumn(mTree, "Time", SWT.LEFT,
+                "20:54", COLUMN_TIME, store); //$NON-NLS-1$
+        TableHelper.createTreeColumn(mTree, "Permissions", SWT.LEFT,
+                "drwxrwxrwx", COLUMN_PERMISSIONS, store); //$NON-NLS-1$
+        TableHelper.createTreeColumn(mTree, "Info", SWT.LEFT,
+                "drwxrwxrwx", COLUMN_INFO, store); //$NON-NLS-1$
+
+        // create the jface wrapper
+        mTreeViewer = new TreeViewer(mTree);
+
+        // setup data provider
+        mContentProvider = new DeviceContentProvider();
+        mTreeViewer.setContentProvider(mContentProvider);
+        mTreeViewer.setLabelProvider(new FileLabelProvider(mFileImage,
+                mFolderImage, mPackageImage, mOtherImage));
+
+        // setup a listener for selection
+        mTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+            @Override
+            public void selectionChanged(SelectionChangedEvent event) {
+                ISelection sel = event.getSelection();
+                if (sel.isEmpty()) {
+                    mPullAction.setEnabled(false);
+                    mPushAction.setEnabled(false);
+                    mDeleteAction.setEnabled(false);
+                    mCreateNewFolderAction.setEnabled(false);
+                    return;
+                }
+                if (sel instanceof IStructuredSelection) {
+                    IStructuredSelection selection = (IStructuredSelection) sel;
+                    Object element = selection.getFirstElement();
+                    if (element == null)
+                        return;
+                    if (element instanceof FileEntry) {
+                        mPullAction.setEnabled(true);
+                        mPushAction.setEnabled(selection.size() == 1);
+                        if (selection.size() == 1) {
+                            FileEntry entry = (FileEntry) element;
+                            setDeleteEnabledState(entry);
+                            mCreateNewFolderAction.setEnabled(entry.isDirectory());
+                        } else {
+                            mDeleteAction.setEnabled(false);
+                        }
+                    }
+                }
+            }
+        });
+
+        // add support for double click
+        mTreeViewer.addDoubleClickListener(new IDoubleClickListener() {
+            @Override
+            public void doubleClick(DoubleClickEvent event) {
+                ISelection sel = event.getSelection();
+
+                if (sel instanceof IStructuredSelection) {
+                    IStructuredSelection selection = (IStructuredSelection) sel;
+
+                    if (selection.size() == 1) {
+                        FileEntry entry = (FileEntry)selection.getFirstElement();
+                        String name = entry.getName();
+
+                        FileEntry parentEntry = entry.getParent();
+
+                        // can't really do anything with no parent
+                        if (parentEntry == null) {
+                            return;
+                        }
+
+                        // check this is a file like we want.
+                        Matcher m = mKeyFilePattern.matcher(name);
+                        if (m.matches()) {
+                            // get the name w/o the extension
+                            String baseName = m.group(1);
+
+                            // add the data extension
+                            String dataName = baseName + TRACE_DATA_EXT;
+
+                            FileEntry dataEntry = parentEntry.findChild(dataName);
+
+                            handleTraceDoubleClick(baseName, entry, dataEntry);
+
+                        } else {
+                            m = mDataFilePattern.matcher(name);
+                            if (m.matches()) {
+                                // get the name w/o the extension
+                                String baseName = m.group(1);
+
+                                // add the key extension
+                                String keyName = baseName + TRACE_KEY_EXT;
+
+                                FileEntry keyEntry = parentEntry.findChild(keyName);
+
+                                handleTraceDoubleClick(baseName, keyEntry, entry);
+                            }
+                        }
+                    }
+                }
+            }
+        });
+
+        // setup drop listener
+        mTreeViewer.addDropSupport(DND.DROP_COPY | DND.DROP_MOVE,
+                new Transfer[] { FileTransfer.getInstance() },
+                new ViewerDropAdapter(mTreeViewer) {
+            @Override
+            public boolean performDrop(Object data) {
+                // get the item on which we dropped the item(s)
+                FileEntry target = (FileEntry)getCurrentTarget();
+
+                // in case we drop at the same level as root
+                if (target == null) {
+                    return false;
+                }
+
+                // if the target is not a directory, we get the parent directory
+                if (target.isDirectory() == false) {
+                    target = target.getParent();
+                }
+
+                if (target == null) {
+                    return false;
+                }
+
+                // get the list of files to drop
+                String[] files = (String[])data;
+
+                // do the drop
+                pushFiles(files, target);
+
+                // we need to finish with a refresh
+                refresh(target);
+
+                return true;
+            }
+
+            @Override
+            public boolean validateDrop(Object target, int operation, TransferData transferType) {
+                if (target == null) {
+                    return false;
+                }
+
+                // convert to the real item
+                FileEntry targetEntry = (FileEntry)target;
+
+                // if the target is not a directory, we get the parent directory
+                if (targetEntry.isDirectory() == false) {
+                    target = targetEntry.getParent();
+                }
+
+                if (target == null) {
+                    return false;
+                }
+
+                return true;
+            }
+        });
+
+        // create and start the refresh thread
+        new Thread("Device Ls refresher") {
+            @Override
+            public void run() {
+                while (true) {
+                    try {
+                        sleep(FileListingService.REFRESH_RATE);
+                    } catch (InterruptedException e) {
+                        return;
+                    }
+
+                    if (mTree != null && mTree.isDisposed() == false) {
+                        Display display = mTree.getDisplay();
+                        if (display.isDisposed() == false) {
+                            display.asyncExec(new Runnable() {
+                                @Override
+                                public void run() {
+                                    if (mTree.isDisposed() == false) {
+                                        mTreeViewer.refresh(true);
+                                    }
+                                }
+                            });
+                        } else {
+                            return;
+                        }
+                    } else {
+                        return;
+                    }
+                }
+
+            }
+        }.start();
+
+        return mTree;
+    }
+
+    @Override
+    protected void postCreation() {
+
+    }
+
+    /**
+     * Sets the focus to the proper control inside the panel.
+     */
+    @Override
+    public void setFocus() {
+        mTree.setFocus();
+    }
+
+    /**
+     * Processes a double click on a trace file
+     * @param baseName the base name of the 2 files.
+     * @param keyEntry The FileEntry for the .key file.
+     * @param dataEntry The FileEntry for the .data file.
+     */
+    private void handleTraceDoubleClick(String baseName, FileEntry keyEntry,
+            FileEntry dataEntry) {
+        // first we need to download the files.
+        File keyFile;
+        File dataFile;
+        String path;
+        try {
+            // create a temp file for keyFile
+            File f = File.createTempFile(baseName, DdmConstants.DOT_TRACE);
+            f.delete();
+            f.mkdir();
+
+            path = f.getAbsolutePath();
+
+            keyFile = new File(path + File.separator + keyEntry.getName());
+            dataFile = new File(path + File.separator + dataEntry.getName());
+        } catch (IOException e) {
+            return;
+        }
+
+        // download the files
+        try {
+            SyncService sync = mCurrentDevice.getSyncService();
+            if (sync != null) {
+                ISyncProgressMonitor monitor = SyncService.getNullProgressMonitor();
+                sync.pullFile(keyEntry, keyFile.getAbsolutePath(), monitor);
+                sync.pullFile(dataEntry, dataFile.getAbsolutePath(), monitor);
+
+                // now that we have the file, we need to launch traceview
+                String[] command = new String[2];
+                command[0] = DdmUiPreferences.getTraceview();
+                command[1] = path + File.separator + baseName;
+
+                try {
+                    final Process p = Runtime.getRuntime().exec(command);
+
+                    // create a thread for the output
+                    new Thread("Traceview output") {
+                        @Override
+                        public void run() {
+                            // create a buffer to read the stderr output
+                            InputStreamReader is = new InputStreamReader(p.getErrorStream());
+                            BufferedReader resultReader = new BufferedReader(is);
+
+                            // read the lines as they come. if null is returned, it's
+                            // because the process finished
+                            try {
+                                while (true) {
+                                    String line = resultReader.readLine();
+                                    if (line != null) {
+                                        DdmConsole.printErrorToConsole("Traceview: " + line);
+                                    } else {
+                                        break;
+                                    }
+                                }
+                                // get the return code from the process
+                                p.waitFor();
+                            } catch (IOException e) {
+                            } catch (InterruptedException e) {
+
+                            }
+                        }
+                    }.start();
+
+                } catch (IOException e) {
+                }
+            }
+        } catch (IOException e) {
+            DdmConsole.printErrorToConsole(String.format(
+                    "Failed to pull %1$s: %2$s", keyEntry.getName(), e.getMessage()));
+            return;
+        } catch (SyncException e) {
+            if (e.wasCanceled() == false) {
+                DdmConsole.printErrorToConsole(String.format(
+                        "Failed to pull %1$s: %2$s", keyEntry.getName(), e.getMessage()));
+                return;
+            }
+        } catch (TimeoutException e) {
+            DdmConsole.printErrorToConsole(String.format(
+                    "Failed to pull %1$s: timeout", keyEntry.getName()));
+        } catch (AdbCommandRejectedException e) {
+            DdmConsole.printErrorToConsole(String.format(
+                    "Failed to pull %1$s: %2$s", keyEntry.getName(), e.getMessage()));
+        }
+    }
+
+    /**
+     * Pull the current selection on the local drive. This method displays
+     * a dialog box to let the user select where to store the file(s) and
+     * folder(s).
+     */
+    public void pullSelection() {
+        // get the selection
+        TreeItem[] items = mTree.getSelection();
+
+        // name of the single file pull, or null if we're pulling a directory
+        // or more than one object.
+        String filePullName = null;
+        FileEntry singleEntry = null;
+
+        // are we pulling a single file?
+        if (items.length == 1) {
+            singleEntry = (FileEntry)items[0].getData();
+            if (singleEntry.getType() == FileListingService.TYPE_FILE) {
+                filePullName = singleEntry.getName();
+            }
+        }
+
+        // where do we save by default?
+        String defaultPath = mDefaultSave;
+        if (defaultPath == null) {
+            defaultPath = System.getProperty("user.home"); //$NON-NLS-1$
+        }
+
+        if (filePullName != null) {
+            FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.SAVE);
+
+            fileDialog.setText("Get Device File");
+            fileDialog.setFileName(filePullName);
+            fileDialog.setFilterPath(defaultPath);
+
+            String fileName = fileDialog.open();
+            if (fileName != null) {
+                mDefaultSave = fileDialog.getFilterPath();
+
+                pullFile(singleEntry, fileName);
+            }
+        } else {
+            DirectoryDialog directoryDialog = new DirectoryDialog(mParent.getShell(), SWT.SAVE);
+
+            directoryDialog.setText("Get Device Files/Folders");
+            directoryDialog.setFilterPath(defaultPath);
+
+            String directoryName = directoryDialog.open();
+            if (directoryName != null) {
+                pullSelection(items, directoryName);
+            }
+        }
+    }
+
+    /**
+     * Push new file(s) and folder(s) into the current selection. Current
+     * selection must be single item. If the current selection is not a
+     * directory, the parent directory is used.
+     * This method displays a dialog to let the user choose file to push to
+     * the device.
+     */
+    public void pushIntoSelection() {
+        // get the name of the object we're going to pull
+        TreeItem[] items = mTree.getSelection();
+
+        if (items.length == 0) {
+            return;
+        }
+
+        FileDialog dlg = new FileDialog(mParent.getShell(), SWT.OPEN);
+        String fileName;
+
+        dlg.setText("Put File on Device");
+
+        // There should be only one.
+        FileEntry entry = (FileEntry)items[0].getData();
+        dlg.setFileName(entry.getName());
+
+        String defaultPath = mDefaultSave;
+        if (defaultPath == null) {
+            defaultPath = System.getProperty("user.home"); //$NON-NLS-1$
+        }
+        dlg.setFilterPath(defaultPath);
+
+        fileName = dlg.open();
+        if (fileName != null) {
+            mDefaultSave = dlg.getFilterPath();
+
+            // we need to figure out the remote path based on the current selection type.
+            String remotePath;
+            FileEntry toRefresh = entry;
+            if (entry.isDirectory()) {
+                remotePath = entry.getFullPath();
+            } else {
+                toRefresh = entry.getParent();
+                remotePath = toRefresh.getFullPath();
+            }
+
+            pushFile(fileName, remotePath);
+            mTreeViewer.refresh(toRefresh);
+        }
+    }
+
+    public void deleteSelection() {
+        // get the name of the object we're going to pull
+        TreeItem[] items = mTree.getSelection();
+
+        if (items.length != 1) {
+            return;
+        }
+
+        FileEntry entry = (FileEntry)items[0].getData();
+        final FileEntry parentEntry = entry.getParent();
+
+        // create the delete command
+        String command = "rm " + entry.getFullEscapedPath(); //$NON-NLS-1$
+
+        try {
+            mCurrentDevice.executeShellCommand(command, new IShellOutputReceiver() {
+                @Override
+                public void addOutput(byte[] data, int offset, int length) {
+                    // pass
+                    // TODO get output to display errors if any.
+                }
+
+                @Override
+                public void flush() {
+                    mTreeViewer.refresh(parentEntry);
+                }
+
+                @Override
+                public boolean isCancelled() {
+                    return false;
+                }
+            });
+        } catch (IOException e) {
+            // adb failed somehow, we do nothing. We should be displaying the error from the output
+            // of the shell command.
+        } catch (TimeoutException e) {
+            // adb failed somehow, we do nothing. We should be displaying the error from the output
+            // of the shell command.
+        } catch (AdbCommandRejectedException e) {
+            // adb failed somehow, we do nothing. We should be displaying the error from the output
+            // of the shell command.
+        } catch (ShellCommandUnresponsiveException e) {
+            // adb failed somehow, we do nothing. We should be displaying the error from the output
+            // of the shell command.
+        }
+
+    }
+
+    public void createNewFolderInSelection() {
+        TreeItem[] items = mTree.getSelection();
+
+        if (items.length != 1) {
+            return;
+        }
+
+        final FileEntry entry = (FileEntry) items[0].getData();
+
+        if (entry.isDirectory()) {
+            InputDialog inputDialog = new InputDialog(mTree.getShell(), "New Folder",
+                    "Please enter the new folder name", "New Folder", new IInputValidator() {
+                        @Override
+                        public String isValid(String newText) {
+                            if ((newText != null) && (newText.length() > 0)
+                                    && (newText.trim().length() > 0)
+                                    && (newText.indexOf('/') == -1)
+                                    && (newText.indexOf('\\') == -1)) {
+                                return null;
+                            } else {
+                                return "Invalid name";
+                            }
+                        }
+                    });
+            inputDialog.open();
+            String value = inputDialog.getValue();
+
+            if (value != null) {
+                // create the mkdir command
+                String command = "mkdir " + entry.getFullEscapedPath() //$NON-NLS-1$
+                        + FileListingService.FILE_SEPARATOR + FileEntry.escape(value);
+
+                try {
+                    mCurrentDevice.executeShellCommand(command, new IShellOutputReceiver() {
+
+                        @Override
+                        public boolean isCancelled() {
+                            return false;
+                        }
+
+                        @Override
+                        public void flush() {
+                            mTreeViewer.refresh(entry);
+                        }
+
+                        @Override
+                        public void addOutput(byte[] data, int offset, int length) {
+                            String errorMessage;
+                            if (data != null) {
+                                errorMessage = new String(data);
+                            } else {
+                                errorMessage = "";
+                            }
+                            Status status = new Status(IStatus.ERROR,
+                                    "DeviceExplorer", 0, errorMessage, null); //$NON-NLS-1$
+                            ErrorDialog.openError(mTree.getShell(), "New Folder Error",
+                                    "New Folder Error", status);
+                        }
+                    });
+                } catch (TimeoutException e) {
+                    // adb failed somehow, we do nothing. We should be
+                    // displaying the error from the output of the shell
+                    // command.
+                } catch (AdbCommandRejectedException e) {
+                    // adb failed somehow, we do nothing. We should be
+                    // displaying the error from the output of the shell
+                    // command.
+                } catch (ShellCommandUnresponsiveException e) {
+                    // adb failed somehow, we do nothing. We should be
+                    // displaying the error from the output of the shell
+                    // command.
+                } catch (IOException e) {
+                    // adb failed somehow, we do nothing. We should be
+                    // displaying the error from the output of the shell
+                    // command.
+                }
+            }
+        }
+    }
+
+    /**
+     * Force a full refresh of the explorer.
+     */
+    public void refresh() {
+        mTreeViewer.refresh(true);
+    }
+
+    /**
+     * Sets the new device to explorer
+     */
+    public void switchDevice(final IDevice device) {
+        if (device != mCurrentDevice) {
+            mCurrentDevice = device;
+            // now we change the input. but we need to do that in the
+            // ui thread.
+            if (mTree.isDisposed() == false) {
+                Display d = mTree.getDisplay();
+                d.asyncExec(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (mTree.isDisposed() == false) {
+                            // new service
+                            if (mCurrentDevice != null) {
+                                FileListingService fls = mCurrentDevice.getFileListingService();
+                                mContentProvider.setListingService(fls);
+                                mTreeViewer.setInput(fls.getRoot());
+                            }
+                        }
+                    }
+                });
+            }
+        }
+    }
+
+    /**
+     * Refresh an entry from a non ui thread.
+     * @param entry the entry to refresh.
+     */
+    private void refresh(final FileEntry entry) {
+        Display d = mTreeViewer.getTree().getDisplay();
+        d.asyncExec(new Runnable() {
+            @Override
+            public void run() {
+                mTreeViewer.refresh(entry);
+            }
+        });
+    }
+
+    /**
+     * Pulls the selection from a device.
+     * @param items the tree selection the remote file on the device
+     * @param localDirector the local directory in which to save the files.
+     */
+    private void pullSelection(TreeItem[] items, final String localDirectory) {
+        try {
+            final SyncService sync = mCurrentDevice.getSyncService();
+            if (sync != null) {
+                // make a list of the FileEntry.
+                ArrayList<FileEntry> entries = new ArrayList<FileEntry>();
+                for (TreeItem item : items) {
+                    Object data = item.getData();
+                    if (data instanceof FileEntry) {
+                        entries.add((FileEntry)data);
+                    }
+                }
+                final FileEntry[] entryArray = entries.toArray(
+                        new FileEntry[entries.size()]);
+
+                SyncProgressHelper.run(new SyncRunnable() {
+                    @Override
+                    public void run(ISyncProgressMonitor monitor)
+                            throws SyncException, IOException, TimeoutException {
+                        sync.pull(entryArray, localDirectory, monitor);
+                    }
+
+                    @Override
+                    public void close() {
+                        sync.close();
+                    }
+                }, "Pulling file(s) from the device", mParent.getShell());
+            }
+        } catch (SyncException e) {
+            if (e.wasCanceled() == false) {
+                DdmConsole.printErrorToConsole(String.format(
+                        "Failed to pull selection: %1$s", e.getMessage()));
+            }
+        } catch (Exception e) {
+            DdmConsole.printErrorToConsole( "Failed to pull selection");
+            DdmConsole.printErrorToConsole(e.getMessage());
+        }
+    }
+
+    /**
+     * Pulls a file from a device.
+     * @param remote the remote file on the device
+     * @param local the destination filepath
+     */
+    private void pullFile(final FileEntry remote, final String local) {
+        try {
+            final SyncService sync = mCurrentDevice.getSyncService();
+            if (sync != null) {
+                SyncProgressHelper.run(new SyncRunnable() {
+                        @Override
+                        public void run(ISyncProgressMonitor monitor)
+                                throws SyncException, IOException, TimeoutException {
+                            sync.pullFile(remote, local, monitor);
+                        }
+
+                        @Override
+                        public void close() {
+                            sync.close();
+                        }
+                    }, String.format("Pulling %1$s from the device", remote.getName()),
+                    mParent.getShell());
+            }
+        } catch (SyncException e) {
+            if (e.wasCanceled() == false) {
+                DdmConsole.printErrorToConsole(String.format(
+                        "Failed to pull selection: %1$s", e.getMessage()));
+            }
+        } catch (Exception e) {
+            DdmConsole.printErrorToConsole( "Failed to pull selection");
+            DdmConsole.printErrorToConsole(e.getMessage());
+        }
+    }
+
+    /**
+     * Pushes several files and directory into a remote directory.
+     * @param localFiles
+     * @param remoteDirectory
+     */
+    private void pushFiles(final String[] localFiles, final FileEntry remoteDirectory) {
+        try {
+            final SyncService sync = mCurrentDevice.getSyncService();
+            if (sync != null) {
+                SyncProgressHelper.run(new SyncRunnable() {
+                        @Override
+                        public void run(ISyncProgressMonitor monitor)
+                                throws SyncException, IOException, TimeoutException {
+                            sync.push(localFiles, remoteDirectory, monitor);
+                        }
+
+                        @Override
+                        public void close() {
+                            sync.close();
+                        }
+                    }, "Pushing file(s) to the device", mParent.getShell());
+            }
+        } catch (SyncException e) {
+            if (e.wasCanceled() == false) {
+                DdmConsole.printErrorToConsole(String.format(
+                        "Failed to push selection: %1$s", e.getMessage()));
+            }
+        } catch (Exception e) {
+            DdmConsole.printErrorToConsole("Failed to push the items");
+            DdmConsole.printErrorToConsole(e.getMessage());
+        }
+    }
+
+    /**
+     * Pushes a file on a device.
+     * @param local the local filepath of the file to push
+     * @param remoteDirectory the remote destination directory on the device
+     */
+    private void pushFile(final String local, final String remoteDirectory) {
+        try {
+            final SyncService sync = mCurrentDevice.getSyncService();
+            if (sync != null) {
+                // get the file name
+                String[] segs = local.split(Pattern.quote(File.separator));
+                String name = segs[segs.length-1];
+                final String remoteFile = remoteDirectory + FileListingService.FILE_SEPARATOR
+                        + name;
+
+                SyncProgressHelper.run(new SyncRunnable() {
+                        @Override
+                        public void run(ISyncProgressMonitor monitor)
+                                throws SyncException, IOException, TimeoutException {
+                            sync.pushFile(local, remoteFile, monitor);
+                        }
+
+                        @Override
+                        public void close() {
+                            sync.close();
+                        }
+                    }, String.format("Pushing %1$s to the device.", name), mParent.getShell());
+            }
+        } catch (SyncException e) {
+            if (e.wasCanceled() == false) {
+                DdmConsole.printErrorToConsole(String.format(
+                        "Failed to push selection: %1$s", e.getMessage()));
+            }
+        } catch (Exception e) {
+            DdmConsole.printErrorToConsole("Failed to push the item(s).");
+            DdmConsole.printErrorToConsole(e.getMessage());
+        }
+    }
+
+    /**
+     * Sets the enabled state based on a FileEntry properties
+     * @param element The selected FileEntry
+     */
+    protected void setDeleteEnabledState(FileEntry element) {
+        mDeleteAction.setEnabled(element.getType() == FileListingService.TYPE_FILE);
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/FileLabelProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/FileLabelProvider.java
new file mode 100644
index 0000000..1240e59
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/FileLabelProvider.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib.explorer;
+
+import com.android.ddmlib.FileListingService;
+import com.android.ddmlib.FileListingService.FileEntry;
+
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.swt.graphics.Image;
+
+/**
+ * Label provider for the FileEntry.
+ */
+class FileLabelProvider implements ILabelProvider, ITableLabelProvider {
+
+    private Image mFileImage;
+    private Image mFolderImage;
+    private Image mPackageImage;
+    private Image mOtherImage;
+
+    /**
+     * Creates Label provider with custom images.
+     * @param fileImage the Image to represent a file
+     * @param folderImage the Image to represent a folder
+     * @param packageImage the Image to represent a .apk file. If null,
+     *      fileImage is used instead.
+     * @param otherImage the Image to represent all other entry type.
+     */
+    public FileLabelProvider(Image fileImage, Image folderImage,
+            Image packageImage, Image otherImage) {
+        mFileImage = fileImage;
+        mFolderImage = folderImage;
+        mOtherImage = otherImage;
+        if (packageImage != null) {
+            mPackageImage = packageImage;
+        } else {
+            mPackageImage = fileImage;
+        }
+    }
+
+    /**
+     * Creates a label provider with default images.
+     *
+     */
+    public FileLabelProvider() {
+
+    }
+
+    /* (non-Javadoc)
+     * @see org.eclipse.jface.viewers.ILabelProvider#getImage(java.lang.Object)
+     */
+    @Override
+    public Image getImage(Object element) {
+        return null;
+    }
+
+    /* (non-Javadoc)
+     * @see org.eclipse.jface.viewers.ILabelProvider#getText(java.lang.Object)
+     */
+    @Override
+    public String getText(Object element) {
+        return null;
+    }
+
+    @Override
+    public Image getColumnImage(Object element, int columnIndex) {
+        if (columnIndex == 0) {
+            if (element instanceof FileEntry) {
+                FileEntry entry = (FileEntry)element;
+                switch (entry.getType()) {
+                    case FileListingService.TYPE_FILE:
+                    case FileListingService.TYPE_LINK:
+                        // get the name and extension
+                        if (entry.isApplicationPackage()) {
+                            return mPackageImage;
+                        }
+                        return mFileImage;
+                    case FileListingService.TYPE_DIRECTORY:
+                    case FileListingService.TYPE_DIRECTORY_LINK:
+                        return mFolderImage;
+                }
+            }
+
+            // default case return a different image.
+            return mOtherImage;
+        }
+        return null;
+    }
+
+    @Override
+    public String getColumnText(Object element, int columnIndex) {
+        if (element instanceof FileEntry) {
+            FileEntry entry = (FileEntry)element;
+
+            switch (columnIndex) {
+                case 0:
+                    return entry.getName();
+                case 1:
+                    return entry.getSize();
+                case 2:
+                    return entry.getDate();
+                case 3:
+                    return entry.getTime();
+                case 4:
+                    return entry.getPermissions();
+                case 5:
+                    return entry.getInfo();
+            }
+        }
+        return null;
+    }
+
+    /* (non-Javadoc)
+     * @see org.eclipse.jface.viewers.IBaseLabelProvider#addListener(org.eclipse.jface.viewers.ILabelProviderListener)
+     */
+    @Override
+    public void addListener(ILabelProviderListener listener) {
+        // we don't need listeners.
+    }
+
+    /* (non-Javadoc)
+     * @see org.eclipse.jface.viewers.IBaseLabelProvider#dispose()
+     */
+    @Override
+    public void dispose() {
+    }
+
+    /* (non-Javadoc)
+     * @see org.eclipse.jface.viewers.IBaseLabelProvider#isLabelProperty(java.lang.Object, java.lang.String)
+     */
+    @Override
+    public boolean isLabelProperty(Object element, String property) {
+        return false;
+    }
+
+    /* (non-Javadoc)
+     * @see org.eclipse.jface.viewers.IBaseLabelProvider#removeListener(org.eclipse.jface.viewers.ILabelProviderListener)
+     */
+    @Override
+    public void removeListener(ILabelProviderListener listener) {
+        // we don't need listeners
+    }
+
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/handler/BaseFileHandler.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/handler/BaseFileHandler.java
new file mode 100644
index 0000000..f50a94c
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/handler/BaseFileHandler.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2009 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.ddmuilib.handler;
+
+import com.android.ddmlib.ClientData.IHprofDumpHandler;
+import com.android.ddmlib.ClientData.IMethodProfilingHandler;
+import com.android.ddmlib.SyncException;
+import com.android.ddmlib.SyncService;
+import com.android.ddmlib.SyncService.ISyncProgressMonitor;
+import com.android.ddmlib.TimeoutException;
+import com.android.ddmuilib.SyncProgressHelper;
+import com.android.ddmuilib.SyncProgressHelper.SyncRunnable;
+
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Shell;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * Base handler class for handler dealing with files located on a device.
+ *
+ * @see IHprofDumpHandler
+ * @see IMethodProfilingHandler
+ */
+public abstract class BaseFileHandler {
+
+    protected final Shell mParentShell;
+
+    public BaseFileHandler(Shell parentShell) {
+        mParentShell = parentShell;
+    }
+
+    protected abstract String getDialogTitle();
+
+    /**
+     * Prompts the user for a save location and pulls the remote files into this location.
+     * <p/>This <strong>must</strong> be called from the UI Thread.
+     * @param sync the {@link SyncService} to use to pull the file from the device
+     * @param localFileName The default local name
+     * @param remoteFilePath The name of the file to pull off of the device
+     * @param title The title of the File Save dialog.
+     * @return The result of the pull as a {@link SyncResult} object, or null if the sync
+     * didn't happen (canceled by the user).
+     * @throws InvocationTargetException
+     * @throws InterruptedException
+     * @throws SyncException if an error happens during the push of the package on the device.
+     * @throws IOException
+     */
+    protected void promptAndPull(final SyncService sync,
+            String localFileName, final String remoteFilePath, String title)
+            throws InvocationTargetException, InterruptedException, SyncException, TimeoutException,
+            IOException {
+        FileDialog fileDialog = new FileDialog(mParentShell, SWT.SAVE);
+
+        fileDialog.setText(title);
+        fileDialog.setFileName(localFileName);
+
+        final String localFilePath = fileDialog.open();
+        if (localFilePath != null) {
+            SyncProgressHelper.run(new SyncRunnable() {
+                @Override
+                public void run(ISyncProgressMonitor monitor) throws SyncException, IOException,
+                        TimeoutException {
+                    sync.pullFile(remoteFilePath, localFilePath, monitor);
+                }
+
+                @Override
+                public void close() {
+                    sync.close();
+                }
+            },
+            String.format("Pulling %1$s from the device", remoteFilePath), mParentShell);
+        }
+    }
+
+    /**
+     * Prompts the user for a save location and copies a temp file into it.
+     * <p/>This <strong>must</strong> be called from the UI Thread.
+     * @param localFileName The default local name
+     * @param tempFilePath The name of the temp file to copy.
+     * @param title The title of the File Save dialog.
+     * @return true if success, false on error or cancel.
+     */
+    protected boolean promptAndSave(String localFileName, byte[] data, String title) {
+        FileDialog fileDialog = new FileDialog(mParentShell, SWT.SAVE);
+
+        fileDialog.setText(title);
+        fileDialog.setFileName(localFileName);
+
+        String localFilePath = fileDialog.open();
+        if (localFilePath != null) {
+            try {
+                saveFile(data, new File(localFilePath));
+                return true;
+            } catch (IOException e) {
+                String errorMsg = e.getMessage();
+                displayErrorInUiThread(
+                        "Failed to save file '%1$s'%2$s",
+                        localFilePath,
+                        errorMsg != null ? ":\n" + errorMsg : ".");
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Display an error message.
+     * <p/>This will call about to {@link Display} to run this in an async {@link Runnable} in the
+     * UI Thread. This is safe to be called from a non-UI Thread.
+     * @param format the string to display
+     * @param args the string arguments
+     */
+    protected void displayErrorInUiThread(final String format, final Object... args) {
+        mParentShell.getDisplay().asyncExec(new Runnable() {
+            @Override
+            public void run() {
+                MessageDialog.openError(mParentShell, getDialogTitle(),
+                        String.format(format, args));
+            }
+        });
+    }
+
+    /**
+     * Display an error message.
+     * This must be called from the UI Thread.
+     * @param format the string to display
+     * @param args the string arguments
+     */
+    protected void displayErrorFromUiThread(final String format, final Object... args) {
+        MessageDialog.openError(mParentShell, getDialogTitle(),
+                String.format(format, args));
+    }
+
+    /**
+     * Saves a given data into a temp file and returns its corresponding {@link File} object.
+     * @param data the data to save
+     * @return the File into which the data was written or null if it failed.
+     * @throws IOException
+     */
+    protected File saveTempFile(byte[] data, String extension) throws IOException {
+        File f = File.createTempFile("ddms", extension);
+        saveFile(data, f);
+        return f;
+    }
+
+    /**
+     * Saves some data into a given File.
+     * @param data the data to save
+     * @param output the file into the data is saved.
+     * @throws IOException
+     */
+    protected void saveFile(byte[] data, File output) throws IOException {
+        FileOutputStream fos = null;
+        try {
+            fos = new FileOutputStream(output);
+            fos.write(data);
+        } finally {
+            if (fos != null) {
+                fos.close();
+            }
+        }
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/handler/MethodProfilingHandler.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/handler/MethodProfilingHandler.java
new file mode 100644
index 0000000..ab1b5f7
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/handler/MethodProfilingHandler.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2009 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.ddmuilib.handler;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData.IMethodProfilingHandler;
+import com.android.ddmlib.DdmConstants;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.SyncException;
+import com.android.ddmlib.SyncService;
+import com.android.ddmlib.SyncService.ISyncProgressMonitor;
+import com.android.ddmlib.TimeoutException;
+import com.android.ddmuilib.DdmUiPreferences;
+import com.android.ddmuilib.SyncProgressHelper;
+import com.android.ddmuilib.SyncProgressHelper.SyncRunnable;
+import com.android.ddmuilib.console.DdmConsole;
+
+import org.eclipse.swt.widgets.Shell;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * Handler for Method tracing.
+ * This will pull the trace file into a temp file and launch traceview.
+ */
+public class MethodProfilingHandler extends BaseFileHandler
+        implements IMethodProfilingHandler {
+
+    public MethodProfilingHandler(Shell parentShell) {
+        super(parentShell);
+    }
+
+    @Override
+    protected String getDialogTitle() {
+        return "Method Profiling Error";
+    }
+
+    @Override
+    public void onStartFailure(final Client client, final String message) {
+        displayErrorInUiThread(
+                "Unable to create Method Profiling file for application '%1$s'\n\n%2$s" +
+                "Check logcat for more information.",
+                client.getClientData().getClientDescription(),
+                message != null ? message + "\n\n" : "");
+    }
+
+    @Override
+    public void onEndFailure(final Client client, final String message) {
+        displayErrorInUiThread(
+                "Unable to finish Method Profiling for application '%1$s'\n\n%2$s" +
+                "Check logcat for more information.",
+                client.getClientData().getClientDescription(),
+                message != null ? message + "\n\n" : "");
+    }
+
+    @Override
+    public void onSuccess(final String remoteFilePath, final Client client) {
+        mParentShell.getDisplay().asyncExec(new Runnable() {
+            @Override
+            public void run() {
+                if (remoteFilePath == null) {
+                    displayErrorFromUiThread(
+                            "Unable to download trace file: unknown file name.\n" +
+                            "This can happen if you disconnected the device while recording the trace.");
+                    return;
+                }
+
+                final IDevice device = client.getDevice();
+                try {
+                    // get the sync service to pull the HPROF file
+                    final SyncService sync = client.getDevice().getSyncService();
+                    if (sync != null) {
+                        pullAndOpen(sync, remoteFilePath);
+                    } else {
+                        displayErrorFromUiThread(
+                                "Unable to download trace file from device '%1$s'.",
+                                device.getSerialNumber());
+                    }
+                } catch (Exception e) {
+                    displayErrorFromUiThread("Unable to download trace file from device '%1$s'.",
+                            device.getSerialNumber());
+                }
+            }
+
+        });
+    }
+
+    @Override
+    public void onSuccess(byte[] data, final Client client) {
+        try {
+            File tempFile = saveTempFile(data, DdmConstants.DOT_TRACE);
+            open(tempFile.getAbsolutePath());
+        } catch (IOException e) {
+            String errorMsg = e.getMessage();
+            displayErrorInUiThread(
+                    "Failed to save trace data into temp file%1$s",
+                    errorMsg != null ? ":\n" + errorMsg : ".");
+        }
+    }
+
+    /**
+     * pulls and open a file. This is run from the UI thread.
+     */
+    private void pullAndOpen(final SyncService sync, final String remoteFilePath)
+            throws InvocationTargetException, InterruptedException, IOException {
+        // get a temp file
+        File temp = File.createTempFile("android", DdmConstants.DOT_TRACE); //$NON-NLS-1$
+        final String tempPath = temp.getAbsolutePath();
+
+        // pull the file
+        try {
+            SyncProgressHelper.run(new SyncRunnable() {
+                    @Override
+                    public void run(ISyncProgressMonitor monitor)
+                            throws SyncException, IOException, TimeoutException {
+                        sync.pullFile(remoteFilePath, tempPath, monitor);
+                    }
+
+                    @Override
+                    public void close() {
+                        sync.close();
+                    }
+                },
+                String.format("Pulling %1$s from the device", remoteFilePath), mParentShell);
+
+            // open the temp file in traceview
+            open(tempPath);
+        } catch (SyncException e) {
+            if (e.wasCanceled() == false) {
+                displayErrorFromUiThread("Unable to download trace file:\n\n%1$s", e.getMessage());
+            }
+        } catch (TimeoutException e) {
+            displayErrorFromUiThread("Unable to download trace file:\n\ntimeout");
+        }
+    }
+
+    protected void open(String tempPath) {
+        // now that we have the file, we need to launch traceview
+        String[] command = new String[2];
+        command[0] = DdmUiPreferences.getTraceview();
+        command[1] = tempPath;
+
+        try {
+            final Process p = Runtime.getRuntime().exec(command);
+
+            // create a thread for the output
+            new Thread("Traceview output") {
+                @Override
+                public void run() {
+                    // create a buffer to read the stderr output
+                    InputStreamReader is = new InputStreamReader(p.getErrorStream());
+                    BufferedReader resultReader = new BufferedReader(is);
+
+                    // read the lines as they come. if null is returned, it's
+                    // because the process finished
+                    try {
+                        while (true) {
+                            String line = resultReader.readLine();
+                            if (line != null) {
+                                DdmConsole.printErrorToConsole("Traceview: " + line);
+                            } else {
+                                break;
+                            }
+                        }
+                        // get the return code from the process
+                        p.waitFor();
+                    } catch (Exception e) {
+                        Log.e("traceview", e);
+                    }
+                }
+            }.start();
+        } catch (IOException e) {
+            Log.e("traceview", e);
+        }
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDataImporter.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDataImporter.java
new file mode 100644
index 0000000..88db5cc
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDataImporter.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.heap;
+
+import com.android.ddmlib.NativeAllocationInfo;
+import com.android.ddmlib.NativeStackCallInfo;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+
+import java.io.IOException;
+import java.io.LineNumberReader;
+import java.io.Reader;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.InputMismatchException;
+import java.util.List;
+import java.util.Scanner;
+import java.util.regex.Pattern;
+
+public class NativeHeapDataImporter implements IRunnableWithProgress {
+    private LineNumberReader mReader;
+    private int mStartLineNumber;
+    private int mEndLineNumber;
+
+    private NativeHeapSnapshot mSnapshot;
+
+    public NativeHeapDataImporter(Reader stream) {
+        mReader = new LineNumberReader(stream);
+        mReader.setLineNumber(1); // start numbering at 1
+    }
+
+    @Override
+    public void run(IProgressMonitor monitor)
+            throws InvocationTargetException, InterruptedException {
+        monitor.beginTask("Importing Heap Data", IProgressMonitor.UNKNOWN);
+
+        List<NativeAllocationInfo> allocations = new ArrayList<NativeAllocationInfo>();
+        try {
+            while (true) {
+                String line;
+                StringBuilder sb = new StringBuilder();
+
+                // read in a sequence of lines corresponding to a single NativeAllocationInfo
+                mStartLineNumber = mReader.getLineNumber();
+                while ((line = mReader.readLine()) != null) {
+                    if (line.trim().length() == 0) {
+                        // each block of allocations end with an empty line
+                        break;
+                    }
+
+                    sb.append(line);
+                    sb.append('\n');
+                }
+                mEndLineNumber = mReader.getLineNumber();
+
+                // parse those lines into a NativeAllocationInfo object
+                String allocationBlock = sb.toString();
+                if (allocationBlock.trim().length() > 0) {
+                    allocations.add(getNativeAllocation(allocationBlock));
+                }
+
+                if (line == null) { // EOF
+                    break;
+                }
+            }
+        } catch (Exception e) {
+            if (e.getMessage() == null) {
+                e = new RuntimeException(genericErrorMessage("Unexpected Parse error"));
+            }
+            throw new InvocationTargetException(e);
+        } finally {
+            try {
+                mReader.close();
+            } catch (IOException e) {
+                // we can ignore this exception
+            }
+            monitor.done();
+        }
+
+        mSnapshot = new NativeHeapSnapshot(allocations);
+    }
+
+    /** Parse a single native allocation dump. This is the complement of
+     * {@link NativeAllocationInfo#toString()}.
+     *
+     * An allocation is of the following form:
+     * Allocations: 1
+     * Size: 344748
+     * Total Size: 344748
+     * BeginStackTrace:
+     *    40069bd8    /lib/libc_malloc_leak.so --- get_backtrace --- /libc/bionic/malloc_leak.c:258
+     *    40069dd8    /lib/libc_malloc_leak.so --- leak_calloc --- /libc/bionic/malloc_leak.c:576
+     *    40069bd8    /lib/libc_malloc_leak.so --- 40069bd8 ---
+     *    40069dd8    /lib/libc_malloc_leak.so --- 40069dd8 ---
+     * EndStackTrace
+     * Note that in the above stack trace, the last two lines are examples where the address
+     * was not resolved.
+     *
+     * @param block a string of lines corresponding to a single {@code NativeAllocationInfo}
+     * @return parse the input and return the corresponding {@link NativeAllocationInfo}
+     * @throws InputMismatchException if there are any parse errors
+     */
+    private NativeAllocationInfo getNativeAllocation(String block) {
+        Scanner sc = new Scanner(block);
+
+        String kw = sc.next();
+        if (!NativeAllocationInfo.ALLOCATIONS_KW.equals(kw)) {
+            throw new InputMismatchException(
+                    expectedKeywordErrorMessage(NativeAllocationInfo.ALLOCATIONS_KW, kw));
+        }
+
+        int allocations = sc.nextInt();
+
+        kw = sc.next();
+        if (!NativeAllocationInfo.SIZE_KW.equals(kw)) {
+            throw new InputMismatchException(
+                    expectedKeywordErrorMessage(NativeAllocationInfo.SIZE_KW, kw));
+        }
+
+        int size = sc.nextInt();
+
+        kw = sc.next();
+        if (!NativeAllocationInfo.TOTAL_SIZE_KW.equals(kw)) {
+            throw new InputMismatchException(
+                    expectedKeywordErrorMessage(NativeAllocationInfo.TOTAL_SIZE_KW, kw));
+        }
+
+        int totalSize = sc.nextInt();
+        if (totalSize != size * allocations) {
+            throw new InputMismatchException(
+                    genericErrorMessage("Total Size does not match size * # of allocations"));
+        }
+
+        NativeAllocationInfo info = new NativeAllocationInfo(size, allocations);
+
+        kw = sc.next();
+        if (!NativeAllocationInfo.BEGIN_STACKTRACE_KW.equals(kw)) {
+            throw new InputMismatchException(
+                    expectedKeywordErrorMessage(NativeAllocationInfo.BEGIN_STACKTRACE_KW, kw));
+        }
+
+        List<NativeStackCallInfo> stackInfo = new ArrayList<NativeStackCallInfo>();
+        Pattern endTracePattern = Pattern.compile(NativeAllocationInfo.END_STACKTRACE_KW);
+
+        while (true) {
+            long address = sc.nextLong(16);
+            info.addStackCallAddress(address);
+
+            String library = sc.next();
+            sc.next();  // ignore "---"
+            String method = scanTillSeparator(sc, "---");
+
+            String filename = "";
+            if (!isUnresolved(method, address)) {
+                filename = sc.next();
+            }
+
+            stackInfo.add(new NativeStackCallInfo(address, library, method, filename));
+
+            if (sc.hasNext(endTracePattern)) {
+                break;
+            }
+        }
+
+        info.setResolvedStackCall(stackInfo);
+        return info;
+    }
+
+    private String scanTillSeparator(Scanner sc, String separator) {
+        StringBuilder sb = new StringBuilder();
+
+        while (true) {
+            String token = sc.next();
+            if (token.equals(separator)) {
+                break;
+            }
+
+            sb.append(token);
+
+            // We do not know the exact delimiter that was skipped over, but we know
+            // that there was atleast 1 whitespace. Add a single whitespace character
+            // to account for this.
+            sb.append(' ');
+        }
+
+        return sb.toString().trim();
+    }
+
+    private boolean isUnresolved(String method, long address) {
+        // a method is unresolved if it is just the hex representation of the address
+        return Long.toString(address, 16).equals(method);
+    }
+
+    private String genericErrorMessage(String message) {
+        return String.format("%1$s between lines %2$d and %3$d",
+                message, mStartLineNumber, mEndLineNumber);
+    }
+
+    private String expectedKeywordErrorMessage(String expected, String actual) {
+        return String.format("Expected keyword '%1$s', saw '%2$s' between lines %3$d to %4$d.",
+                expected, actual, mStartLineNumber, mEndLineNumber);
+    }
+
+    public NativeHeapSnapshot getImportedSnapshot() {
+        return mSnapshot;
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDiffSnapshot.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDiffSnapshot.java
new file mode 100644
index 0000000..9eb6ddf
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDiffSnapshot.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.heap;
+
+import com.android.ddmlib.NativeAllocationInfo;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Models a heap snapshot that is the difference between two snapshots.
+ */
+public class NativeHeapDiffSnapshot extends NativeHeapSnapshot {
+    private long mCommonAllocationsTotalMemory;
+
+    public NativeHeapDiffSnapshot(NativeHeapSnapshot newSnapshot, NativeHeapSnapshot oldSnapshot) {
+        // The diff snapshots behaves like a snapshot that only contains the new allocations
+        // not present in the old snapshot
+        super(getNewAllocations(newSnapshot, oldSnapshot));
+
+        Set<NativeAllocationInfo> commonAllocations =
+                new HashSet<NativeAllocationInfo>(oldSnapshot.getAllocations());
+        commonAllocations.retainAll(newSnapshot.getAllocations());
+
+        // Memory common between the old and new snapshots
+        mCommonAllocationsTotalMemory = getTotalMemory(commonAllocations);
+    }
+
+    private static List<NativeAllocationInfo> getNewAllocations(NativeHeapSnapshot newSnapshot,
+            NativeHeapSnapshot oldSnapshot) {
+        Set<NativeAllocationInfo> allocations =
+                new HashSet<NativeAllocationInfo>(newSnapshot.getAllocations());
+        allocations.removeAll(oldSnapshot.getAllocations());
+        return new ArrayList<NativeAllocationInfo>(allocations);
+    }
+
+    @Override
+    public String getFormattedMemorySize() {
+        // for a diff snapshot, we report the following string for display:
+        //       xxx bytes new allocation + yyy bytes retained from previous allocation
+        //          = zzz bytes total
+
+        long newAllocations = getTotalSize();
+        return String.format("%s bytes new + %s bytes retained = %s bytes total",
+                formatMemorySize(newAllocations),
+                formatMemorySize(mCommonAllocationsTotalMemory),
+                formatMemorySize(newAllocations + mCommonAllocationsTotalMemory));
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapLabelProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapLabelProvider.java
new file mode 100644
index 0000000..b96fa02
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapLabelProvider.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.heap;
+
+import com.android.ddmlib.NativeAllocationInfo;
+import com.android.ddmlib.NativeStackCallInfo;
+
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.swt.graphics.Image;
+
+/**
+ * A Label Provider for the Native Heap TreeViewer in {@link NativeHeapPanel}.
+ */
+public class NativeHeapLabelProvider extends LabelProvider implements ITableLabelProvider {
+    private long mTotalSize;
+
+    @Override
+    public Image getColumnImage(Object arg0, int arg1) {
+        return null;
+    }
+
+    @Override
+    public String getColumnText(Object element, int index) {
+        if (element instanceof NativeAllocationInfo) {
+            return getColumnTextForNativeAllocation((NativeAllocationInfo) element, index);
+        }
+
+        if (element instanceof NativeLibraryAllocationInfo) {
+            return getColumnTextForNativeLibrary((NativeLibraryAllocationInfo) element, index);
+        }
+
+        return null;
+    }
+
+    private String getColumnTextForNativeAllocation(NativeAllocationInfo info, int index) {
+        NativeStackCallInfo stackInfo = info.getRelevantStackCallInfo();
+
+        switch (index) {
+            case 0:
+                return stackInfo == null ? stackResolutionStatus(info) : stackInfo.getLibraryName();
+            case 1:
+                return Integer.toString(info.getSize() * info.getAllocationCount());
+            case 2:
+                return getPercentageString(info.getSize() * info.getAllocationCount(), mTotalSize);
+            case 3:
+                String prefix = "";
+                if (!info.isZygoteChild()) {
+                    prefix = "Z ";
+                }
+                return prefix + Integer.toString(info.getAllocationCount());
+            case 4:
+                return Integer.toString(info.getSize());
+            case 5:
+                return stackInfo == null ? stackResolutionStatus(info) : stackInfo.getMethodName();
+            default:
+                return null;
+        }
+    }
+
+    private String getColumnTextForNativeLibrary(NativeLibraryAllocationInfo info, int index) {
+        switch (index) {
+            case 0:
+                return info.getLibraryName();
+            case 1:
+                return Long.toString(info.getTotalSize());
+            case 2:
+                return getPercentageString(info.getTotalSize(), mTotalSize);
+            default:
+                return null;
+        }
+    }
+
+    private String getPercentageString(long size, long total) {
+        if (total == 0) {
+            return "";
+        }
+
+        return String.format("%.1f%%", (float)(size * 100)/(float)total);
+    }
+
+    private String stackResolutionStatus(NativeAllocationInfo info) {
+        if (info.isStackCallResolved()) {
+            return "?"; // resolved and unknown
+        } else {
+            return "Resolving...";  // still resolving...
+        }
+    }
+
+    /**
+     * Set the total size of the heap dump for use in percentage calculations.
+     * This value should be set whenever the input to the tree changes so that the percentages
+     * are computed correctly.
+     */
+    public void setTotalSize(long totalSize) {
+        mTotalSize = totalSize;
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapPanel.java
new file mode 100644
index 0000000..f6631b7
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapPanel.java
@@ -0,0 +1,1150 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.heap;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.NativeAllocationInfo;
+import com.android.ddmlib.NativeLibraryMapInfo;
+import com.android.ddmlib.NativeStackCallInfo;
+import com.android.ddmuilib.Addr2Line;
+import com.android.ddmuilib.BaseHeapPanel;
+import com.android.ddmuilib.ITableFocusListener;
+import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator;
+import com.android.ddmuilib.ImageLoader;
+import com.android.ddmuilib.TableHelper;
+
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.dialogs.ProgressMonitorDialog;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Sash;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.swt.widgets.ToolItem;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeItem;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Panel to display native heap information. */
+public class NativeHeapPanel extends BaseHeapPanel {
+    private static final boolean USE_OLD_RESOLVER;
+    static {
+        String useOldResolver = System.getenv("ANDROID_DDMS_OLD_SYMRESOLVER");
+        if (useOldResolver != null && useOldResolver.equalsIgnoreCase("true")) {
+            USE_OLD_RESOLVER = true;
+        } else {
+            USE_OLD_RESOLVER = false;
+        }
+    }
+    private final int MAX_DISPLAYED_ERROR_ITEMS = 5;
+
+    private static final String TOOLTIP_EXPORT_DATA = "Export Heap Data";
+    private static final String TOOLTIP_ZYGOTE_ALLOCATIONS = "Show Zygote Allocations";
+    private static final String TOOLTIP_DIFFS_ONLY = "Only show new allocations not present in previous snapshot";
+    private static final String TOOLTIP_GROUPBY = "Group allocations by library.";
+
+    private static final String EXPORT_DATA_IMAGE = "save.png";
+    private static final String ZYGOTE_IMAGE = "zygote.png";
+    private static final String DIFFS_ONLY_IMAGE = "diff.png";
+    private static final String GROUPBY_IMAGE = "groupby.png";
+
+    private static final String SNAPSHOT_HEAP_BUTTON_TEXT = "Snapshot Current Native Heap Usage";
+    private static final String LOAD_HEAP_DATA_BUTTON_TEXT = "Import Heap Data";
+    private static final String SYMBOL_SEARCH_PATH_LABEL_TEXT = "Symbol Search Path:";
+    private static final String SYMBOL_SEARCH_PATH_TEXT_MESSAGE =
+            "List of colon separated paths to search for symbol debug information. See tooltip for examples.";
+    private static final String SYMBOL_SEARCH_PATH_TOOLTIP_TEXT =
+            "Colon separated paths that contain unstripped libraries with debug symbols.\n"
+                    + "e.g.: <android-src>/out/target/product/generic/symbols/system/lib:/path/to/my/app/obj/local/armeabi";
+
+    private static final String PREFS_SHOW_DIFFS_ONLY = "nativeheap.show.diffs.only";
+    private static final String PREFS_SHOW_ZYGOTE_ALLOCATIONS = "nativeheap.show.zygote";
+    private static final String PREFS_GROUP_BY_LIBRARY = "nativeheap.grouby.library";
+    private static final String PREFS_SYMBOL_SEARCH_PATH = "nativeheap.search.path";
+    private static final String PREFS_SASH_HEIGHT_PERCENT = "nativeheap.sash.percent";
+    private static final String PREFS_LAST_IMPORTED_HEAPPATH = "nativeheap.last.import.path";
+    private IPreferenceStore mPrefStore;
+
+    private List<NativeHeapSnapshot> mNativeHeapSnapshots;
+
+    // Maintain the differences between a snapshot and its predecessor.
+    // mDiffSnapshots[i] = mNativeHeapSnapshots[i] - mNativeHeapSnapshots[i-1]
+    // The zeroth entry is null since there is no predecessor.
+    // The list is filled lazily on demand.
+    private List<NativeHeapSnapshot> mDiffSnapshots;
+
+    private Map<Integer, List<NativeHeapSnapshot>> mImportedSnapshotsPerPid;
+
+    private Button mSnapshotHeapButton;
+    private Button mLoadHeapDataButton;
+    private Text mSymbolSearchPathText;
+    private Combo mSnapshotIndexCombo;
+    private Label mMemoryAllocatedText;
+
+    private TreeViewer mDetailsTreeViewer;
+    private TreeViewer mStackTraceTreeViewer;
+    private NativeHeapProviderByAllocations mContentProviderByAllocations;
+    private NativeHeapProviderByLibrary mContentProviderByLibrary;
+    private NativeHeapLabelProvider mDetailsTreeLabelProvider;
+
+    private ToolBar mDetailsToolBar;
+    private ToolItem mGroupByButton;
+    private ToolItem mDiffsOnlyButton;
+    private ToolItem mShowZygoteAllocationsButton;
+    private ToolItem mExportHeapDataButton;
+
+    public NativeHeapPanel(IPreferenceStore prefStore) {
+        mPrefStore = prefStore;
+        mPrefStore.setDefault(PREFS_SASH_HEIGHT_PERCENT, 75);
+        mPrefStore.setDefault(PREFS_SYMBOL_SEARCH_PATH, "");
+        mPrefStore.setDefault(PREFS_GROUP_BY_LIBRARY, false);
+        mPrefStore.setDefault(PREFS_SHOW_ZYGOTE_ALLOCATIONS, true);
+        mPrefStore.setDefault(PREFS_SHOW_DIFFS_ONLY, false);
+
+        mNativeHeapSnapshots = new ArrayList<NativeHeapSnapshot>();
+        mDiffSnapshots = new ArrayList<NativeHeapSnapshot>();
+        mImportedSnapshotsPerPid = new HashMap<Integer, List<NativeHeapSnapshot>>();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void clientChanged(final Client client, int changeMask) {
+        if (client != getCurrentClient()) {
+            return;
+        }
+
+        if ((changeMask & Client.CHANGE_NATIVE_HEAP_DATA) != Client.CHANGE_NATIVE_HEAP_DATA) {
+            return;
+        }
+
+        List<NativeAllocationInfo> allocations = client.getClientData().getNativeAllocationList();
+        if (allocations.size() == 0) {
+            return;
+        }
+
+        // We need to clone this list since getClientData().getNativeAllocationList() clobbers
+        // the list on future updates
+        final List<NativeAllocationInfo> nativeAllocations = shallowCloneList(allocations);
+
+        addNativeHeapSnapshot(new NativeHeapSnapshot(nativeAllocations));
+        updateDisplay();
+
+        // Attempt to resolve symbols in a separate thread.
+        // The UI should be refreshed once the symbols have been resolved.
+        if (USE_OLD_RESOLVER) {
+            Thread t = new Thread(new SymbolResolverTask(nativeAllocations,
+                    client.getClientData().getMappedNativeLibraries()));
+            t.setName("Address to Symbol Resolver");
+            t.start();
+        } else {
+            Display.getDefault().asyncExec(new Runnable() {
+                @Override
+                public void run() {
+                    resolveSymbols();
+                    mDetailsTreeViewer.refresh();
+                    mStackTraceTreeViewer.refresh();
+                }
+
+                public void resolveSymbols() {
+                    Shell shell = Display.getDefault().getActiveShell();
+                    ProgressMonitorDialog d = new ProgressMonitorDialog(shell);
+
+                    NativeSymbolResolverTask resolver = new NativeSymbolResolverTask(
+                            nativeAllocations,
+                            client.getClientData().getMappedNativeLibraries(),
+                            mSymbolSearchPathText.getText());
+
+                    try {
+                        d.run(true, true, resolver);
+                    } catch (InvocationTargetException e) {
+                        MessageDialog.openError(shell,
+                                "Error Resolving Symbols",
+                                e.getCause().getMessage());
+                        return;
+                    } catch (InterruptedException e) {
+                        return;
+                    }
+
+                    MessageDialog.openInformation(shell, "Symbol Resolution Status",
+                            getResolutionStatusMessage(resolver));
+                }
+            });
+        }
+    }
+
+    private String getResolutionStatusMessage(NativeSymbolResolverTask resolver) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("Symbol Resolution Complete.\n\n");
+
+        // show addresses that were not mapped
+        Set<Long> unmappedAddresses = resolver.getUnmappedAddresses();
+        if (unmappedAddresses.size() > 0) {
+            sb.append(String.format("Unmapped addresses (%d): ",
+                    unmappedAddresses.size()));
+            sb.append(getSampleForDisplay(unmappedAddresses));
+            sb.append('\n');
+        }
+
+        // show libraries that were not present on disk
+        Set<String> notFoundLibraries = resolver.getNotFoundLibraries();
+        if (notFoundLibraries.size() > 0) {
+            sb.append(String.format("Libraries not found on disk (%d): ",
+                    notFoundLibraries.size()));
+            sb.append(getSampleForDisplay(notFoundLibraries));
+            sb.append('\n');
+        }
+
+        // show addresses that were mapped but not resolved
+        Set<Long> unresolvableAddresses = resolver.getUnresolvableAddresses();
+        if (unresolvableAddresses.size() > 0) {
+            sb.append(String.format("Unresolved addresses (%d): ",
+                    unresolvableAddresses.size()));
+            sb.append(getSampleForDisplay(unresolvableAddresses));
+            sb.append('\n');
+        }
+
+        if (resolver.getAddr2LineErrorMessage() != null) {
+            sb.append("Error launching addr2line: ");
+            sb.append(resolver.getAddr2LineErrorMessage());
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * Get the string representation for a collection of items.
+     * If there are more items than {@link #MAX_DISPLAYED_ERROR_ITEMS}, then only the first
+     * {@link #MAX_DISPLAYED_ERROR_ITEMS} items are taken into account,
+     * and an ellipsis is added at the end.
+     */
+    private String getSampleForDisplay(Collection<?> items) {
+        StringBuilder sb = new StringBuilder();
+
+        int c = 1;
+        Iterator<?> it = items.iterator();
+        while (it.hasNext()) {
+            Object item = it.next();
+            if (item instanceof Long) {
+                sb.append(String.format("0x%x", item));
+            } else {
+                sb.append(item);
+            }
+
+            if (c == MAX_DISPLAYED_ERROR_ITEMS && it.hasNext()) {
+                sb.append(", ...");
+                break;
+            } else if (it.hasNext()) {
+                sb.append(", ");
+            }
+
+            c++;
+        }
+        return sb.toString();
+    }
+
+    private void addNativeHeapSnapshot(NativeHeapSnapshot snapshot) {
+        mNativeHeapSnapshots.add(snapshot);
+
+        // The diff snapshots are filled in lazily on demand.
+        // But the list needs to be the same size as mNativeHeapSnapshots, so we add a null.
+        mDiffSnapshots.add(null);
+    }
+
+    private List<NativeAllocationInfo> shallowCloneList(List<NativeAllocationInfo> allocations) {
+        List<NativeAllocationInfo> clonedList =
+                new ArrayList<NativeAllocationInfo>(allocations.size());
+
+        for (NativeAllocationInfo i : allocations) {
+            clonedList.add(i);
+        }
+
+        return clonedList;
+    }
+
+    @Override
+    public void deviceSelected() {
+        // pass
+    }
+
+    @Override
+    public void clientSelected() {
+        Client c = getCurrentClient();
+
+        if (c == null) {
+            // if there is no client selected, then we disable the buttons but leave the
+            // display as is so that whatever snapshots are displayed continue to stay
+            // visible to the user.
+            mSnapshotHeapButton.setEnabled(false);
+            mLoadHeapDataButton.setEnabled(false);
+            return;
+        }
+
+        mNativeHeapSnapshots = new ArrayList<NativeHeapSnapshot>();
+        mDiffSnapshots = new ArrayList<NativeHeapSnapshot>();
+
+        mSnapshotHeapButton.setEnabled(true);
+        mLoadHeapDataButton.setEnabled(true);
+
+        List<NativeHeapSnapshot> importedSnapshots = mImportedSnapshotsPerPid.get(
+                c.getClientData().getPid());
+        if (importedSnapshots != null) {
+            for (NativeHeapSnapshot n : importedSnapshots) {
+                addNativeHeapSnapshot(n);
+            }
+        }
+
+        List<NativeAllocationInfo> allocations = c.getClientData().getNativeAllocationList();
+        allocations = shallowCloneList(allocations);
+
+        if (allocations.size() > 0) {
+            addNativeHeapSnapshot(new NativeHeapSnapshot(allocations));
+        }
+
+        updateDisplay();
+    }
+
+    private void updateDisplay() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                updateSnapshotIndexCombo();
+                updateToolbars();
+
+                int lastSnapshotIndex = mNativeHeapSnapshots.size() - 1;
+                displaySnapshot(lastSnapshotIndex);
+                displayStackTraceForSelection();
+            }
+        });
+    }
+
+    private void displaySelectedSnapshot() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                int idx = mSnapshotIndexCombo.getSelectionIndex();
+                displaySnapshot(idx);
+            }
+        });
+    }
+
+    private void displaySnapshot(int index) {
+        if (index < 0 || mNativeHeapSnapshots.size() == 0) {
+            mDetailsTreeViewer.setInput(null);
+            mMemoryAllocatedText.setText("");
+            return;
+        }
+
+        assert index < mNativeHeapSnapshots.size() : "Invalid snapshot index";
+
+        NativeHeapSnapshot snapshot = mNativeHeapSnapshots.get(index);
+        if (mDiffsOnlyButton.getSelection() && index > 0) {
+            snapshot = getDiffSnapshot(index);
+        }
+
+        mMemoryAllocatedText.setText(snapshot.getFormattedMemorySize());
+        mMemoryAllocatedText.pack();
+
+        mDetailsTreeLabelProvider.setTotalSize(snapshot.getTotalSize());
+        mDetailsTreeViewer.setInput(snapshot);
+        mDetailsTreeViewer.refresh();
+    }
+
+    /** Obtain the diff of snapshot[index] & snapshot[index-1] */
+    private NativeHeapSnapshot getDiffSnapshot(int index) {
+        // if it was already computed, simply return that
+        NativeHeapSnapshot diffSnapshot = mDiffSnapshots.get(index);
+        if (diffSnapshot != null) {
+            return diffSnapshot;
+        }
+
+        // compute the diff
+        NativeHeapSnapshot cur = mNativeHeapSnapshots.get(index);
+        NativeHeapSnapshot prev = mNativeHeapSnapshots.get(index - 1);
+        diffSnapshot = new NativeHeapDiffSnapshot(cur, prev);
+
+        // cache for future use
+        mDiffSnapshots.set(index, diffSnapshot);
+
+        return diffSnapshot;
+    }
+
+    private void updateDisplayGrouping() {
+        boolean groupByLibrary = mGroupByButton.getSelection();
+        mPrefStore.setValue(PREFS_GROUP_BY_LIBRARY, groupByLibrary);
+
+        if (groupByLibrary) {
+            mDetailsTreeViewer.setContentProvider(mContentProviderByLibrary);
+        } else {
+            mDetailsTreeViewer.setContentProvider(mContentProviderByAllocations);
+        }
+    }
+
+    private void updateDisplayForZygotes() {
+        boolean displayZygoteMemory = mShowZygoteAllocationsButton.getSelection();
+        mPrefStore.setValue(PREFS_SHOW_ZYGOTE_ALLOCATIONS, displayZygoteMemory);
+
+        // inform the content providers of the zygote display setting
+        mContentProviderByLibrary.displayZygoteMemory(displayZygoteMemory);
+        mContentProviderByAllocations.displayZygoteMemory(displayZygoteMemory);
+
+        // refresh the UI
+        mDetailsTreeViewer.refresh();
+    }
+
+    private void updateSnapshotIndexCombo() {
+        List<String> items = new ArrayList<String>();
+
+        int numSnapshots = mNativeHeapSnapshots.size();
+        for (int i = 0; i < numSnapshots; i++) {
+            // offset indices by 1 so that users see index starting at 1 rather than 0
+            items.add("Snapshot " + (i + 1));
+        }
+
+        mSnapshotIndexCombo.setItems(items.toArray(new String[0]));
+
+        if (numSnapshots > 0) {
+            mSnapshotIndexCombo.setEnabled(true);
+            mSnapshotIndexCombo.select(numSnapshots - 1);
+        } else {
+            mSnapshotIndexCombo.setEnabled(false);
+        }
+    }
+
+    private void updateToolbars() {
+        int numSnapshots = mNativeHeapSnapshots.size();
+        mExportHeapDataButton.setEnabled(numSnapshots > 0);
+    }
+
+    @Override
+    protected Control createControl(Composite parent) {
+        Composite c = new Composite(parent, SWT.NONE);
+        c.setLayout(new GridLayout(1, false));
+        c.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        createControlsSection(c);
+        createDetailsSection(c);
+
+        // Initialize widget state based on whether a client
+        // is selected or not.
+        clientSelected();
+
+        return c;
+    }
+
+    private void createControlsSection(Composite parent) {
+        Composite c = new Composite(parent, SWT.NONE);
+        c.setLayout(new GridLayout(3, false));
+        c.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        createGetHeapDataSection(c);
+
+        Label l = new Label(c, SWT.SEPARATOR | SWT.VERTICAL);
+        l.setLayoutData(new GridData(GridData.FILL_VERTICAL));
+
+        createDisplaySection(c);
+    }
+
+    private void createGetHeapDataSection(Composite parent) {
+        Composite c = new Composite(parent, SWT.NONE);
+        c.setLayout(new GridLayout(1, false));
+
+        createTakeHeapSnapshotButton(c);
+
+        Label l = new Label(c, SWT.SEPARATOR | SWT.HORIZONTAL);
+        l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        createLoadHeapDataButton(c);
+    }
+
+    private void createTakeHeapSnapshotButton(Composite parent) {
+        mSnapshotHeapButton = new Button(parent, SWT.BORDER | SWT.PUSH);
+        mSnapshotHeapButton.setText(SNAPSHOT_HEAP_BUTTON_TEXT);
+        mSnapshotHeapButton.setLayoutData(new GridData());
+
+        // disable by default, enabled only when a client is selected
+        mSnapshotHeapButton.setEnabled(false);
+
+        mSnapshotHeapButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent evt) {
+                snapshotHeap();
+            }
+        });
+    }
+
+    private void snapshotHeap() {
+        Client c = getCurrentClient();
+        assert c != null : "Snapshot Heap could not have been enabled w/o a selected client.";
+
+        // send an async request
+        c.requestNativeHeapInformation();
+    }
+
+    private void createLoadHeapDataButton(Composite parent) {
+        mLoadHeapDataButton = new Button(parent, SWT.BORDER | SWT.PUSH);
+        mLoadHeapDataButton.setText(LOAD_HEAP_DATA_BUTTON_TEXT);
+        mLoadHeapDataButton.setLayoutData(new GridData());
+
+        // disable by default, enabled only when a client is selected
+        mLoadHeapDataButton.setEnabled(false);
+
+        mLoadHeapDataButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent evt) {
+                loadHeapDataFromFile();
+            }
+        });
+    }
+
+    private void loadHeapDataFromFile() {
+        // pop up a file dialog and get the file to load
+        final String path = getHeapDumpToImport();
+        if (path == null) {
+            return;
+        }
+
+        Reader reader = null;
+        try {
+            reader = new FileReader(path);
+        } catch (FileNotFoundException e) {
+            // cannot occur since user input was via a FileDialog
+        }
+
+        Shell shell = Display.getDefault().getActiveShell();
+        ProgressMonitorDialog d = new ProgressMonitorDialog(shell);
+
+        NativeHeapDataImporter importer = new NativeHeapDataImporter(reader);
+        try {
+            d.run(true, true, importer);
+        } catch (InvocationTargetException e) {
+            // exception while parsing, display error to user and then return
+            MessageDialog.openError(shell,
+                    "Error Importing Heap Data",
+                    e.getCause().getMessage());
+            return;
+        } catch (InterruptedException e) {
+            // operation cancelled by user, simply return
+            return;
+        }
+
+        NativeHeapSnapshot snapshot = importer.getImportedSnapshot();
+
+        addToImportedSnapshots(snapshot);   // save imported snapshot for future use
+        addNativeHeapSnapshot(snapshot); // add to currently displayed snapshots as well
+
+        updateDisplay();
+    }
+
+    private void addToImportedSnapshots(NativeHeapSnapshot snapshot) {
+        Client c = getCurrentClient();
+
+        if (c == null) {
+            return;
+        }
+
+        Integer pid = c.getClientData().getPid();
+        List<NativeHeapSnapshot> importedSnapshots = mImportedSnapshotsPerPid.get(pid);
+        if (importedSnapshots == null) {
+            importedSnapshots = new ArrayList<NativeHeapSnapshot>();
+        }
+
+        importedSnapshots.add(snapshot);
+        mImportedSnapshotsPerPid.put(pid, importedSnapshots);
+    }
+
+    private String getHeapDumpToImport() {
+        FileDialog fileDialog = new FileDialog(Display.getDefault().getActiveShell(),
+                SWT.OPEN);
+
+        fileDialog.setText("Import Heap Dump");
+        fileDialog.setFilterExtensions(new String[] {"*.txt"});
+        fileDialog.setFilterPath(mPrefStore.getString(PREFS_LAST_IMPORTED_HEAPPATH));
+
+        String selectedFile = fileDialog.open();
+        if (selectedFile != null) {
+            // save the path to restore in future dialog open
+            mPrefStore.setValue(PREFS_LAST_IMPORTED_HEAPPATH, new File(selectedFile).getParent());
+        }
+        return selectedFile;
+    }
+
+    private void createDisplaySection(Composite parent) {
+        Composite c = new Composite(parent, SWT.NONE);
+        c.setLayout(new GridLayout(2, false));
+        c.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        // Create: Display: __________________
+        createLabel(c, "Display:");
+        mSnapshotIndexCombo = new Combo(c, SWT.NONE | SWT.READ_ONLY);
+        mSnapshotIndexCombo.setItems(new String[] {"No heap snapshots available."});
+        mSnapshotIndexCombo.setEnabled(false);
+        mSnapshotIndexCombo.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                displaySelectedSnapshot();
+            }
+        });
+
+        // Create: Memory Allocated (bytes): _________________
+        createLabel(c, "Memory Allocated:");
+        mMemoryAllocatedText = new Label(c, SWT.NONE);
+        GridData gd = new GridData();
+        gd.widthHint = 100;
+        mMemoryAllocatedText.setLayoutData(gd);
+
+        // Create: Search Path: __________________
+        createLabel(c, SYMBOL_SEARCH_PATH_LABEL_TEXT);
+        mSymbolSearchPathText = new Text(c, SWT.BORDER);
+        mSymbolSearchPathText.setMessage(SYMBOL_SEARCH_PATH_TEXT_MESSAGE);
+        mSymbolSearchPathText.setToolTipText(SYMBOL_SEARCH_PATH_TOOLTIP_TEXT);
+        mSymbolSearchPathText.addModifyListener(new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent arg0) {
+                String path = mSymbolSearchPathText.getText();
+                updateSearchPath(path);
+                mPrefStore.setValue(PREFS_SYMBOL_SEARCH_PATH, path);
+            }
+        });
+        mSymbolSearchPathText.setText(mPrefStore.getString(PREFS_SYMBOL_SEARCH_PATH));
+        mSymbolSearchPathText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+    }
+
+    private void updateSearchPath(String path) {
+        Addr2Line.setSearchPath(path);
+    }
+
+    private void createLabel(Composite parent, String text) {
+        Label l = new Label(parent, SWT.NONE);
+        l.setText(text);
+        GridData gd = new GridData();
+        gd.horizontalAlignment = SWT.RIGHT;
+        l.setLayoutData(gd);
+    }
+
+    /**
+     * Create the details section displaying the details table and the stack trace
+     * corresponding to the selection.
+     *
+     * The details is laid out like so:
+     *   Details Toolbar
+     *   Details Table
+     *   ------------sash---
+     *   Stack Trace Label
+     *   Stack Trace Text
+     * There is a sash in between the two sections, and we need to save/restore the sash
+     * preferences. Using FormLayout seems like the easiest solution here, but the layout
+     * code looks ugly as a result.
+     */
+    private void createDetailsSection(Composite parent) {
+        final Composite c = new Composite(parent, SWT.NONE);
+        c.setLayout(new FormLayout());
+        c.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        mDetailsToolBar = new ToolBar(c, SWT.FLAT | SWT.BORDER);
+        initializeDetailsToolBar(mDetailsToolBar);
+
+        Tree detailsTree = new Tree(c, SWT.VIRTUAL | SWT.BORDER | SWT.MULTI);
+        initializeDetailsTree(detailsTree);
+
+        final Sash sash = new Sash(c, SWT.HORIZONTAL | SWT.BORDER);
+
+        Label stackTraceLabel = new Label(c, SWT.NONE);
+        stackTraceLabel.setText("Stack Trace:");
+
+        Tree stackTraceTree = new Tree(c, SWT.BORDER | SWT.MULTI);
+        initializeStackTraceTree(stackTraceTree);
+
+        // layout the widgets created above
+        FormData data = new FormData();
+        data.top    = new FormAttachment(0, 0);
+        data.left   = new FormAttachment(0, 0);
+        data.right  = new FormAttachment(100, 0);
+        mDetailsToolBar.setLayoutData(data);
+
+        data = new FormData();
+        data.top    = new FormAttachment(mDetailsToolBar, 0);
+        data.bottom = new FormAttachment(sash, 0);
+        data.left   = new FormAttachment(0, 0);
+        data.right  = new FormAttachment(100, 0);
+        detailsTree.setLayoutData(data);
+
+        final FormData sashData = new FormData();
+        sashData.top    = new FormAttachment(mPrefStore.getInt(PREFS_SASH_HEIGHT_PERCENT), 0);
+        sashData.left   = new FormAttachment(0, 0);
+        sashData.right  = new FormAttachment(100, 0);
+        sash.setLayoutData(sashData);
+
+        data = new FormData();
+        data.top    = new FormAttachment(sash, 0);
+        data.left   = new FormAttachment(0, 0);
+        data.right  = new FormAttachment(100, 0);
+        stackTraceLabel.setLayoutData(data);
+
+        data = new FormData();
+        data.top    = new FormAttachment(stackTraceLabel, 0);
+        data.left   = new FormAttachment(0, 0);
+        data.bottom = new FormAttachment(100, 0);
+        data.right  = new FormAttachment(100, 0);
+        stackTraceTree.setLayoutData(data);
+
+        sash.addListener(SWT.Selection, new Listener() {
+            @Override
+            public void handleEvent(Event e) {
+                Rectangle sashRect = sash.getBounds();
+                Rectangle panelRect = c.getClientArea();
+                int sashPercent = sashRect.y * 100 / panelRect.height;
+                mPrefStore.setValue(PREFS_SASH_HEIGHT_PERCENT, sashPercent);
+
+                sashData.top = new FormAttachment(0, e.y);
+                c.layout();
+            }
+        });
+    }
+
+    private void initializeDetailsToolBar(ToolBar toolbar) {
+        mGroupByButton = new ToolItem(toolbar, SWT.CHECK);
+        mGroupByButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage(GROUPBY_IMAGE,
+                toolbar.getDisplay()));
+        mGroupByButton.setToolTipText(TOOLTIP_GROUPBY);
+        mGroupByButton.setSelection(mPrefStore.getBoolean(PREFS_GROUP_BY_LIBRARY));
+        mGroupByButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                updateDisplayGrouping();
+            }
+        });
+
+        mDiffsOnlyButton = new ToolItem(toolbar, SWT.CHECK);
+        mDiffsOnlyButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage(DIFFS_ONLY_IMAGE,
+                toolbar.getDisplay()));
+        mDiffsOnlyButton.setToolTipText(TOOLTIP_DIFFS_ONLY);
+        mDiffsOnlyButton.setSelection(mPrefStore.getBoolean(PREFS_SHOW_DIFFS_ONLY));
+        mDiffsOnlyButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                // simply refresh the display, as the display logic takes care of
+                // the current state of the diffs only checkbox.
+                int idx = mSnapshotIndexCombo.getSelectionIndex();
+                displaySnapshot(idx);
+            }
+        });
+
+        mShowZygoteAllocationsButton = new ToolItem(toolbar, SWT.CHECK);
+        mShowZygoteAllocationsButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage(
+                ZYGOTE_IMAGE, toolbar.getDisplay()));
+        mShowZygoteAllocationsButton.setToolTipText(TOOLTIP_ZYGOTE_ALLOCATIONS);
+        mShowZygoteAllocationsButton.setSelection(
+                mPrefStore.getBoolean(PREFS_SHOW_ZYGOTE_ALLOCATIONS));
+        mShowZygoteAllocationsButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                updateDisplayForZygotes();
+            }
+        });
+
+        mExportHeapDataButton = new ToolItem(toolbar, SWT.PUSH);
+        mExportHeapDataButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage(
+                EXPORT_DATA_IMAGE, toolbar.getDisplay()));
+        mExportHeapDataButton.setToolTipText(TOOLTIP_EXPORT_DATA);
+        mExportHeapDataButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                exportSnapshot();
+            }
+        });
+    }
+
+    /** Export currently displayed snapshot to a file */
+    private void exportSnapshot() {
+        int idx = mSnapshotIndexCombo.getSelectionIndex();
+        String snapshotName = mSnapshotIndexCombo.getItem(idx);
+
+        FileDialog fileDialog = new FileDialog(Display.getDefault().getActiveShell(),
+                SWT.SAVE);
+
+        fileDialog.setText("Save " + snapshotName);
+        fileDialog.setFileName("allocations.txt");
+
+        final String fileName = fileDialog.open();
+        if (fileName == null) {
+            return;
+        }
+
+        final NativeHeapSnapshot snapshot = mNativeHeapSnapshots.get(idx);
+        Thread t = new Thread(new Runnable() {
+            @Override
+            public void run() {
+                PrintWriter out;
+                try {
+                    out = new PrintWriter(new BufferedWriter(new FileWriter(fileName)));
+                } catch (IOException e) {
+                    displayErrorMessage(e.getMessage());
+                    return;
+                }
+
+                for (NativeAllocationInfo alloc : snapshot.getAllocations()) {
+                    out.println(alloc.toString());
+                }
+                out.close();
+            }
+
+            private void displayErrorMessage(final String message) {
+                Display.getDefault().syncExec(new Runnable() {
+                    @Override
+                    public void run() {
+                        MessageDialog.openError(Display.getDefault().getActiveShell(),
+                                "Failed to export heap data", message);
+                    }
+                });
+            }
+        });
+        t.setName("Saving Heap Data to File...");
+        t.start();
+    }
+
+    private void initializeDetailsTree(Tree tree) {
+        tree.setHeaderVisible(true);
+        tree.setLinesVisible(true);
+
+        List<String> properties = Arrays.asList(new String[] {
+                "Library",
+                "Total",
+                "Percentage",
+                "Count",
+                "Size",
+                "Method",
+        });
+
+        List<String> sampleValues = Arrays.asList(new String[] {
+                "/path/in/device/to/system/library.so",
+                "123456789",
+                " 100%",
+                "123456789",
+                "123456789",
+                "PossiblyLongDemangledMethodName",
+        });
+
+        // right align numeric values
+        List<Integer> swtFlags = Arrays.asList(new Integer[] {
+                SWT.LEFT,
+                SWT.RIGHT,
+                SWT.RIGHT,
+                SWT.RIGHT,
+                SWT.RIGHT,
+                SWT.LEFT,
+        });
+
+        for (int i = 0; i < properties.size(); i++) {
+            String p = properties.get(i);
+            String v = sampleValues.get(i);
+            int flags = swtFlags.get(i);
+            TableHelper.createTreeColumn(tree, p, flags, v, getPref("details", p), mPrefStore);
+        }
+
+        mDetailsTreeViewer = new TreeViewer(tree);
+
+        mDetailsTreeViewer.setUseHashlookup(true);
+
+        boolean displayZygotes = mPrefStore.getBoolean(PREFS_SHOW_ZYGOTE_ALLOCATIONS);
+        mContentProviderByAllocations = new NativeHeapProviderByAllocations(mDetailsTreeViewer,
+                displayZygotes);
+        mContentProviderByLibrary = new NativeHeapProviderByLibrary(mDetailsTreeViewer,
+                displayZygotes);
+        if (mPrefStore.getBoolean(PREFS_GROUP_BY_LIBRARY)) {
+            mDetailsTreeViewer.setContentProvider(mContentProviderByLibrary);
+        } else {
+            mDetailsTreeViewer.setContentProvider(mContentProviderByAllocations);
+        }
+
+        mDetailsTreeLabelProvider = new NativeHeapLabelProvider();
+        mDetailsTreeViewer.setLabelProvider(mDetailsTreeLabelProvider);
+
+        mDetailsTreeViewer.setInput(null);
+
+        tree.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent event) {
+                displayStackTraceForSelection();
+            }
+        });
+    }
+
+    private void initializeStackTraceTree(Tree tree) {
+        tree.setHeaderVisible(true);
+        tree.setLinesVisible(true);
+
+        List<String> properties = Arrays.asList(new String[] {
+                "Address",
+                "Library",
+                "Method",
+                "File",
+                "Line",
+        });
+
+        List<String> sampleValues = Arrays.asList(new String[] {
+                "0x1234_5678",
+                "/path/in/device/to/system/library.so",
+                "PossiblyLongDemangledMethodName",
+                "/android/out/prefix/in/home/directory/to/path/in/device/to/system/library.so",
+                "2000",
+        });
+
+        for (int i = 0; i < properties.size(); i++) {
+            String p = properties.get(i);
+            String v = sampleValues.get(i);
+            TableHelper.createTreeColumn(tree, p, SWT.LEFT, v, getPref("stack", p), mPrefStore);
+        }
+
+        mStackTraceTreeViewer = new TreeViewer(tree);
+
+        mStackTraceTreeViewer.setContentProvider(new NativeStackContentProvider());
+        mStackTraceTreeViewer.setLabelProvider(new NativeStackLabelProvider());
+
+        mStackTraceTreeViewer.setInput(null);
+    }
+
+    private void displayStackTraceForSelection() {
+        TreeItem []items = mDetailsTreeViewer.getTree().getSelection();
+        if (items.length == 0) {
+            mStackTraceTreeViewer.setInput(null);
+            return;
+        }
+
+        Object data = items[0].getData();
+        if (!(data instanceof NativeAllocationInfo)) {
+            mStackTraceTreeViewer.setInput(null);
+            return;
+        }
+
+        NativeAllocationInfo info = (NativeAllocationInfo) data;
+        if (info.isStackCallResolved()) {
+            mStackTraceTreeViewer.setInput(info.getResolvedStackCall());
+        } else {
+            mStackTraceTreeViewer.setInput(info.getStackCallAddresses());
+        }
+    }
+
+    private String getPref(String prefix, String s) {
+        return "nativeheap.tree." + prefix + "." + s;
+    }
+
+    @Override
+    public void setFocus() {
+    }
+
+    private ITableFocusListener mTableFocusListener;
+
+    @Override
+    public void setTableFocusListener(ITableFocusListener listener) {
+        mTableFocusListener = listener;
+
+        final Tree heapSitesTree = mDetailsTreeViewer.getTree();
+        final IFocusedTableActivator heapSitesActivator = new IFocusedTableActivator() {
+            @Override
+            public void copy(Clipboard clipboard) {
+                TreeItem[] items = heapSitesTree.getSelection();
+                copyToClipboard(items, clipboard);
+            }
+
+            @Override
+            public void selectAll() {
+                heapSitesTree.selectAll();
+            }
+        };
+
+        heapSitesTree.addFocusListener(new FocusListener() {
+            @Override
+            public void focusLost(FocusEvent arg0) {
+                mTableFocusListener.focusLost(heapSitesActivator);
+            }
+
+            @Override
+            public void focusGained(FocusEvent arg0) {
+                mTableFocusListener.focusGained(heapSitesActivator);
+            }
+        });
+
+        final Tree stackTraceTree = mStackTraceTreeViewer.getTree();
+        final IFocusedTableActivator stackTraceActivator = new IFocusedTableActivator() {
+            @Override
+            public void copy(Clipboard clipboard) {
+                TreeItem[] items = stackTraceTree.getSelection();
+                copyToClipboard(items, clipboard);
+            }
+
+            @Override
+            public void selectAll() {
+                stackTraceTree.selectAll();
+            }
+        };
+
+        stackTraceTree.addFocusListener(new FocusListener() {
+            @Override
+            public void focusLost(FocusEvent arg0) {
+                mTableFocusListener.focusLost(stackTraceActivator);
+            }
+
+            @Override
+            public void focusGained(FocusEvent arg0) {
+                mTableFocusListener.focusGained(stackTraceActivator);
+            }
+        });
+    }
+
+    private void copyToClipboard(TreeItem[] items, Clipboard clipboard) {
+        StringBuilder sb = new StringBuilder();
+
+        for (TreeItem item : items) {
+            Object data = item.getData();
+            if (data != null) {
+                sb.append(data.toString());
+                sb.append('\n');
+            }
+        }
+
+        String content = sb.toString();
+        if (content.length() > 0) {
+            clipboard.setContents(
+                    new Object[] {sb.toString()},
+                    new Transfer[] {TextTransfer.getInstance()}
+                    );
+        }
+    }
+
+    private class SymbolResolverTask implements Runnable {
+        private List<NativeAllocationInfo> mCallSites;
+        private List<NativeLibraryMapInfo> mMappedLibraries;
+        private Map<Long, NativeStackCallInfo> mResolvedSymbolCache;
+
+        public SymbolResolverTask(List<NativeAllocationInfo> callSites,
+                List<NativeLibraryMapInfo> mappedLibraries) {
+            mCallSites = callSites;
+            mMappedLibraries = mappedLibraries;
+
+            mResolvedSymbolCache = new HashMap<Long, NativeStackCallInfo>();
+        }
+
+        @Override
+        public void run() {
+            for (NativeAllocationInfo callSite : mCallSites) {
+                if (callSite.isStackCallResolved()) {
+                    continue;
+                }
+
+                List<Long> addresses = callSite.getStackCallAddresses();
+                List<NativeStackCallInfo> resolvedStackInfo =
+                        new ArrayList<NativeStackCallInfo>(addresses.size());
+
+                for (Long address : addresses) {
+                    NativeStackCallInfo info = mResolvedSymbolCache.get(address);
+
+                    if (info != null) {
+                        resolvedStackInfo.add(info);
+                    } else {
+                        info = resolveAddress(address);
+                        resolvedStackInfo.add(info);
+                        mResolvedSymbolCache.put(address, info);
+                    }
+                }
+
+                callSite.setResolvedStackCall(resolvedStackInfo);
+            }
+
+            Display.getDefault().asyncExec(new Runnable() {
+                @Override
+                public void run() {
+                    mDetailsTreeViewer.refresh();
+                    mStackTraceTreeViewer.refresh();
+                }
+            });
+        }
+
+        private NativeStackCallInfo resolveAddress(long addr) {
+            NativeLibraryMapInfo library = getLibraryFor(addr);
+
+            if (library != null) {
+                Addr2Line process = Addr2Line.getProcess(library);
+                if (process != null) {
+                    NativeStackCallInfo info = process.getAddress(addr);
+                    if (info != null) {
+                        return info;
+                    }
+                }
+            }
+
+            return new NativeStackCallInfo(addr,
+                    library != null ? library.getLibraryName() : null,
+                    Long.toHexString(addr),
+                    "");
+        }
+
+        private NativeLibraryMapInfo getLibraryFor(long addr) {
+            for (NativeLibraryMapInfo info : mMappedLibraries) {
+                if (info.isWithinLibrary(addr)) {
+                    return info;
+                }
+            }
+
+            Log.d("ddm-nativeheap", "Failed finding Library for " + Long.toHexString(addr));
+            return null;
+        }
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByAllocations.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByAllocations.java
new file mode 100644
index 0000000..c31716b
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByAllocations.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.heap;
+
+import com.android.ddmlib.NativeAllocationInfo;
+
+import org.eclipse.jface.viewers.ILazyTreeContentProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+
+import java.util.List;
+
+/**
+ * Content Provider for the native heap tree viewer in {@link NativeHeapPanel}.
+ * It expects a {@link NativeHeapSnapshot} as input, and provides the list of allocations
+ * in the heap dump as content to the UI.
+ */
+public final class NativeHeapProviderByAllocations implements ILazyTreeContentProvider {
+    private TreeViewer mViewer;
+    private boolean mDisplayZygoteMemory;
+    private NativeHeapSnapshot mNativeHeapDump;
+
+    public NativeHeapProviderByAllocations(TreeViewer viewer, boolean displayZygotes) {
+        mViewer = viewer;
+        mDisplayZygoteMemory = displayZygotes;
+    }
+
+    @Override
+    public void dispose() {
+    }
+
+    @Override
+    public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+        mNativeHeapDump = (NativeHeapSnapshot) newInput;
+    }
+
+    @Override
+    public Object getParent(Object arg0) {
+        return null;
+    }
+
+    @Override
+    public void updateChildCount(Object element, int currentChildCount) {
+        int childCount = 0;
+
+        if (element == mNativeHeapDump) { // root element
+            childCount = getAllocations().size();
+        }
+
+        mViewer.setChildCount(element, childCount);
+    }
+
+    @Override
+    public void updateElement(Object parent, int index) {
+        Object item = null;
+
+        if (parent == mNativeHeapDump) { // root element
+            item = getAllocations().get(index);
+        }
+
+        mViewer.replace(parent, index, item);
+        mViewer.setChildCount(item, 0);
+    }
+
+    public void displayZygoteMemory(boolean en) {
+        mDisplayZygoteMemory = en;
+    }
+
+    private List<NativeAllocationInfo> getAllocations() {
+        if (mDisplayZygoteMemory) {
+            return mNativeHeapDump.getAllocations();
+        } else {
+            return mNativeHeapDump.getNonZygoteAllocations();
+        }
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByLibrary.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByLibrary.java
new file mode 100644
index 0000000..b786bfa
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByLibrary.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.heap;
+
+import org.eclipse.jface.viewers.ILazyTreeContentProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+
+import java.util.List;
+
+/**
+ * Content Provider for the native heap tree viewer in {@link NativeHeapPanel}.
+ * It expects input of type {@link NativeHeapSnapshot}, and provides heap allocations
+ * grouped by library to the UI.
+ */
+public class NativeHeapProviderByLibrary implements ILazyTreeContentProvider {
+    private TreeViewer mViewer;
+    private boolean mDisplayZygoteMemory;
+
+    public NativeHeapProviderByLibrary(TreeViewer viewer, boolean displayZygotes) {
+        mViewer = viewer;
+        mDisplayZygoteMemory = displayZygotes;
+    }
+
+    @Override
+    public void dispose() {
+    }
+
+    @Override
+    public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+    }
+
+    @Override
+    public Object getParent(Object element) {
+        return null;
+    }
+
+    @Override
+    public void updateChildCount(Object element, int currentChildCount) {
+        int childCount = 0;
+
+        if (element instanceof NativeHeapSnapshot) {
+            NativeHeapSnapshot snapshot = (NativeHeapSnapshot) element;
+            childCount = getLibraryAllocations(snapshot).size();
+        }
+
+        mViewer.setChildCount(element, childCount);
+    }
+
+    @Override
+    public void updateElement(Object parent, int index) {
+        Object item = null;
+        int childCount = 0;
+
+        if (parent instanceof NativeHeapSnapshot) { // root element
+            NativeHeapSnapshot snapshot = (NativeHeapSnapshot) parent;
+            item = getLibraryAllocations(snapshot).get(index);
+            childCount = ((NativeLibraryAllocationInfo) item).getAllocations().size();
+        } else if (parent instanceof NativeLibraryAllocationInfo) {
+            item = ((NativeLibraryAllocationInfo) parent).getAllocations().get(index);
+        }
+
+        mViewer.replace(parent, index, item);
+        mViewer.setChildCount(item, childCount);
+    }
+
+    public void displayZygoteMemory(boolean en) {
+        mDisplayZygoteMemory = en;
+    }
+
+    private List<NativeLibraryAllocationInfo> getLibraryAllocations(NativeHeapSnapshot snapshot) {
+        if (mDisplayZygoteMemory) {
+            return snapshot.getAllocationsByLibrary();
+        } else {
+            return snapshot.getNonZygoteAllocationsByLibrary();
+        }
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapSnapshot.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapSnapshot.java
new file mode 100644
index 0000000..e2023d2
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapSnapshot.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.heap;
+
+import com.android.ddmlib.NativeAllocationInfo;
+
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * A Native Heap Snapshot models a single heap dump.
+ *
+ * It primarily consists of a list of {@link NativeAllocationInfo} objects. From this list,
+ * other objects of interest to the UI are computed and cached for future use.
+ */
+public class NativeHeapSnapshot {
+    private static final NumberFormat NUMBER_FORMATTER = NumberFormat.getInstance();
+
+    private List<NativeAllocationInfo> mHeapAllocations;
+    private List<NativeLibraryAllocationInfo> mHeapAllocationsByLibrary;
+
+    private List<NativeAllocationInfo> mNonZygoteHeapAllocations;
+    private List<NativeLibraryAllocationInfo> mNonZygoteHeapAllocationsByLibrary;
+
+    private long mTotalSize;
+
+    public NativeHeapSnapshot(List<NativeAllocationInfo> heapAllocations) {
+        mHeapAllocations = heapAllocations;
+
+        // precompute the total size as this is always needed.
+        mTotalSize = getTotalMemory(heapAllocations);
+    }
+
+    protected long getTotalMemory(Collection<NativeAllocationInfo> heapSnapshot) {
+        long total = 0;
+
+        for (NativeAllocationInfo info : heapSnapshot) {
+            total += info.getAllocationCount() * info.getSize();
+        }
+
+        return total;
+    }
+
+    public List<NativeAllocationInfo> getAllocations() {
+        return mHeapAllocations;
+    }
+
+    public List<NativeLibraryAllocationInfo> getAllocationsByLibrary() {
+        if (mHeapAllocationsByLibrary != null) {
+            return mHeapAllocationsByLibrary;
+        }
+
+        List<NativeLibraryAllocationInfo> heapAllocations =
+                NativeLibraryAllocationInfo.constructFrom(mHeapAllocations);
+
+        // cache for future uses only if it is fully resolved.
+        if (isFullyResolved(heapAllocations)) {
+            mHeapAllocationsByLibrary = heapAllocations;
+        }
+
+        return heapAllocations;
+    }
+
+    private boolean isFullyResolved(List<NativeLibraryAllocationInfo> heapAllocations) {
+        for (NativeLibraryAllocationInfo info : heapAllocations) {
+            if (info.getLibraryName().equals(NativeLibraryAllocationInfo.UNRESOLVED_LIBRARY_NAME)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    public long getTotalSize() {
+        return mTotalSize;
+    }
+
+    public String getFormattedMemorySize() {
+        return String.format("%s bytes", formatMemorySize(getTotalSize()));
+    }
+
+    protected String formatMemorySize(long memSize) {
+        return NUMBER_FORMATTER.format(memSize);
+    }
+
+    public List<NativeAllocationInfo> getNonZygoteAllocations() {
+        if (mNonZygoteHeapAllocations != null) {
+            return mNonZygoteHeapAllocations;
+        }
+
+        // filter out all zygote allocations
+        mNonZygoteHeapAllocations = new ArrayList<NativeAllocationInfo>();
+        for (NativeAllocationInfo info : mHeapAllocations) {
+            if (info.isZygoteChild()) {
+                mNonZygoteHeapAllocations.add(info);
+            }
+        }
+
+        return mNonZygoteHeapAllocations;
+    }
+
+    public List<NativeLibraryAllocationInfo> getNonZygoteAllocationsByLibrary() {
+        if (mNonZygoteHeapAllocationsByLibrary != null) {
+            return mNonZygoteHeapAllocationsByLibrary;
+        }
+
+        List<NativeLibraryAllocationInfo> heapAllocations =
+                NativeLibraryAllocationInfo.constructFrom(getNonZygoteAllocations());
+
+        // cache for future uses only if it is fully resolved.
+        if (isFullyResolved(heapAllocations)) {
+            mNonZygoteHeapAllocationsByLibrary = heapAllocations;
+        }
+
+        return heapAllocations;
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeLibraryAllocationInfo.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeLibraryAllocationInfo.java
new file mode 100644
index 0000000..1722cdb
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeLibraryAllocationInfo.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.heap;
+
+import com.android.ddmlib.NativeAllocationInfo;
+import com.android.ddmlib.NativeStackCallInfo;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A heap dump representation where each call site is associated with its source library.
+ */
+public final class NativeLibraryAllocationInfo {
+    /** Library name to use when grouping before symbol resolution is complete. */
+    public static final String UNRESOLVED_LIBRARY_NAME = "Resolving..";
+
+    /** Any call site that cannot be resolved to a specific library goes under this name. */
+    private static final String UNKNOWN_LIBRARY_NAME = "unknown";
+
+    private final String mLibraryName;
+    private final List<NativeAllocationInfo> mHeapAllocations;
+    private int mTotalSize;
+
+    private NativeLibraryAllocationInfo(String libraryName) {
+        mLibraryName = libraryName;
+        mHeapAllocations = new ArrayList<NativeAllocationInfo>();
+    }
+
+    private void addAllocation(NativeAllocationInfo info) {
+        mHeapAllocations.add(info);
+    }
+
+    private void updateTotalSize() {
+        mTotalSize = 0;
+        for (NativeAllocationInfo i : mHeapAllocations) {
+            mTotalSize += i.getAllocationCount() * i.getSize();
+        }
+    }
+
+    public String getLibraryName() {
+        return mLibraryName;
+    }
+
+    public long getTotalSize() {
+        return mTotalSize;
+    }
+
+    public List<NativeAllocationInfo> getAllocations() {
+        return mHeapAllocations;
+    }
+
+    /**
+     * Factory method to create a list of {@link NativeLibraryAllocationInfo} objects,
+     * given the list of {@link NativeAllocationInfo} objects.
+     *
+     * If the {@link NativeAllocationInfo} objects do not have their symbols resolved,
+     * then they are grouped under the library {@link #UNRESOLVED_LIBRARY_NAME}. If they do
+     * have their symbols resolved, but map to an unknown library, then they are grouped under
+     * the library {@link #UNKNOWN_LIBRARY_NAME}.
+     */
+    public static List<NativeLibraryAllocationInfo> constructFrom(
+            List<NativeAllocationInfo> allocations) {
+        if (allocations == null) {
+            return null;
+        }
+
+        Map<String, NativeLibraryAllocationInfo> allocationsByLibrary =
+                new HashMap<String, NativeLibraryAllocationInfo>();
+
+        // go through each native allocation and assign it to the appropriate library
+        for (NativeAllocationInfo info : allocations) {
+            String libName = UNRESOLVED_LIBRARY_NAME;
+
+            if (info.isStackCallResolved()) {
+                NativeStackCallInfo relevantStackCall = info.getRelevantStackCallInfo();
+                if (relevantStackCall != null) {
+                    libName = relevantStackCall.getLibraryName();
+                } else {
+                    libName = UNKNOWN_LIBRARY_NAME;
+                }
+            }
+
+            addtoLibrary(allocationsByLibrary, libName, info);
+        }
+
+        List<NativeLibraryAllocationInfo> libraryAllocations =
+                new ArrayList<NativeLibraryAllocationInfo>(allocationsByLibrary.values());
+
+        // now update some summary statistics for each library
+        for (NativeLibraryAllocationInfo l : libraryAllocations) {
+            l.updateTotalSize();
+        }
+
+        // finally, sort by total size
+        Collections.sort(libraryAllocations, new Comparator<NativeLibraryAllocationInfo>() {
+                    @Override
+                    public int compare(NativeLibraryAllocationInfo o1,
+                            NativeLibraryAllocationInfo o2) {
+                        return (int) (o2.getTotalSize() - o1.getTotalSize());
+                    }
+                });
+
+        return libraryAllocations;
+    }
+
+    private static void addtoLibrary(Map<String, NativeLibraryAllocationInfo> libraryAllocations,
+            String libName, NativeAllocationInfo info) {
+        NativeLibraryAllocationInfo libAllocationInfo = libraryAllocations.get(libName);
+        if (libAllocationInfo == null) {
+            libAllocationInfo = new NativeLibraryAllocationInfo(libName);
+            libraryAllocations.put(libName, libAllocationInfo);
+        }
+
+        libAllocationInfo.addAllocation(info);
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackContentProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackContentProvider.java
new file mode 100644
index 0000000..9a6ddb2
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackContentProvider.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.heap;
+
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+import java.util.List;
+
+public class NativeStackContentProvider implements ITreeContentProvider {
+    @Override
+    public Object[] getElements(Object arg0) {
+        return getChildren(arg0);
+    }
+
+    @Override
+    public void dispose() {
+    }
+
+    @Override
+    public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+    }
+
+    @Override
+    public Object[] getChildren(Object parentElement) {
+        if (parentElement instanceof List<?>) {
+            return ((List<?>) parentElement).toArray();
+        }
+
+        return null;
+    }
+
+    @Override
+    public Object getParent(Object element) {
+        return null;
+    }
+
+    @Override
+    public boolean hasChildren(Object element) {
+        return false;
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackLabelProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackLabelProvider.java
new file mode 100644
index 0000000..b7428b9
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackLabelProvider.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.heap;
+
+import com.android.ddmlib.NativeStackCallInfo;
+
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.swt.graphics.Image;
+
+public class NativeStackLabelProvider extends LabelProvider implements ITableLabelProvider {
+    @Override
+    public Image getColumnImage(Object arg0, int arg1) {
+        return null;
+    }
+
+    @Override
+    public String getColumnText(Object element, int index) {
+        if (element instanceof NativeStackCallInfo) {
+            return getResolvedStackTraceColumnText((NativeStackCallInfo) element, index);
+        }
+
+        if (element instanceof Long) {
+            // if the addresses have not been resolved, then just display the
+            // addresses alone
+            return getStackAddressColumnText((Long) element, index);
+        }
+
+        return null;
+    }
+
+    public String getResolvedStackTraceColumnText(NativeStackCallInfo info, int index) {
+        switch (index) {
+        case 0:
+            return String.format("0x%08x", info.getAddress());
+        case 1:
+            return info.getLibraryName();
+        case 2:
+            return info.getMethodName();
+        case 3:
+            return info.getSourceFile();
+        case 4:
+            int l = info.getLineNumber();
+            return l == -1 ? "" : Integer.toString(l);
+        }
+
+        return null;
+    }
+
+    private String getStackAddressColumnText(Long address, int index) {
+        if (index == 0) {
+            return String.format("0x%08x", address);
+        }
+
+        return null;
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeSymbolResolverTask.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeSymbolResolverTask.java
new file mode 100644
index 0000000..1a75c6e
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeSymbolResolverTask.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.heap;
+
+import com.android.ddmlib.NativeAllocationInfo;
+import com.android.ddmlib.NativeLibraryMapInfo;
+import com.android.ddmlib.NativeStackCallInfo;
+import com.android.ddmuilib.DdmUiPreferences;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/**
+ * A symbol resolver task that can resolve a set of addresses to their corresponding
+ * source method name + file name:line number.
+ *
+ * It first identifies the library that contains the address, and then runs addr2line on
+ * the library to get the symbol name + source location.
+ */
+public class NativeSymbolResolverTask implements IRunnableWithProgress {
+    private static final String ADDR2LINE;
+    private static final String DEFAULT_SYMBOLS_FOLDER;
+
+    static {
+        String addr2lineEnv = System.getenv("ANDROID_ADDR2LINE");
+        ADDR2LINE = addr2lineEnv != null ? addr2lineEnv : DdmUiPreferences.getAddr2Line();
+
+        String symbols = System.getenv("ANDROID_SYMBOLS");
+        DEFAULT_SYMBOLS_FOLDER = symbols != null ? symbols : DdmUiPreferences.getSymbolDirectory();
+    }
+
+    private List<NativeAllocationInfo> mCallSites;
+    private List<NativeLibraryMapInfo> mMappedLibraries;
+    private List<String> mSymbolSearchFolders;
+
+    /** All unresolved addresses from all the callsites. */
+    private SortedSet<Long> mUnresolvedAddresses;
+
+    /** Set of all addresses that could were not resolved at the end of the resolution process. */
+    private Set<Long> mUnresolvableAddresses;
+
+    /** Map of library -> [unresolved addresses mapping to this library]. */
+    private Map<NativeLibraryMapInfo, Set<Long>> mUnresolvedAddressesPerLibrary;
+
+    /** Addresses that could not be mapped to a library, should be mostly empty. */
+    private Set<Long> mUnmappedAddresses;
+
+    /** Cache of the resolution for every unresolved address. */
+    private Map<Long, NativeStackCallInfo> mAddressResolution;
+
+    /** List of libraries that were not located on disk. */
+    private Set<String> mNotFoundLibraries;
+    private String mAddr2LineErrorMessage = null;
+
+    public NativeSymbolResolverTask(List<NativeAllocationInfo> callSites,
+                List<NativeLibraryMapInfo> mappedLibraries,
+                String symbolSearchPath) {
+        mCallSites = callSites;
+        mMappedLibraries = mappedLibraries;
+        mSymbolSearchFolders = new ArrayList<String>();
+        mSymbolSearchFolders.add(DEFAULT_SYMBOLS_FOLDER);
+        mSymbolSearchFolders.addAll(Arrays.asList(symbolSearchPath.split(":")));
+
+        mUnresolvedAddresses = new TreeSet<Long>();
+        mUnresolvableAddresses = new HashSet<Long>();
+        mUnresolvedAddressesPerLibrary = new HashMap<NativeLibraryMapInfo, Set<Long>>();
+        mUnmappedAddresses = new HashSet<Long>();
+        mAddressResolution = new HashMap<Long, NativeStackCallInfo>();
+        mNotFoundLibraries = new HashSet<String>();
+    }
+
+    @Override
+    public void run(IProgressMonitor monitor)
+            throws InvocationTargetException, InterruptedException {
+        monitor.beginTask("Resolving symbols", IProgressMonitor.UNKNOWN);
+
+        collectAllUnresolvedAddresses();
+        checkCancellation(monitor);
+
+        mapUnresolvedAddressesToLibrary();
+        checkCancellation(monitor);
+
+        resolveLibraryAddresses(monitor);
+        checkCancellation(monitor);
+
+        resolveCallSites(mCallSites);
+
+        monitor.done();
+    }
+
+    private void collectAllUnresolvedAddresses() {
+        for (NativeAllocationInfo callSite : mCallSites) {
+            mUnresolvedAddresses.addAll(callSite.getStackCallAddresses());
+        }
+    }
+
+    private void mapUnresolvedAddressesToLibrary() {
+        Set<Long> mappedAddresses = new HashSet<Long>();
+
+        for (NativeLibraryMapInfo lib : mMappedLibraries) {
+            SortedSet<Long> addressesInLibrary = mUnresolvedAddresses.subSet(lib.getStartAddress(),
+                    lib.getEndAddress() + 1);
+            if (addressesInLibrary.size() > 0) {
+                mUnresolvedAddressesPerLibrary.put(lib, addressesInLibrary);
+                mappedAddresses.addAll(addressesInLibrary);
+            }
+        }
+
+        // unmapped addresses = unresolved addresses - mapped addresses
+        mUnmappedAddresses.addAll(mUnresolvedAddresses);
+        mUnmappedAddresses.removeAll(mappedAddresses);
+    }
+
+    private void resolveLibraryAddresses(IProgressMonitor monitor) throws InterruptedException {
+        for (NativeLibraryMapInfo lib : mUnresolvedAddressesPerLibrary.keySet()) {
+            String libPath = getLibraryLocation(lib);
+            Set<Long> addressesToResolve = mUnresolvedAddressesPerLibrary.get(lib);
+
+            if (libPath == null) {
+                mNotFoundLibraries.add(lib.getLibraryName());
+                markAddressesNotResolvable(addressesToResolve, lib);
+            } else {
+                monitor.subTask(String.format("Resolving addresses mapped to %s.", libPath));
+                resolveAddresses(lib, libPath, addressesToResolve);
+            }
+
+            checkCancellation(monitor);
+        }
+    }
+
+    private void resolveAddresses(NativeLibraryMapInfo lib, String libPath,
+            Set<Long> addressesToResolve) {
+        Process addr2line = null;
+        try {
+            addr2line = new ProcessBuilder(ADDR2LINE,
+                    "-C",   // demangle
+                    "-f",   // display function names in addition to file:number
+                    "-e", libPath).start();
+        } catch (IOException e) {
+            // Since the library path is known to be valid, the only reason for an exception
+            // is that addr2line was not found. We just save the message in this case.
+            mAddr2LineErrorMessage = e.getMessage();
+            markAddressesNotResolvable(addressesToResolve, lib);
+            return;
+        }
+
+        BufferedReader resultReader = new BufferedReader(new InputStreamReader(
+                                                                    addr2line.getInputStream()));
+        BufferedWriter addressWriter = new BufferedWriter(new OutputStreamWriter(
+                                                                    addr2line.getOutputStream()));
+
+        long libStartAddress = isExecutable(lib) ? 0 : lib.getStartAddress();
+        try {
+            for (Long addr : addressesToResolve) {
+                long offset = addr.longValue() - libStartAddress;
+                addressWriter.write(Long.toHexString(offset));
+                addressWriter.newLine();
+                addressWriter.flush();
+                String method = resultReader.readLine();
+                String sourceFile = resultReader.readLine();
+
+                mAddressResolution.put(addr,
+                        new NativeStackCallInfo(addr.longValue(),
+                                lib.getLibraryName(),
+                                method,
+                                sourceFile));
+            }
+        } catch (IOException e) {
+            // if there is any error, then mark the addresses not already resolved
+            // as unresolvable.
+            for (Long addr : addressesToResolve) {
+                if (mAddressResolution.get(addr) == null) {
+                    markAddressNotResolvable(lib, addr);
+                }
+            }
+        }
+
+        try {
+            resultReader.close();
+            addressWriter.close();
+        } catch (IOException e) {
+            // we can ignore these exceptions
+        }
+
+        addr2line.destroy();
+    }
+
+    private boolean isExecutable(NativeLibraryMapInfo object) {
+        // TODO: Use a tool like readelf or nm to determine whether this object is a library
+        //       or an executable.
+        // For now, we'll just assume that any object present in the bin folder is an executable.
+        String devicePath = object.getLibraryName();
+        return devicePath.contains("/bin/");
+    }
+
+    private void markAddressesNotResolvable(Set<Long> addressesToResolve,
+                                NativeLibraryMapInfo lib) {
+        for (Long addr : addressesToResolve) {
+            markAddressNotResolvable(lib, addr);
+        }
+    }
+
+    private void markAddressNotResolvable(NativeLibraryMapInfo lib, Long addr) {
+        mAddressResolution.put(addr,
+                new NativeStackCallInfo(addr.longValue(),
+                        lib.getLibraryName(),
+                        Long.toHexString(addr),
+                        ""));
+        mUnresolvableAddresses.add(addr);
+    }
+
+    /**
+     * Locate on local disk the debug library w/ symbols corresponding to the
+     * library on the device. It searches for this library in the symbol path.
+     * @return absolute path if found, null otherwise
+     */
+    private String getLibraryLocation(NativeLibraryMapInfo lib) {
+        String pathOnDevice = lib.getLibraryName();
+        String libName = new File(pathOnDevice).getName();
+
+        for (String p : mSymbolSearchFolders) {
+            // try appending the full path on device
+            String fullPath = p + File.separator + pathOnDevice;
+            if (new File(fullPath).exists()) {
+                return fullPath;
+            }
+
+            // try appending basename(library)
+            fullPath = p + File.separator + libName;
+            if (new File(fullPath).exists()) {
+                return fullPath;
+            }
+        }
+
+        return null;
+    }
+
+    private void resolveCallSites(List<NativeAllocationInfo> callSites) {
+        for (NativeAllocationInfo callSite : callSites) {
+            List<NativeStackCallInfo> stackInfo = new ArrayList<NativeStackCallInfo>();
+
+            for (Long addr : callSite.getStackCallAddresses()) {
+                NativeStackCallInfo info = mAddressResolution.get(addr);
+
+                if (info != null) {
+                    stackInfo.add(info);
+                }
+            }
+
+            callSite.setResolvedStackCall(stackInfo);
+        }
+    }
+
+    private void checkCancellation(IProgressMonitor monitor) throws InterruptedException {
+        if (monitor.isCanceled()) {
+            throw new InterruptedException();
+        }
+    }
+
+    public String getAddr2LineErrorMessage() {
+        return mAddr2LineErrorMessage;
+    }
+
+    public Set<Long> getUnmappedAddresses() {
+        return mUnmappedAddresses;
+    }
+
+    public Set<Long> getUnresolvableAddresses() {
+        return mUnresolvableAddresses;
+    }
+
+    public Set<String> getNotFoundLibraries() {
+        return mNotFoundLibraries;
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/CoordinateControls.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/CoordinateControls.java
new file mode 100644
index 0000000..2aef53c
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/CoordinateControls.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2008 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.ddmuilib.location;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Text;
+
+import java.text.DecimalFormat;
+import java.text.ParseException;
+
+/**
+ * Encapsulation of controls handling a location coordinate in decimal and sexagesimal.
+ * <p/>This handle the conversion between both modes automatically by using a {@link ModifyListener}
+ * on all the {@link Text} widgets.
+ * <p/>To get/set the coordinate, use {@link #setValue(double)} and {@link #getValue()} (preceded by
+ * a call to {@link #isValueValid()})
+ */
+public final class CoordinateControls {
+    private double mValue;
+    private boolean mValueValidity = false;
+    private Text mDecimalText;
+    private Text mSexagesimalDegreeText;
+    private Text mSexagesimalMinuteText;
+    private Text mSexagesimalSecondText;
+    private final DecimalFormat mDecimalFormat = new DecimalFormat();
+
+    /** Internal flag to prevent {@link ModifyEvent} to be sent when {@link Text#setText(String)}
+     * is called. This is an int instead of a boolean to act as a counter. */
+    private int mManualTextChange = 0;
+
+    /**
+     * ModifyListener for the 3 {@link Text} controls of the sexagesimal mode.
+     */
+    private ModifyListener mSexagesimalListener = new ModifyListener() {
+        @Override
+        public void modifyText(ModifyEvent event) {
+            if (mManualTextChange > 0) {
+                return;
+            }
+            try {
+                mValue = getValueFromSexagesimalControls();
+                setValueIntoDecimalControl(mValue);
+                mValueValidity = true;
+            } catch (ParseException e) {
+                // wrong format empty the decimal controls.
+                mValueValidity = false;
+                resetDecimalControls();
+            }
+        }
+    };
+
+    /**
+     * Creates the {@link Text} control for the decimal display of the coordinate.
+     * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}.
+     * @param parent The {@link Composite} parent of the control.
+     */
+    public void createDecimalText(Composite parent) {
+        mDecimalText = createTextControl(parent, "-199.999999", new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent event) {
+                if (mManualTextChange > 0) {
+                    return;
+                }
+                try {
+                    mValue = mDecimalFormat.parse(mDecimalText.getText()).doubleValue();
+                    setValueIntoSexagesimalControl(mValue);
+                    mValueValidity = true;
+                } catch (ParseException e) {
+                    // wrong format empty the sexagesimal controls.
+                    mValueValidity = false;
+                    resetSexagesimalControls();
+                }
+            }
+        });
+    }
+
+    /**
+     * Creates the {@link Text} control for the "degree" display of the coordinate in sexagesimal
+     * mode.
+     * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}.
+     * @param parent The {@link Composite} parent of the control.
+     */
+    public void createSexagesimalDegreeText(Composite parent) {
+        mSexagesimalDegreeText = createTextControl(parent, "-199", mSexagesimalListener); //$NON-NLS-1$
+    }
+
+    /**
+     * Creates the {@link Text} control for the "minute" display of the coordinate in sexagesimal
+     * mode.
+     * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}.
+     * @param parent The {@link Composite} parent of the control.
+     */
+    public void createSexagesimalMinuteText(Composite parent) {
+        mSexagesimalMinuteText = createTextControl(parent, "99", mSexagesimalListener); //$NON-NLS-1$
+    }
+
+    /**
+     * Creates the {@link Text} control for the "second" display of the coordinate in sexagesimal
+     * mode.
+     * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}.
+     * @param parent The {@link Composite} parent of the control.
+     */
+    public void createSexagesimalSecondText(Composite parent) {
+        mSexagesimalSecondText = createTextControl(parent, "99.999", mSexagesimalListener); //$NON-NLS-1$
+    }
+
+    /**
+     * Sets the coordinate into the {@link Text} controls.
+     * @param value the coordinate value to set.
+     */
+    public void setValue(double value) {
+        mValue = value;
+        mValueValidity = true;
+        setValueIntoDecimalControl(value);
+        setValueIntoSexagesimalControl(value);
+    }
+
+    /**
+     * Returns whether the value in the control(s) is valid.
+     */
+    public boolean isValueValid() {
+        return mValueValidity;
+    }
+
+    /**
+     * Returns the current value set in the control(s).
+     * <p/>This value can be erroneous, and a check with {@link #isValueValid()} should be performed
+     * before any call to this method.
+     */
+    public double getValue() {
+        return mValue;
+    }
+
+    /**
+     * Enables or disables all the {@link Text} controls.
+     * @param enabled the enabled state.
+     */
+    public void setEnabled(boolean enabled) {
+        mDecimalText.setEnabled(enabled);
+        mSexagesimalDegreeText.setEnabled(enabled);
+        mSexagesimalMinuteText.setEnabled(enabled);
+        mSexagesimalSecondText.setEnabled(enabled);
+    }
+
+    private void resetDecimalControls() {
+        mManualTextChange++;
+        mDecimalText.setText(""); //$NON-NLS-1$
+        mManualTextChange--;
+    }
+
+    private void resetSexagesimalControls() {
+        mManualTextChange++;
+        mSexagesimalDegreeText.setText(""); //$NON-NLS-1$
+        mSexagesimalMinuteText.setText(""); //$NON-NLS-1$
+        mSexagesimalSecondText.setText(""); //$NON-NLS-1$
+        mManualTextChange--;
+    }
+
+    /**
+     * Creates a {@link Text} with a given parent, default string and a {@link ModifyListener}
+     * @param parent the parent {@link Composite}.
+     * @param defaultString the default string to be used to compute the {@link Text} control
+     * size hint.
+     * @param listener the {@link ModifyListener} to be called when the {@link Text} control is
+     * modified.
+     */
+    private Text createTextControl(Composite parent, String defaultString,
+            ModifyListener listener) {
+        // create the control
+        Text text = new Text(parent, SWT.BORDER | SWT.LEFT | SWT.SINGLE);
+
+        // add the standard listener to it.
+        text.addModifyListener(listener);
+
+        // compute its size/
+        mManualTextChange++;
+        text.setText(defaultString);
+        text.pack();
+        Point size = text.computeSize(SWT.DEFAULT, SWT.DEFAULT);
+        text.setText(""); //$NON-NLS-1$
+        mManualTextChange--;
+
+        GridData gridData = new GridData();
+        gridData.widthHint = size.x;
+        text.setLayoutData(gridData);
+
+        return text;
+    }
+
+    private double getValueFromSexagesimalControls() throws ParseException {
+        double degrees = mDecimalFormat.parse(mSexagesimalDegreeText.getText()).doubleValue();
+        double minutes = mDecimalFormat.parse(mSexagesimalMinuteText.getText()).doubleValue();
+        double seconds = mDecimalFormat.parse(mSexagesimalSecondText.getText()).doubleValue();
+
+        boolean isPositive = (degrees >= 0.);
+        degrees = Math.abs(degrees);
+
+        double value = degrees + minutes / 60. + seconds / 3600.;
+        return isPositive ? value : - value;
+    }
+
+    private void setValueIntoDecimalControl(double value) {
+        mManualTextChange++;
+        mDecimalText.setText(String.format("%.6f", value));
+        mManualTextChange--;
+    }
+
+    private void setValueIntoSexagesimalControl(double value) {
+        // get the sign and make the number positive no matter what.
+        boolean isPositive = (value >= 0.);
+        value = Math.abs(value);
+
+        // get the degree
+        double degrees = Math.floor(value);
+
+        // get the minutes
+        double minutes = Math.floor((value - degrees) * 60.);
+
+        // get the seconds.
+        double seconds = (value - degrees) * 3600. - minutes * 60.;
+
+        mManualTextChange++;
+        mSexagesimalDegreeText.setText(
+                Integer.toString(isPositive ? (int)degrees : (int)- degrees));
+        mSexagesimalMinuteText.setText(Integer.toString((int)minutes));
+        mSexagesimalSecondText.setText(String.format("%.3f", seconds)); //$NON-NLS-1$
+        mManualTextChange--;
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/GpxParser.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/GpxParser.java
new file mode 100644
index 0000000..a30337a
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/GpxParser.java
@@ -0,0 +1,373 @@
+/*
+ * Copyright (C) 2008 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.ddmuilib.location;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+import java.util.TimeZone;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+/**
+ * A very basic GPX parser to meet the need of the emulator control panel.
+ * <p/>
+ * It parses basic waypoint information, and tracks (merging segments).
+ */
+public class GpxParser {
+    
+    private final static String NS_GPX = "http://www.topografix.com/GPX/1/1";  //$NON-NLS-1$
+        
+    private final static String NODE_WAYPOINT = "wpt"; //$NON-NLS-1$
+    private final static String NODE_TRACK = "trk"; //$NON-NLS-1$
+    private final static String NODE_TRACK_SEGMENT = "trkseg"; //$NON-NLS-1$
+    private final static String NODE_TRACK_POINT = "trkpt"; //$NON-NLS-1$
+    private final static String NODE_NAME = "name"; //$NON-NLS-1$
+    private final static String NODE_TIME = "time"; //$NON-NLS-1$
+    private final static String NODE_ELEVATION = "ele"; //$NON-NLS-1$
+    private final static String NODE_DESCRIPTION = "desc"; //$NON-NLS-1$
+    private final static String ATTR_LONGITUDE = "lon"; //$NON-NLS-1$
+    private final static String ATTR_LATITUDE = "lat"; //$NON-NLS-1$
+    
+    private static SAXParserFactory sParserFactory;
+    
+    static {
+        sParserFactory = SAXParserFactory.newInstance();
+        sParserFactory.setNamespaceAware(true);
+    }
+
+    private String mFileName;
+
+    private GpxHandler mHandler;
+    
+    /** Pattern to parse time with optional sub-second precision, and optional
+     * Z indicating the time is in UTC. */
+    private final static Pattern ISO8601_TIME =
+        Pattern.compile("(\\d{4})-(\\d\\d)-(\\d\\d)T(\\d\\d):(\\d\\d):(\\d\\d)(?:(\\.\\d+))?(Z)?"); //$NON-NLS-1$
+    
+    /**
+     * Handler for the SAX parser.
+     */
+    private static class GpxHandler extends DefaultHandler {
+        // --------- parsed data --------- 
+        List<WayPoint> mWayPoints;
+        List<Track> mTrackList;
+        
+        // --------- state for parsing --------- 
+        Track mCurrentTrack;
+        TrackPoint mCurrentTrackPoint;
+        WayPoint mCurrentWayPoint;
+        final StringBuilder mStringAccumulator = new StringBuilder();
+        
+        boolean mSuccess = true;
+
+        @Override
+        public void startElement(String uri, String localName, String name, Attributes attributes)
+                throws SAXException {
+            // we only care about the standard GPX nodes.
+            try {
+                if (NS_GPX.equals(uri)) {
+                    if (NODE_WAYPOINT.equals(localName)) {
+                        if (mWayPoints == null) {
+                            mWayPoints = new ArrayList<WayPoint>();
+                        }
+                        
+                        mWayPoints.add(mCurrentWayPoint = new WayPoint());
+                        handleLocation(mCurrentWayPoint, attributes);
+                    } else if (NODE_TRACK.equals(localName)) {
+                        if (mTrackList == null) {
+                            mTrackList = new ArrayList<Track>();
+                        }
+                        
+                        mTrackList.add(mCurrentTrack = new Track());
+                    } else if (NODE_TRACK_SEGMENT.equals(localName)) {
+                        // for now we do nothing here. This will merge all the segments into
+                        // a single TrackPoint list in the Track.
+                    } else if (NODE_TRACK_POINT.equals(localName)) {
+                        if (mCurrentTrack != null) {
+                            mCurrentTrack.addPoint(mCurrentTrackPoint = new TrackPoint());
+                            handleLocation(mCurrentTrackPoint, attributes);
+                        }
+                    }
+                }
+            } finally {
+                // no matter the node, we empty the StringBuilder accumulator when we start
+                // a new node.
+                mStringAccumulator.setLength(0);
+            }
+        }
+
+        /**
+         * Processes new characters for the node content. The characters are simply stored,
+         * and will be processed when {@link #endElement(String, String, String)} is called.
+         */
+        @Override
+        public void characters(char[] ch, int start, int length) throws SAXException {
+            mStringAccumulator.append(ch, start, length);
+        }
+        
+        @Override
+        public void endElement(String uri, String localName, String name) throws SAXException {
+            if (NS_GPX.equals(uri)) {
+                if (NODE_WAYPOINT.equals(localName)) {
+                    mCurrentWayPoint = null;
+                } else if (NODE_TRACK.equals(localName)) {
+                    mCurrentTrack = null;
+                } else if (NODE_TRACK_POINT.equals(localName)) {
+                    mCurrentTrackPoint = null;
+                } else if (NODE_NAME.equals(localName)) {
+                    if (mCurrentTrack != null) {
+                        mCurrentTrack.setName(mStringAccumulator.toString());
+                    } else if (mCurrentWayPoint != null) {
+                        mCurrentWayPoint.setName(mStringAccumulator.toString());
+                    }
+                } else if (NODE_TIME.equals(localName)) {
+                    if (mCurrentTrackPoint != null) {
+                        mCurrentTrackPoint.setTime(computeTime(mStringAccumulator.toString()));
+                    }
+                } else if (NODE_ELEVATION.equals(localName)) {
+                    if (mCurrentTrackPoint != null) {
+                        mCurrentTrackPoint.setElevation(
+                                Double.parseDouble(mStringAccumulator.toString()));
+                    } else if (mCurrentWayPoint != null) {
+                        mCurrentWayPoint.setElevation(
+                                Double.parseDouble(mStringAccumulator.toString()));
+                    }
+                } else if (NODE_DESCRIPTION.equals(localName)) {
+                    if (mCurrentWayPoint != null) {
+                        mCurrentWayPoint.setDescription(mStringAccumulator.toString());
+                    }
+                }
+            }
+        }
+
+        @Override
+        public void error(SAXParseException e) throws SAXException {
+            mSuccess = false;
+        }
+
+        @Override
+        public void fatalError(SAXParseException e) throws SAXException {
+            mSuccess = false;
+        }
+        
+        /**
+         * Converts the string description of the time into milliseconds since epoch.
+         * @param timeString the string data.
+         * @return date in milliseconds.
+         */
+        private long computeTime(String timeString) {
+            // Time looks like: 2008-04-05T19:24:50Z
+            Matcher m = ISO8601_TIME.matcher(timeString);
+            if (m.matches()) {
+                // get the various elements and reconstruct time as a long.
+                try {
+                    int year = Integer.parseInt(m.group(1));
+                    int month = Integer.parseInt(m.group(2));
+                    int date = Integer.parseInt(m.group(3));
+                    int hourOfDay = Integer.parseInt(m.group(4));
+                    int minute = Integer.parseInt(m.group(5));
+                    int second = Integer.parseInt(m.group(6));
+                    
+                    // handle the optional parameters.
+                    int milliseconds = 0;
+
+                    String subSecondGroup = m.group(7);
+                    if (subSecondGroup != null) {
+                        milliseconds = (int)(1000 * Double.parseDouble(subSecondGroup));
+                    }
+                    
+                    boolean utcTime = m.group(8) != null;
+
+                    // now we convert into milliseconds since epoch.
+                    Calendar c;
+                    if (utcTime) {
+                        c = Calendar.getInstance(TimeZone.getTimeZone("GMT")); //$NON-NLS-1$
+                    } else {
+                        c = Calendar.getInstance();
+                    }
+                    
+                    c.set(year, month, date, hourOfDay, minute, second);
+                    
+                    return c.getTimeInMillis() + milliseconds;
+                } catch (NumberFormatException e) {
+                    // format is invalid, we'll return -1 below.
+                }
+                
+            }
+
+            // invalid time!
+            return -1;
+        }
+        
+        /**
+         * Handles the location attributes and store them into a {@link LocationPoint}.
+         * @param locationNode the {@link LocationPoint} to receive the location data.
+         * @param attributes the attributes from the XML node.
+         */
+        private void handleLocation(LocationPoint locationNode, Attributes attributes) {
+            try {
+                double longitude = Double.parseDouble(attributes.getValue(ATTR_LONGITUDE));
+                double latitude = Double.parseDouble(attributes.getValue(ATTR_LATITUDE));
+                
+                locationNode.setLocation(longitude, latitude);
+            } catch (NumberFormatException e) {
+                // wrong data, do nothing.
+            }
+        }
+
+        WayPoint[] getWayPoints() {
+            if (mWayPoints != null) {
+                return mWayPoints.toArray(new WayPoint[mWayPoints.size()]);
+            }
+
+            return null;
+        }
+
+        Track[] getTracks() {
+            if (mTrackList != null) {
+                return mTrackList.toArray(new Track[mTrackList.size()]);
+            }
+
+            return null;
+        }
+        
+        boolean getSuccess() {
+            return mSuccess;
+        }
+    }
+
+    /**
+     * A GPS track.
+     * <p/>A track is composed of a list of {@link TrackPoint} and optional name and comment.
+     */
+    public final static class Track {
+        private String mName;
+        private String mComment;
+        private List<TrackPoint> mPoints = new ArrayList<TrackPoint>();
+
+        void setName(String name) {
+            mName = name;
+        }
+        
+        public String getName() {
+            return mName;
+        }
+        
+        void setComment(String comment) {
+            mComment = comment;
+        }
+        
+        public String getComment() {
+            return mComment;
+        }
+        
+        void addPoint(TrackPoint trackPoint) {
+            mPoints.add(trackPoint);
+        }
+        
+        public TrackPoint[] getPoints() {
+            return mPoints.toArray(new TrackPoint[mPoints.size()]);
+        }
+        
+        public long getFirstPointTime() {
+            if (mPoints.size() > 0) {
+                return mPoints.get(0).getTime();
+            }
+            
+            return -1;
+        }
+
+        public long getLastPointTime() {
+            if (mPoints.size() > 0) {
+                return mPoints.get(mPoints.size()-1).getTime();
+            }
+            
+            return -1;
+        }
+        
+        public int getPointCount() {
+            return mPoints.size();
+        }
+    }
+    
+    /**
+     * Creates a new GPX parser for a file specified by its full path.
+     * @param fileName The full path of the GPX file to parse.
+     */
+    public GpxParser(String fileName) {
+        mFileName = fileName;
+    }
+
+    /**
+     * Parses the GPX file.
+     * @return <code>true</code> if success.
+     */
+    public boolean parse() {
+        try {
+            SAXParser parser = sParserFactory.newSAXParser();
+
+            mHandler = new GpxHandler();
+
+            parser.parse(new InputSource(new FileReader(mFileName)), mHandler);
+            
+            return mHandler.getSuccess();
+        } catch (ParserConfigurationException e) {
+        } catch (SAXException e) {
+        } catch (IOException e) {
+        } finally {
+        }
+
+        return false;
+    }
+    
+    /**
+     * Returns the parsed {@link WayPoint} objects, or <code>null</code> if none were found (or
+     * if the parsing failed.
+     */
+    public WayPoint[] getWayPoints() {
+        if (mHandler != null) {
+            return mHandler.getWayPoints();
+        }
+        
+        return null;
+    }
+    
+    /**
+     * Returns the parsed {@link Track} objects, or <code>null</code> if none were found (or
+     * if the parsing failed.
+     */
+    public Track[] getTracks() {
+        if (mHandler != null) {
+            return mHandler.getTracks();
+        }
+        
+        return null;
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/KmlParser.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/KmlParser.java
new file mode 100644
index 0000000..af485ac
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/KmlParser.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2008 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.ddmuilib.location;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+/**
+ * A very basic KML parser to meet the need of the emulator control panel.
+ * <p/>
+ * It parses basic Placemark information.
+ */
+public class KmlParser {
+    
+    private final static String NS_KML_2 = "http://earth.google.com/kml/2.";  //$NON-NLS-1$
+        
+    private final static String NODE_PLACEMARK = "Placemark"; //$NON-NLS-1$
+    private final static String NODE_NAME = "name"; //$NON-NLS-1$
+    private final static String NODE_COORDINATES = "coordinates"; //$NON-NLS-1$
+    
+    private final static Pattern sLocationPattern = Pattern.compile("([^,]+),([^,]+)(?:,([^,]+))?");
+    
+    private static SAXParserFactory sParserFactory;
+    
+    static {
+        sParserFactory = SAXParserFactory.newInstance();
+        sParserFactory.setNamespaceAware(true);
+    }
+
+    private String mFileName;
+
+    private KmlHandler mHandler;
+    
+    /**
+     * Handler for the SAX parser.
+     */
+    private static class KmlHandler extends DefaultHandler {
+        // --------- parsed data --------- 
+        List<WayPoint> mWayPoints;
+        
+        // --------- state for parsing --------- 
+        WayPoint mCurrentWayPoint;
+        final StringBuilder mStringAccumulator = new StringBuilder();
+
+        boolean mSuccess = true;
+
+        @Override
+        public void startElement(String uri, String localName, String name, Attributes attributes)
+                throws SAXException {
+            // we only care about the standard GPX nodes.
+            try {
+                if (uri.startsWith(NS_KML_2)) {
+                    if (NODE_PLACEMARK.equals(localName)) {
+                        if (mWayPoints == null) {
+                            mWayPoints = new ArrayList<WayPoint>();
+                        }
+                        
+                        mWayPoints.add(mCurrentWayPoint = new WayPoint());
+                    }
+                }
+            } finally {
+                // no matter the node, we empty the StringBuilder accumulator when we start
+                // a new node.
+                mStringAccumulator.setLength(0);
+            }
+        }
+
+        /**
+         * Processes new characters for the node content. The characters are simply stored,
+         * and will be processed when {@link #endElement(String, String, String)} is called.
+         */
+        @Override
+        public void characters(char[] ch, int start, int length) throws SAXException {
+            mStringAccumulator.append(ch, start, length);
+        }
+        
+        @Override
+        public void endElement(String uri, String localName, String name) throws SAXException {
+            if (uri.startsWith(NS_KML_2)) {
+                if (NODE_PLACEMARK.equals(localName)) {
+                    mCurrentWayPoint = null;
+                } else if (NODE_NAME.equals(localName)) {
+                    if (mCurrentWayPoint != null) {
+                        mCurrentWayPoint.setName(mStringAccumulator.toString());
+                    }
+                } else if (NODE_COORDINATES.equals(localName)) {
+                    if (mCurrentWayPoint != null) {
+                        parseLocation(mCurrentWayPoint, mStringAccumulator.toString());
+                    }
+                }
+            }
+        }
+
+        @Override
+        public void error(SAXParseException e) throws SAXException {
+            mSuccess = false;
+        }
+
+        @Override
+        public void fatalError(SAXParseException e) throws SAXException {
+            mSuccess = false;
+        }
+        
+        /**
+         * Parses the location string and store the information into a {@link LocationPoint}.
+         * @param locationNode the {@link LocationPoint} to receive the location data.
+         * @param location The string containing the location info.
+         */
+        private void parseLocation(LocationPoint locationNode, String location) {
+            Matcher m = sLocationPattern.matcher(location);
+            if (m.matches()) {
+                try {
+                    double longitude = Double.parseDouble(m.group(1));
+                    double latitude = Double.parseDouble(m.group(2));
+                    
+                    locationNode.setLocation(longitude, latitude);
+                    
+                    if (m.groupCount() == 3) {
+                        // looks like we have elevation data.
+                        locationNode.setElevation(Double.parseDouble(m.group(3)));
+                    }
+                } catch (NumberFormatException e) {
+                    // wrong data, do nothing.
+                }
+            }
+        }
+        
+        WayPoint[] getWayPoints() {
+            if (mWayPoints != null) {
+                return mWayPoints.toArray(new WayPoint[mWayPoints.size()]);
+            }
+
+            return null;
+        }
+
+        boolean getSuccess() {
+            return mSuccess;
+        }
+    }
+
+    /**
+     * Creates a new GPX parser for a file specified by its full path.
+     * @param fileName The full path of the GPX file to parse.
+     */
+    public KmlParser(String fileName) {
+        mFileName = fileName;
+    }
+
+    /**
+     * Parses the GPX file.
+     * @return <code>true</code> if success.
+     */
+    public boolean parse() {
+        try {
+            SAXParser parser = sParserFactory.newSAXParser();
+
+            mHandler = new KmlHandler();
+
+            parser.parse(new InputSource(new FileReader(mFileName)), mHandler);
+            
+            return mHandler.getSuccess();
+        } catch (ParserConfigurationException e) {
+        } catch (SAXException e) {
+        } catch (IOException e) {
+        } finally {
+        }
+
+        return false;
+    }
+    
+    /**
+     * Returns the parsed {@link WayPoint} objects, or <code>null</code> if none were found (or
+     * if the parsing failed.
+     */
+    public WayPoint[] getWayPoints() {
+        if (mHandler != null) {
+            return mHandler.getWayPoints();
+        }
+        
+        return null;
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/LocationPoint.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/LocationPoint.java
new file mode 100644
index 0000000..dbb8f41
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/LocationPoint.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2008 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.ddmuilib.location;
+
+/**
+ * Base class for Location aware points.
+ */
+class LocationPoint {
+    private double mLongitude;
+    private double mLatitude;
+    private boolean mHasElevation = false;
+    private double mElevation;
+
+    final void setLocation(double longitude, double latitude) {
+        mLongitude = longitude;
+        mLatitude = latitude;
+    }
+    
+    public final double getLongitude() {
+        return mLongitude;
+    }
+    
+    public final double getLatitude() {
+        return mLatitude;
+    }
+
+    final void setElevation(double elevation) {
+        mElevation = elevation;
+        mHasElevation = true;
+    }
+    
+    public final boolean hasElevation() {
+        return mHasElevation;
+    }
+    
+    public final double getElevation() {
+        return mElevation;
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackContentProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackContentProvider.java
new file mode 100644
index 0000000..da21920
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackContentProvider.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2008 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.ddmuilib.location;
+
+import com.android.ddmuilib.location.GpxParser.Track;
+
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+/**
+ * Content provider to display {@link Track} objects in a Table.
+ * <p/>The expected type for the input is {@link Track}<code>[]</code>.
+ */
+public class TrackContentProvider implements IStructuredContentProvider {
+
+    @Override
+    public Object[] getElements(Object inputElement) {
+        if (inputElement instanceof Track[]) {
+            return (Track[])inputElement;
+        }
+
+        return new Object[0];
+    }
+
+    @Override
+    public void dispose() {
+        // pass
+    }
+
+    @Override
+    public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+        // pass
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackLabelProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackLabelProvider.java
new file mode 100644
index 0000000..50acb53
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackLabelProvider.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2008 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.ddmuilib.location;
+
+import com.android.ddmuilib.location.GpxParser.Track;
+
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Table;
+
+import java.util.Date;
+
+/**
+ * Label Provider for {@link Table} objects displaying {@link Track} objects.
+ */
+public class TrackLabelProvider implements ITableLabelProvider {
+
+    @Override
+    public Image getColumnImage(Object element, int columnIndex) {
+        return null;
+    }
+
+    @Override
+    public String getColumnText(Object element, int columnIndex) {
+        if (element instanceof Track) {
+            Track track = (Track)element;
+            switch (columnIndex) {
+                case 0:
+                    return track.getName();
+                case 1:
+                    return Integer.toString(track.getPointCount());
+                case 2:
+                    long time = track.getFirstPointTime();
+                    if (time != -1) {
+                        return new Date(time).toString();
+                    }
+                    break;
+                case 3:
+                    time = track.getLastPointTime();
+                    if (time != -1) {
+                        return new Date(time).toString();
+                    }
+                    break;
+                case 4:
+                    return track.getComment();
+            }
+        }
+
+        return null;
+    }
+
+    @Override
+    public void addListener(ILabelProviderListener listener) {
+        // pass
+    }
+
+    @Override
+    public void dispose() {
+        // pass
+    }
+
+    @Override
+    public boolean isLabelProperty(Object element, String property) {
+        // pass
+        return false;
+    }
+
+    @Override
+    public void removeListener(ILabelProviderListener listener) {
+        // pass
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackPoint.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackPoint.java
new file mode 100644
index 0000000..527f4bf
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackPoint.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2008 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.ddmuilib.location;
+
+
+/**
+ * A Track Point.
+ * <p/>A track point is a point in time and space.
+ */
+public class TrackPoint extends LocationPoint {
+    private long mTime;
+
+    void setTime(long time) {
+        mTime = time;
+    }
+    
+    public long getTime() {
+        return mTime;
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPoint.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPoint.java
new file mode 100644
index 0000000..32880bd
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPoint.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2008 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.ddmuilib.location;
+
+/**
+ * A GPS/KML way point.
+ * <p/>A waypoint is a user specified location, with a name and an optional description.
+ */
+public final class WayPoint extends LocationPoint {
+    private String mName;
+    private String mDescription;
+
+    void setName(String name) {
+        mName = name;
+    }
+    
+    public String getName() {
+        return mName;
+    }
+
+    void setDescription(String description) {
+        mDescription = description;
+    }
+
+    public String getDescription() {
+        return mDescription;
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointContentProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointContentProvider.java
new file mode 100644
index 0000000..1b7fe15
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointContentProvider.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2008 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.ddmuilib.location;
+
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+/**
+ * Content provider to display {@link WayPoint} objects in a Table.
+ * <p/>The expected type for the input is {@link WayPoint}<code>[]</code>.
+ */
+public class WayPointContentProvider implements IStructuredContentProvider {
+
+    @Override
+    public Object[] getElements(Object inputElement) {
+        if (inputElement instanceof WayPoint[]) {
+            return (WayPoint[])inputElement;
+        }
+
+        return new Object[0];
+    }
+
+    @Override
+    public void dispose() {
+        // pass
+    }
+
+    @Override
+    public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+        // pass
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointLabelProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointLabelProvider.java
new file mode 100644
index 0000000..9f642f1
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointLabelProvider.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2008 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.ddmuilib.location;
+
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Table;
+
+/**
+ * Label Provider for {@link Table} objects displaying {@link WayPoint} objects.
+ */
+public class WayPointLabelProvider implements ITableLabelProvider {
+
+    @Override
+    public Image getColumnImage(Object element, int columnIndex) {
+        return null;
+    }
+
+    @Override
+    public String getColumnText(Object element, int columnIndex) {
+        if (element instanceof WayPoint) {
+            WayPoint wayPoint = (WayPoint)element;
+            switch (columnIndex) {
+                case 0:
+                    return wayPoint.getName();
+                case 1:
+                    return String.format("%.6f", wayPoint.getLongitude());
+                case 2:
+                    return String.format("%.6f", wayPoint.getLatitude());
+                case 3:
+                    if (wayPoint.hasElevation()) {
+                        return String.format("%.1f", wayPoint.getElevation());
+                    } else {
+                        return "-";
+                    }
+                case 4:
+                    return wayPoint.getDescription();
+            }
+        }
+
+        return null;
+    }
+
+    @Override
+    public void addListener(ILabelProviderListener listener) {
+        // pass
+    }
+
+    @Override
+    public void dispose() {
+        // pass
+    }
+
+    @Override
+    public boolean isLabelProperty(Object element, String property) {
+        // pass
+        return false;
+    }
+
+    @Override
+    public void removeListener(ILabelProviderListener listener) {
+        // pass
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/BugReportImporter.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/BugReportImporter.java
new file mode 100644
index 0000000..da41e70
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/BugReportImporter.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2008 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.ddmuilib.log.event;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+
+public class BugReportImporter {
+
+    private final static String TAG_HEADER = "------ EVENT LOG TAGS ------";
+    private final static String LOG_HEADER = "------ EVENT LOG ------";
+    private final static String HEADER_TAG = "------";
+
+    private String[] mTags;
+    private String[] mLog;
+
+    public BugReportImporter(String filePath) throws FileNotFoundException {
+        BufferedReader reader = new BufferedReader(
+                new InputStreamReader(new FileInputStream(filePath)));
+
+        try {
+            String line;
+            while ((line = reader.readLine()) != null) {
+                if (TAG_HEADER.equals(line)) {
+                    readTags(reader);
+                    return;
+                }
+            }
+        } catch (IOException e) {
+        } finally {
+            if (reader != null) {
+                try {
+                    reader.close();
+                } catch (IOException ignore) {
+                }
+            }
+        }
+    }
+
+    public String[] getTags() {
+        return mTags;
+    }
+
+    public String[] getLog() {
+        return mLog;
+    }
+
+    private void readTags(BufferedReader reader) throws IOException {
+        String line;
+
+        ArrayList<String> content = new ArrayList<String>();
+        while ((line = reader.readLine()) != null) {
+            if (LOG_HEADER.equals(line)) {
+                mTags = content.toArray(new String[content.size()]);
+                readLog(reader);
+                return;
+            } else {
+                content.add(line);
+            }
+        }
+    }
+
+    private void readLog(BufferedReader reader) throws IOException {
+        String line;
+
+        ArrayList<String> content = new ArrayList<String>();
+        while ((line = reader.readLine()) != null) {
+            if (line.startsWith(HEADER_TAG) == false) {
+                content.add(line);
+            } else {
+                break;
+            }
+        }
+
+        mLog = content.toArray(new String[content.size()]);
+    }
+
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayFilteredLog.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayFilteredLog.java
new file mode 100644
index 0000000..473387a
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayFilteredLog.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2008 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.ddmuilib.log.event;
+
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventLogParser;
+
+import java.util.ArrayList;
+
+public class DisplayFilteredLog extends DisplayLog {
+
+    public DisplayFilteredLog(String name) {
+        super(name);
+    }
+
+    /**
+     * Adds event to the display.
+     */
+    @Override
+    void newEvent(EventContainer event, EventLogParser logParser) {
+        ArrayList<ValueDisplayDescriptor> valueDescriptors =
+                new ArrayList<ValueDisplayDescriptor>();
+
+        ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors =
+                new ArrayList<OccurrenceDisplayDescriptor>();
+
+        if (filterEvent(event, valueDescriptors, occurrenceDescriptors)) {
+            addToLog(event, logParser, valueDescriptors, occurrenceDescriptors);
+        }
+    }
+
+    /**
+     * Gets display type
+     *
+     * @return display type as an integer
+     */
+    @Override
+    int getDisplayType() {
+        return DISPLAY_TYPE_FILTERED_LOG;
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayGraph.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayGraph.java
new file mode 100644
index 0000000..0cffd7e
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayGraph.java
@@ -0,0 +1,422 @@
+/*
+ * Copyright (C) 2008 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.ddmuilib.log.event;
+
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventLogParser;
+import com.android.ddmlib.log.EventValueDescription;
+import com.android.ddmlib.log.InvalidTypeException;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.jfree.chart.axis.AxisLocation;
+import org.jfree.chart.axis.NumberAxis;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.AbstractXYItemRenderer;
+import org.jfree.chart.renderer.xy.XYAreaRenderer;
+import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
+import org.jfree.data.time.Millisecond;
+import org.jfree.data.time.TimeSeries;
+import org.jfree.data.time.TimeSeriesCollection;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+public class DisplayGraph extends EventDisplay {
+
+    public DisplayGraph(String name) {
+        super(name);
+    }
+
+    /**
+     * Resets the display.
+     */
+    @Override
+    void resetUI() {
+        Collection<TimeSeriesCollection> datasets = mValueTypeDataSetMap.values();
+        for (TimeSeriesCollection dataset : datasets) {
+            dataset.removeAllSeries();
+        }
+        if (mOccurrenceDataSet != null) {
+            mOccurrenceDataSet.removeAllSeries();
+        }
+        mValueDescriptorSeriesMap.clear();
+        mOcurrenceDescriptorSeriesMap.clear();
+    }
+
+    /**
+     * Creates the UI for the event display.
+     * @param parent the parent composite.
+     * @param logParser the current log parser.
+     * @return the created control (which may have children).
+     */
+    @Override
+    public Control createComposite(final Composite parent, EventLogParser logParser,
+            final ILogColumnListener listener) {
+        String title = getChartTitle(logParser);
+        return createCompositeChart(parent, logParser, title);
+    }
+
+    /**
+     * Adds event to the display.
+     */
+    @Override
+    void newEvent(EventContainer event, EventLogParser logParser) {
+        ArrayList<ValueDisplayDescriptor> valueDescriptors =
+                new ArrayList<ValueDisplayDescriptor>();
+
+        ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors =
+                new ArrayList<OccurrenceDisplayDescriptor>();
+
+        if (filterEvent(event, valueDescriptors, occurrenceDescriptors)) {
+            updateChart(event, logParser, valueDescriptors, occurrenceDescriptors);
+        }
+    }
+
+     /**
+     * Updates the chart with the {@link EventContainer} by adding the values/occurrences defined
+     * by the {@link ValueDisplayDescriptor} and {@link OccurrenceDisplayDescriptor} objects from
+     * the two lists.
+     * <p/>This method is only called when at least one of the descriptor list is non empty.
+     * @param event
+     * @param logParser
+     * @param valueDescriptors
+     * @param occurrenceDescriptors
+     */
+    private void updateChart(EventContainer event, EventLogParser logParser,
+            ArrayList<ValueDisplayDescriptor> valueDescriptors,
+            ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors) {
+        Map<Integer, String> tagMap = logParser.getTagMap();
+
+        Millisecond millisecondTime = null;
+        long msec = -1;
+
+        // If the event container is a cpu container (tag == 2721), and there is no descriptor
+        // for the total CPU load, then we do accumulate all the values.
+        boolean accumulateValues = false;
+        double accumulatedValue = 0;
+
+        if (event.mTag == 2721) {
+            accumulateValues = true;
+            for (ValueDisplayDescriptor descriptor : valueDescriptors) {
+                accumulateValues &= (descriptor.valueIndex != 0);
+            }
+        }
+
+        for (ValueDisplayDescriptor descriptor : valueDescriptors) {
+            try {
+                // get the hashmap for this descriptor
+                HashMap<Integer, TimeSeries> map = mValueDescriptorSeriesMap.get(descriptor);
+
+                // if it's not there yet, we create it.
+                if (map == null) {
+                    map = new HashMap<Integer, TimeSeries>();
+                    mValueDescriptorSeriesMap.put(descriptor, map);
+                }
+
+                // get the TimeSeries for this pid
+                TimeSeries timeSeries = map.get(event.pid);
+
+                // if it doesn't exist yet, we create it
+                if (timeSeries == null) {
+                    // get the series name
+                    String seriesFullName = null;
+                    String seriesLabel = getSeriesLabel(event, descriptor);
+
+                    switch (mValueDescriptorCheck) {
+                        case EVENT_CHECK_SAME_TAG:
+                            seriesFullName = String.format("%1$s / %2$s", seriesLabel,
+                                    descriptor.valueName);
+                            break;
+                        case EVENT_CHECK_SAME_VALUE:
+                            seriesFullName = String.format("%1$s", seriesLabel);
+                            break;
+                        default:
+                            seriesFullName = String.format("%1$s / %2$s: %3$s", seriesLabel,
+                                    tagMap.get(descriptor.eventTag),
+                                    descriptor.valueName);
+                            break;
+                    }
+
+                    // get the data set for this ValueType
+                    TimeSeriesCollection dataset = getValueDataset(
+                            logParser.getEventInfoMap().get(event.mTag)[descriptor.valueIndex]
+                                                                        .getValueType(),
+                            accumulateValues);
+
+                    // create the series
+                    timeSeries = new TimeSeries(seriesFullName, Millisecond.class);
+                    if (mMaximumChartItemAge != -1) {
+                        timeSeries.setMaximumItemAge(mMaximumChartItemAge * 1000);
+                    }
+
+                    dataset.addSeries(timeSeries);
+
+                    // add it to the map.
+                    map.put(event.pid, timeSeries);
+                }
+
+                // update the timeSeries.
+
+                // get the value from the event
+                double value = event.getValueAsDouble(descriptor.valueIndex);
+
+                // accumulate the values if needed.
+                if (accumulateValues) {
+                    accumulatedValue += value;
+                    value = accumulatedValue;
+                }
+
+                // get the time
+                if (millisecondTime == null) {
+                    msec = (long)event.sec * 1000L + (event.nsec / 1000000L);
+                    millisecondTime = new Millisecond(new Date(msec));
+                }
+
+                // add the value to the time series
+                timeSeries.addOrUpdate(millisecondTime, value);
+            } catch (InvalidTypeException e) {
+                // just ignore this descriptor if there's a type mismatch
+            }
+        }
+
+        for (OccurrenceDisplayDescriptor descriptor : occurrenceDescriptors) {
+            try {
+                // get the hashmap for this descriptor
+                HashMap<Integer, TimeSeries> map = mOcurrenceDescriptorSeriesMap.get(descriptor);
+
+                // if it's not there yet, we create it.
+                if (map == null) {
+                    map = new HashMap<Integer, TimeSeries>();
+                    mOcurrenceDescriptorSeriesMap.put(descriptor, map);
+                }
+
+                // get the TimeSeries for this pid
+                TimeSeries timeSeries = map.get(event.pid);
+
+                // if it doesn't exist yet, we create it.
+                if (timeSeries == null) {
+                    String seriesLabel = getSeriesLabel(event, descriptor);
+
+                    String seriesFullName = String.format("[%1$s:%2$s]",
+                            tagMap.get(descriptor.eventTag), seriesLabel);
+
+                    timeSeries = new TimeSeries(seriesFullName, Millisecond.class);
+                    if (mMaximumChartItemAge != -1) {
+                        timeSeries.setMaximumItemAge(mMaximumChartItemAge);
+                    }
+
+                    getOccurrenceDataSet().addSeries(timeSeries);
+
+                    map.put(event.pid, timeSeries);
+                }
+
+                // update the series
+
+                // get the time
+                if (millisecondTime == null) {
+                    msec = (long)event.sec * 1000L + (event.nsec / 1000000L);
+                    millisecondTime = new Millisecond(new Date(msec));
+                }
+
+                // add the value to the time series
+                timeSeries.addOrUpdate(millisecondTime, 0); // the value is unused
+            } catch (InvalidTypeException e) {
+                // just ignore this descriptor if there's a type mismatch
+            }
+        }
+
+        // go through all the series and remove old values.
+        if (msec != -1 && mMaximumChartItemAge != -1) {
+            Collection<HashMap<Integer, TimeSeries>> pidMapValues =
+                mValueDescriptorSeriesMap.values();
+
+            for (HashMap<Integer, TimeSeries> pidMapValue : pidMapValues) {
+                Collection<TimeSeries> seriesCollection = pidMapValue.values();
+
+                for (TimeSeries timeSeries : seriesCollection) {
+                    timeSeries.removeAgedItems(msec, true);
+                }
+            }
+
+            pidMapValues = mOcurrenceDescriptorSeriesMap.values();
+            for (HashMap<Integer, TimeSeries> pidMapValue : pidMapValues) {
+                Collection<TimeSeries> seriesCollection = pidMapValue.values();
+
+                for (TimeSeries timeSeries : seriesCollection) {
+                    timeSeries.removeAgedItems(msec, true);
+                }
+            }
+        }
+    }
+
+       /**
+     * Returns a {@link TimeSeriesCollection} for a specific {@link com.android.ddmlib.log.EventValueDescription.ValueType}.
+     * If the data set is not yet created, it is first allocated and set up into the
+     * {@link org.jfree.chart.JFreeChart} object.
+     * @param type the {@link com.android.ddmlib.log.EventValueDescription.ValueType} of the data set.
+     * @param accumulateValues
+     */
+    private TimeSeriesCollection getValueDataset(EventValueDescription.ValueType type, boolean accumulateValues) {
+        TimeSeriesCollection dataset = mValueTypeDataSetMap.get(type);
+        if (dataset == null) {
+            // create the data set and store it in the map
+            dataset = new TimeSeriesCollection();
+            mValueTypeDataSetMap.put(type, dataset);
+
+            // create the renderer and configure it depending on the ValueType
+            AbstractXYItemRenderer renderer;
+            if (type == EventValueDescription.ValueType.PERCENT && accumulateValues) {
+                renderer = new XYAreaRenderer();
+            } else {
+                XYLineAndShapeRenderer r = new XYLineAndShapeRenderer();
+                r.setBaseShapesVisible(type != EventValueDescription.ValueType.PERCENT);
+
+                renderer = r;
+            }
+
+            // set both the dataset and the renderer in the plot object.
+            XYPlot xyPlot = mChart.getXYPlot();
+            xyPlot.setDataset(mDataSetCount, dataset);
+            xyPlot.setRenderer(mDataSetCount, renderer);
+
+            // put a new axis label, and configure it.
+            NumberAxis axis = new NumberAxis(type.toString());
+
+            if (type == EventValueDescription.ValueType.PERCENT) {
+                // force percent range to be (0,100) fixed.
+                axis.setAutoRange(false);
+                axis.setRange(0., 100.);
+            }
+
+            // for the index, we ignore the occurrence dataset
+            int count = mDataSetCount;
+            if (mOccurrenceDataSet != null) {
+                count--;
+            }
+
+            xyPlot.setRangeAxis(count, axis);
+            if ((count % 2) == 0) {
+                xyPlot.setRangeAxisLocation(count, AxisLocation.BOTTOM_OR_LEFT);
+            } else {
+                xyPlot.setRangeAxisLocation(count, AxisLocation.TOP_OR_RIGHT);
+            }
+
+            // now we link the dataset and the axis
+            xyPlot.mapDatasetToRangeAxis(mDataSetCount, count);
+
+            mDataSetCount++;
+        }
+
+        return dataset;
+    }
+
+    /**
+     * Return the series label for this event. This only contains the pid information.
+     * @param event the {@link EventContainer}
+     * @param descriptor the {@link OccurrenceDisplayDescriptor}
+     * @return the series label.
+     * @throws InvalidTypeException
+     */
+    private String getSeriesLabel(EventContainer event, OccurrenceDisplayDescriptor descriptor)
+            throws InvalidTypeException {
+        if (descriptor.seriesValueIndex != -1) {
+            if (descriptor.includePid == false) {
+                return event.getValueAsString(descriptor.seriesValueIndex);
+            } else {
+                return String.format("%1$s (%2$d)",
+                        event.getValueAsString(descriptor.seriesValueIndex), event.pid);
+            }
+        }
+
+        return Integer.toString(event.pid);
+    }
+
+    /**
+     * Returns the {@link TimeSeriesCollection} for the occurrence display. If the data set is not
+     * yet created, it is first allocated and set up into the {@link org.jfree.chart.JFreeChart} object.
+     */
+    private TimeSeriesCollection getOccurrenceDataSet() {
+        if (mOccurrenceDataSet == null) {
+            mOccurrenceDataSet = new TimeSeriesCollection();
+
+            XYPlot xyPlot = mChart.getXYPlot();
+            xyPlot.setDataset(mDataSetCount, mOccurrenceDataSet);
+
+            OccurrenceRenderer renderer = new OccurrenceRenderer();
+            renderer.setBaseShapesVisible(false);
+            xyPlot.setRenderer(mDataSetCount, renderer);
+
+            mDataSetCount++;
+        }
+
+        return mOccurrenceDataSet;
+    }
+
+    /**
+     * Gets display type
+     *
+     * @return display type as an integer
+     */
+    @Override
+    int getDisplayType() {
+        return DISPLAY_TYPE_GRAPH;
+    }
+
+    /**
+     * Sets the current {@link EventLogParser} object.
+     */
+    @Override
+    protected void setNewLogParser(EventLogParser logParser) {
+        if (mChart != null) {
+            mChart.setTitle(getChartTitle(logParser));
+        }
+    }
+    /**
+     * Returns a meaningful chart title based on the value of {@link #mValueDescriptorCheck}.
+     *
+     * @param logParser the logParser.
+     * @return the chart title.
+     */
+    private String getChartTitle(EventLogParser logParser) {
+        if (mValueDescriptors.size() > 0) {
+            String chartDesc = null;
+            switch (mValueDescriptorCheck) {
+                case EVENT_CHECK_SAME_TAG:
+                    if (logParser != null) {
+                        chartDesc = logParser.getTagMap().get(mValueDescriptors.get(0).eventTag);
+                    }
+                    break;
+                case EVENT_CHECK_SAME_VALUE:
+                    if (logParser != null) {
+                        chartDesc = String.format("%1$s / %2$s",
+                                logParser.getTagMap().get(mValueDescriptors.get(0).eventTag),
+                                mValueDescriptors.get(0).valueName);
+                    }
+                    break;
+            }
+
+            if (chartDesc != null) {
+                return String.format("%1$s - %2$s", mName, chartDesc);
+            }
+        }
+
+        return mName;
+    }
+}
\ No newline at end of file
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayLog.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayLog.java
new file mode 100644
index 0000000..8e7c1ac
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayLog.java
@@ -0,0 +1,381 @@
+/*
+ * Copyright (C) 2008 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.ddmuilib.log.event;
+
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventLogParser;
+import com.android.ddmlib.log.EventValueDescription;
+import com.android.ddmlib.log.InvalidTypeException;
+import com.android.ddmuilib.DdmUiPreferences;
+import com.android.ddmuilib.TableHelper;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.ScrollBar;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.TableItem;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+
+public class DisplayLog extends EventDisplay {
+    public DisplayLog(String name) {
+        super(name);
+    }
+
+    private final static String PREFS_COL_DATE = "EventLogPanel.log.Col1"; //$NON-NLS-1$
+    private final static String PREFS_COL_PID = "EventLogPanel.log.Col2"; //$NON-NLS-1$
+    private final static String PREFS_COL_EVENTTAG = "EventLogPanel.log.Col3"; //$NON-NLS-1$
+    private final static String PREFS_COL_VALUENAME = "EventLogPanel.log.Col4"; //$NON-NLS-1$
+    private final static String PREFS_COL_VALUE = "EventLogPanel.log.Col5"; //$NON-NLS-1$
+    private final static String PREFS_COL_TYPE = "EventLogPanel.log.Col6"; //$NON-NLS-1$
+
+    /**
+     * Resets the display.
+     */
+    @Override
+    void resetUI() {
+        mLogTable.removeAll();
+    }
+
+    /**
+     * Adds event to the display.
+     */
+    @Override
+    void newEvent(EventContainer event, EventLogParser logParser) {
+        addToLog(event, logParser);
+    }
+
+    /**
+     * Creates the UI for the event display.
+     *
+     * @param parent    the parent composite.
+     * @param logParser the current log parser.
+     * @return the created control (which may have children).
+     */
+    @Override
+    Control createComposite(Composite parent, EventLogParser logParser, ILogColumnListener listener) {
+        return createLogUI(parent, listener);
+    }
+
+    /**
+     * Adds an {@link EventContainer} to the log.
+     *
+     * @param event     the event.
+     * @param logParser the log parser.
+     */
+    private void addToLog(EventContainer event, EventLogParser logParser) {
+        ScrollBar bar = mLogTable.getVerticalBar();
+        boolean scroll = bar.getMaximum() == bar.getSelection() + bar.getThumb();
+
+        // get the date.
+        Calendar c = Calendar.getInstance();
+        long msec = event.sec * 1000L;
+        c.setTimeInMillis(msec);
+
+        // convert the time into a string
+        String date = String.format("%1$tF %1$tT", c);
+
+        String eventName = logParser.getTagMap().get(event.mTag);
+        String pidName = Integer.toString(event.pid);
+
+        // get the value description
+        EventValueDescription[] valueDescription = logParser.getEventInfoMap().get(event.mTag);
+        if (valueDescription != null) {
+            for (int i = 0; i < valueDescription.length; i++) {
+                EventValueDescription description = valueDescription[i];
+                try {
+                    String value = event.getValueAsString(i);
+
+                    logValue(date, pidName, eventName, description.getName(), value,
+                            description.getEventValueType(), description.getValueType());
+                } catch (InvalidTypeException e) {
+                    logValue(date, pidName, eventName, description.getName(), e.getMessage(),
+                            description.getEventValueType(), description.getValueType());
+                }
+            }
+
+            // scroll if needed, by showing the last item
+            if (scroll) {
+                int itemCount = mLogTable.getItemCount();
+                if (itemCount > 0) {
+                    mLogTable.showItem(mLogTable.getItem(itemCount - 1));
+                }
+            }
+        }
+    }
+
+    /**
+     * Adds an {@link EventContainer} to the log. Only add the values/occurrences defined by
+     * the list of descriptors. If an event is configured to be displayed by value and occurrence,
+     * only the values are displayed (as they mark an event occurrence anyway).
+     * <p/>This method is only called when at least one of the descriptor list is non empty.
+     *
+     * @param event
+     * @param logParser
+     * @param valueDescriptors
+     * @param occurrenceDescriptors
+     */
+    protected void addToLog(EventContainer event, EventLogParser logParser,
+            ArrayList<ValueDisplayDescriptor> valueDescriptors,
+            ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors) {
+        ScrollBar bar = mLogTable.getVerticalBar();
+        boolean scroll = bar.getMaximum() == bar.getSelection() + bar.getThumb();
+
+        // get the date.
+        Calendar c = Calendar.getInstance();
+        long msec = event.sec * 1000L;
+        c.setTimeInMillis(msec);
+
+        // convert the time into a string
+        String date = String.format("%1$tF %1$tT", c);
+
+        String eventName = logParser.getTagMap().get(event.mTag);
+        String pidName = Integer.toString(event.pid);
+
+        if (valueDescriptors.size() > 0) {
+            for (ValueDisplayDescriptor descriptor : valueDescriptors) {
+                logDescriptor(event, descriptor, date, pidName, eventName, logParser);
+            }
+        } else {
+            // we display the event. Since the StringBuilder contains the header (date, event name,
+            // pid) at this point, there isn't anything else to display.
+        }
+
+        // scroll if needed, by showing the last item
+        if (scroll) {
+            int itemCount = mLogTable.getItemCount();
+            if (itemCount > 0) {
+                mLogTable.showItem(mLogTable.getItem(itemCount - 1));
+            }
+        }
+    }
+
+
+    /**
+     * Logs a value in the ui.
+     *
+     * @param date
+     * @param pid
+     * @param event
+     * @param valueName
+     * @param value
+     * @param eventValueType
+     * @param valueType
+     */
+    private void logValue(String date, String pid, String event, String valueName,
+            String value, EventContainer.EventValueType eventValueType, EventValueDescription.ValueType valueType) {
+
+        TableItem item = new TableItem(mLogTable, SWT.NONE);
+        item.setText(0, date);
+        item.setText(1, pid);
+        item.setText(2, event);
+        item.setText(3, valueName);
+        item.setText(4, value);
+
+        String type;
+        if (valueType != EventValueDescription.ValueType.NOT_APPLICABLE) {
+            type = String.format("%1$s, %2$s", eventValueType.toString(), valueType.toString());
+        } else {
+            type = eventValueType.toString();
+        }
+
+        item.setText(5, type);
+    }
+
+    /**
+     * Logs a value from an {@link EventContainer} as defined by the {@link ValueDisplayDescriptor}.
+     *
+     * @param event      the EventContainer
+     * @param descriptor the ValueDisplayDescriptor defining which value to display.
+     * @param date       the date of the event in a string.
+     * @param pidName
+     * @param eventName
+     * @param logParser
+     */
+    private void logDescriptor(EventContainer event, ValueDisplayDescriptor descriptor,
+            String date, String pidName, String eventName, EventLogParser logParser) {
+
+        String value;
+        try {
+            value = event.getValueAsString(descriptor.valueIndex);
+        } catch (InvalidTypeException e) {
+            value = e.getMessage();
+        }
+
+        EventValueDescription[] values = logParser.getEventInfoMap().get(event.mTag);
+
+        EventValueDescription valueDescription = values[descriptor.valueIndex];
+
+        logValue(date, pidName, eventName, descriptor.valueName, value,
+                valueDescription.getEventValueType(), valueDescription.getValueType());
+    }
+
+    /**
+     * Creates the UI for a log display.
+     *
+     * @param parent   the parent {@link Composite}
+     * @param listener the {@link ILogColumnListener} to notify on column resize events.
+     * @return the top Composite of the UI.
+     */
+    private Control createLogUI(Composite parent, final ILogColumnListener listener) {
+        Composite mainComp = new Composite(parent, SWT.NONE);
+        GridLayout gl;
+        mainComp.setLayout(gl = new GridLayout(1, false));
+        gl.marginHeight = gl.marginWidth = 0;
+        mainComp.addDisposeListener(new DisposeListener() {
+            @Override
+            public void widgetDisposed(DisposeEvent e) {
+                mLogTable = null;
+            }
+        });
+
+        Label l = new Label(mainComp, SWT.CENTER);
+        l.setText(mName);
+        l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        mLogTable = new Table(mainComp, SWT.MULTI | SWT.FULL_SELECTION | SWT.V_SCROLL |
+                SWT.BORDER);
+        mLogTable.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        IPreferenceStore store = DdmUiPreferences.getStore();
+
+        TableColumn col = TableHelper.createTableColumn(
+                mLogTable, "Time",
+                SWT.LEFT, "0000-00-00 00:00:00", PREFS_COL_DATE, store); //$NON-NLS-1$
+        col.addControlListener(new ControlAdapter() {
+            @Override
+            public void controlResized(ControlEvent e) {
+                Object source = e.getSource();
+                if (source instanceof TableColumn) {
+                    listener.columnResized(0, (TableColumn) source);
+                }
+            }
+        });
+
+        col = TableHelper.createTableColumn(
+                mLogTable, "pid",
+                SWT.LEFT, "0000", PREFS_COL_PID, store); //$NON-NLS-1$
+        col.addControlListener(new ControlAdapter() {
+            @Override
+            public void controlResized(ControlEvent e) {
+                Object source = e.getSource();
+                if (source instanceof TableColumn) {
+                    listener.columnResized(1, (TableColumn) source);
+                }
+            }
+        });
+
+        col = TableHelper.createTableColumn(
+                mLogTable, "Event",
+                SWT.LEFT, "abcdejghijklmno", PREFS_COL_EVENTTAG, store); //$NON-NLS-1$
+        col.addControlListener(new ControlAdapter() {
+            @Override
+            public void controlResized(ControlEvent e) {
+                Object source = e.getSource();
+                if (source instanceof TableColumn) {
+                    listener.columnResized(2, (TableColumn) source);
+                }
+            }
+        });
+
+        col = TableHelper.createTableColumn(
+                mLogTable, "Name",
+                SWT.LEFT, "Process Name", PREFS_COL_VALUENAME, store); //$NON-NLS-1$
+        col.addControlListener(new ControlAdapter() {
+            @Override
+            public void controlResized(ControlEvent e) {
+                Object source = e.getSource();
+                if (source instanceof TableColumn) {
+                    listener.columnResized(3, (TableColumn) source);
+                }
+            }
+        });
+
+        col = TableHelper.createTableColumn(
+                mLogTable, "Value",
+                SWT.LEFT, "0000000", PREFS_COL_VALUE, store); //$NON-NLS-1$
+        col.addControlListener(new ControlAdapter() {
+            @Override
+            public void controlResized(ControlEvent e) {
+                Object source = e.getSource();
+                if (source instanceof TableColumn) {
+                    listener.columnResized(4, (TableColumn) source);
+                }
+            }
+        });
+
+        col = TableHelper.createTableColumn(
+                mLogTable, "Type",
+                SWT.LEFT, "long, seconds", PREFS_COL_TYPE, store); //$NON-NLS-1$
+        col.addControlListener(new ControlAdapter() {
+            @Override
+            public void controlResized(ControlEvent e) {
+                Object source = e.getSource();
+                if (source instanceof TableColumn) {
+                    listener.columnResized(5, (TableColumn) source);
+                }
+            }
+        });
+
+        mLogTable.setHeaderVisible(true);
+        mLogTable.setLinesVisible(true);
+
+        return mainComp;
+    }
+
+    /**
+     * Resizes the <code>index</code>-th column of the log {@link Table} (if applicable).
+     * <p/>
+     * This does nothing if the <code>Table</code> object is <code>null</code> (because the display
+     * type does not use a column) or if the <code>index</code>-th column is in fact the originating
+     * column passed as argument.
+     *
+     * @param index        the index of the column to resize
+     * @param sourceColumn the original column that was resize, and on which we need to sync the
+     *                     index-th column width.
+     */
+    @Override
+    void resizeColumn(int index, TableColumn sourceColumn) {
+        if (mLogTable != null) {
+            TableColumn col = mLogTable.getColumn(index);
+            if (col != sourceColumn) {
+                col.setWidth(sourceColumn.getWidth());
+            }
+        }
+    }
+
+    /**
+     * Gets display type
+     *
+     * @return display type as an integer
+     */
+    @Override
+    int getDisplayType() {
+        return DISPLAY_TYPE_LOG_ALL;
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySync.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySync.java
new file mode 100644
index 0000000..6122513
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySync.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2008 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.ddmuilib.log.event;
+
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventLogParser;
+import com.android.ddmlib.log.InvalidTypeException;
+
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.jfree.chart.labels.CustomXYToolTipGenerator;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.XYBarRenderer;
+import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
+import org.jfree.data.time.FixedMillisecond;
+import org.jfree.data.time.SimpleTimePeriod;
+import org.jfree.data.time.TimePeriodValues;
+import org.jfree.data.time.TimePeriodValuesCollection;
+import org.jfree.data.time.TimeSeries;
+import org.jfree.data.time.TimeSeriesCollection;
+import org.jfree.util.ShapeUtilities;
+
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Scanner;
+import java.util.regex.Pattern;
+
+public class DisplaySync extends SyncCommon {
+
+    // Information to graph for each authority
+    private TimePeriodValues mDatasetsSync[];
+    private List<String> mTooltipsSync[];
+    private CustomXYToolTipGenerator mTooltipGenerators[];
+    private TimeSeries mDatasetsSyncTickle[];
+
+    // Dataset of error events to graph
+    private TimeSeries mDatasetError;
+
+    public DisplaySync(String name) {
+        super(name);
+    }
+
+    /**
+     * Creates the UI for the event display.
+     * @param parent the parent composite.
+     * @param logParser the current log parser.
+     * @return the created control (which may have children).
+     */
+    @Override
+    public Control createComposite(final Composite parent, EventLogParser logParser,
+            final ILogColumnListener listener) {
+        Control composite = createCompositeChart(parent, logParser, "Sync Status");
+        resetUI();
+        return composite;
+    }
+
+    /**
+     * Resets the display.
+     */
+    @Override
+    void resetUI() {
+        super.resetUI();
+        XYPlot xyPlot = mChart.getXYPlot();
+
+        XYBarRenderer br = new XYBarRenderer();
+        mDatasetsSync = new TimePeriodValues[NUM_AUTHS];
+
+        @SuppressWarnings("unchecked")
+        List<String> mTooltipsSyncTmp[] = new List[NUM_AUTHS];
+        mTooltipsSync = mTooltipsSyncTmp;
+
+        mTooltipGenerators = new CustomXYToolTipGenerator[NUM_AUTHS];
+
+        TimePeriodValuesCollection tpvc = new TimePeriodValuesCollection();
+        xyPlot.setDataset(tpvc);
+        xyPlot.setRenderer(0, br);
+
+        XYLineAndShapeRenderer ls = new XYLineAndShapeRenderer();
+        ls.setBaseLinesVisible(false);
+        mDatasetsSyncTickle = new TimeSeries[NUM_AUTHS];
+        TimeSeriesCollection tsc = new TimeSeriesCollection();
+        xyPlot.setDataset(1, tsc);
+        xyPlot.setRenderer(1, ls);
+
+        mDatasetError = new TimeSeries("Errors", FixedMillisecond.class);
+        xyPlot.setDataset(2, new TimeSeriesCollection(mDatasetError));
+        XYLineAndShapeRenderer errls = new XYLineAndShapeRenderer();
+        errls.setBaseLinesVisible(false);
+        errls.setSeriesPaint(0, Color.RED);
+        xyPlot.setRenderer(2, errls);
+
+        for (int i = 0; i < NUM_AUTHS; i++) {
+            br.setSeriesPaint(i, AUTH_COLORS[i]);
+            ls.setSeriesPaint(i, AUTH_COLORS[i]);
+            mDatasetsSync[i] = new TimePeriodValues(AUTH_NAMES[i]);
+            tpvc.addSeries(mDatasetsSync[i]);
+            mTooltipsSync[i] = new ArrayList<String>();
+            mTooltipGenerators[i] = new CustomXYToolTipGenerator();
+            br.setSeriesToolTipGenerator(i, mTooltipGenerators[i]);
+            mTooltipGenerators[i].addToolTipSeries(mTooltipsSync[i]);
+
+            mDatasetsSyncTickle[i] = new TimeSeries(AUTH_NAMES[i] + " tickle",
+                    FixedMillisecond.class);
+            tsc.addSeries(mDatasetsSyncTickle[i]);
+            ls.setSeriesShape(i, ShapeUtilities.createUpTriangle(2.5f));
+        }
+    }
+
+    /**
+     * Updates the display with a new event.
+     *
+     * @param event     The event
+     * @param logParser The parser providing the event.
+     */
+    @Override
+    void newEvent(EventContainer event, EventLogParser logParser) {
+        super.newEvent(event, logParser); // Handle sync operation
+        try {
+            if (event.mTag == EVENT_TICKLE) {
+                int auth = getAuth(event.getValueAsString(0));
+                if (auth >= 0) {
+                    long msec = event.sec * 1000L + (event.nsec / 1000000L);
+                    mDatasetsSyncTickle[auth].addOrUpdate(new FixedMillisecond(msec), -1);
+                }
+            }
+        } catch (InvalidTypeException e) {
+        }
+    }
+
+    /**
+     * Generate the height for an event.
+     * Height is somewhat arbitrarily the count of "things" that happened
+     * during the sync.
+     * When network traffic measurements are available, code should be modified
+     * to use that instead.
+     * @param details The details string associated with the event
+     * @return The height in arbirary units (0-100)
+     */
+    private int getHeightFromDetails(String details) {
+        if (details == null) {
+            return 1; // Arbitrary
+        }
+        int total = 0;
+        String parts[] = details.split("[a-zA-Z]");
+        for (String part : parts) {
+            if ("".equals(part)) continue;
+            total += Integer.parseInt(part);
+        }
+        if (total == 0) {
+            total = 1;
+        }
+        return total;
+    }
+
+    /**
+     * Generates the tooltips text for an event.
+     * This method decodes the cryptic details string.
+     * @param auth The authority associated with the event
+     * @param details The details string
+     * @param eventSource server, poll, etc.
+     * @return The text to display in the tooltips
+     */
+    private String getTextFromDetails(int auth, String details, int eventSource) {
+
+        StringBuffer sb = new StringBuffer();
+        sb.append(AUTH_NAMES[auth]).append(": \n");
+
+        Scanner scanner = new Scanner(details);
+        Pattern charPat = Pattern.compile("[a-zA-Z]");
+        Pattern numPat = Pattern.compile("[0-9]+");
+        while (scanner.hasNext()) {
+            String key = scanner.findInLine(charPat);
+            int val = Integer.parseInt(scanner.findInLine(numPat));
+            if (auth == GMAIL && "M".equals(key)) {
+                sb.append("messages from server: ").append(val).append("\n");
+            } else if (auth == GMAIL && "L".equals(key)) {
+                sb.append("labels from server: ").append(val).append("\n");
+            } else if (auth == GMAIL && "C".equals(key)) {
+                sb.append("check conversation requests from server: ").append(val).append("\n");
+            } else if (auth == GMAIL && "A".equals(key)) {
+                sb.append("attachments from server: ").append(val).append("\n");
+            } else if (auth == GMAIL && "U".equals(key)) {
+                sb.append("op updates from server: ").append(val).append("\n");
+            } else if (auth == GMAIL && "u".equals(key)) {
+                sb.append("op updates to server: ").append(val).append("\n");
+            } else if (auth == GMAIL && "S".equals(key)) {
+                sb.append("send/receive cycles: ").append(val).append("\n");
+            } else if ("Q".equals(key)) {
+                sb.append("queries to server: ").append(val).append("\n");
+            } else if ("E".equals(key)) {
+                sb.append("entries from server: ").append(val).append("\n");
+            } else if ("u".equals(key)) {
+                sb.append("updates from client: ").append(val).append("\n");
+            } else if ("i".equals(key)) {
+                sb.append("inserts from client: ").append(val).append("\n");
+            } else if ("d".equals(key)) {
+                sb.append("deletes from client: ").append(val).append("\n");
+            } else if ("f".equals(key)) {
+                sb.append("full sync requested\n");
+            } else if ("r".equals(key)) {
+                sb.append("partial sync unavailable\n");
+            } else if ("X".equals(key)) {
+                sb.append("hard error\n");
+            } else if ("e".equals(key)) {
+                sb.append("number of parse exceptions: ").append(val).append("\n");
+            } else if ("c".equals(key)) {
+                sb.append("number of conflicts: ").append(val).append("\n");
+            } else if ("a".equals(key)) {
+                sb.append("number of auth exceptions: ").append(val).append("\n");
+            } else if ("D".equals(key)) {
+                sb.append("too many deletions\n");
+            } else if ("R".equals(key)) {
+                sb.append("too many retries: ").append(val).append("\n");
+            } else if ("b".equals(key)) {
+                sb.append("database error\n");
+            } else if ("x".equals(key)) {
+                sb.append("soft error\n");
+            } else if ("l".equals(key)) {
+                sb.append("sync already in progress\n");
+            } else if ("I".equals(key)) {
+                sb.append("io exception\n");
+            } else if (auth == CONTACTS && "g".equals(key)) {
+                sb.append("aggregation query: ").append(val).append("\n");
+            } else if (auth == CONTACTS && "G".equals(key)) {
+                sb.append("aggregation merge: ").append(val).append("\n");
+            } else if (auth == CONTACTS && "n".equals(key)) {
+                sb.append("num entries: ").append(val).append("\n");
+            } else if (auth == CONTACTS && "p".equals(key)) {
+                sb.append("photos uploaded from server: ").append(val).append("\n");
+            } else if (auth == CONTACTS && "P".equals(key)) {
+                sb.append("photos downloaded from server: ").append(val).append("\n");
+            } else if (auth == CALENDAR && "F".equals(key)) {
+                sb.append("server refresh\n");
+            } else if (auth == CALENDAR && "s".equals(key)) {
+                sb.append("server diffs fetched\n");
+            } else {
+                sb.append(key).append("=").append(val);
+            }
+        }
+        if (eventSource == 0) {
+            sb.append("(server)");
+        } else if (eventSource == 1) {
+            sb.append("(local)");
+        } else if (eventSource == 2) {
+            sb.append("(poll)");
+        } else if (eventSource == 3) {
+            sb.append("(user)");
+        }
+        return sb.toString();
+    }
+
+
+    /**
+     * Callback to process a sync event.
+     */
+    @Override
+    void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime,
+            String details, boolean newEvent, int syncSource) {
+        if (!newEvent) {
+            // Details arrived for a previous sync event
+            // Remove event before reinserting.
+            int lastItem = mDatasetsSync[auth].getItemCount();
+            mDatasetsSync[auth].delete(lastItem-1, lastItem-1);
+            mTooltipsSync[auth].remove(lastItem-1);
+        }
+        double height = getHeightFromDetails(details);
+        height = height / (stopTime - startTime + 1) * 10000;
+        if (height > 30) {
+            height = 30;
+        }
+        mDatasetsSync[auth].add(new SimpleTimePeriod(startTime, stopTime), height);
+        mTooltipsSync[auth].add(getTextFromDetails(auth, details, syncSource));
+        mTooltipGenerators[auth].addToolTipSeries(mTooltipsSync[auth]);
+        if (details.indexOf('x') >= 0 || details.indexOf('X') >= 0) {
+            long msec = event.sec * 1000L + (event.nsec / 1000000L);
+            mDatasetError.addOrUpdate(new FixedMillisecond(msec), -1);
+        }
+    }
+
+    /**
+     * Gets display type
+     *
+     * @return display type as an integer
+     */
+    @Override
+    int getDisplayType() {
+        return DISPLAY_TYPE_SYNC;
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncHistogram.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncHistogram.java
new file mode 100644
index 0000000..5bfc039
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncHistogram.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2008 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.ddmuilib.log.event;
+
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventLogParser;
+
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.AbstractXYItemRenderer;
+import org.jfree.chart.renderer.xy.XYBarRenderer;
+import org.jfree.data.time.RegularTimePeriod;
+import org.jfree.data.time.SimpleTimePeriod;
+import org.jfree.data.time.TimePeriodValues;
+import org.jfree.data.time.TimePeriodValuesCollection;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TimeZone;
+
+public class DisplaySyncHistogram extends SyncCommon {
+
+    Map<SimpleTimePeriod, Integer> mTimePeriodMap[];
+
+    // Information to graph for each authority
+    private TimePeriodValues mDatasetsSyncHist[];
+
+    public DisplaySyncHistogram(String name) {
+        super(name);
+    }
+
+    /**
+     * Creates the UI for the event display.
+     * @param parent the parent composite.
+     * @param logParser the current log parser.
+     * @return the created control (which may have children).
+     */
+    @Override
+    public Control createComposite(final Composite parent, EventLogParser logParser,
+            final ILogColumnListener listener) {
+        Control composite = createCompositeChart(parent, logParser, "Sync Histogram");
+        resetUI();
+        return composite;
+    }
+
+    /**
+     * Resets the display.
+     */
+    @Override
+    void resetUI() {
+        super.resetUI();
+        XYPlot xyPlot = mChart.getXYPlot();
+
+        AbstractXYItemRenderer br = new XYBarRenderer();
+        mDatasetsSyncHist = new TimePeriodValues[NUM_AUTHS+1];
+
+        @SuppressWarnings("unchecked")
+        Map<SimpleTimePeriod, Integer> mTimePeriodMapTmp[] = new HashMap[NUM_AUTHS + 1];
+        mTimePeriodMap = mTimePeriodMapTmp;
+
+        TimePeriodValuesCollection tpvc = new TimePeriodValuesCollection();
+        xyPlot.setDataset(tpvc);
+        xyPlot.setRenderer(br);
+
+        for (int i = 0; i < NUM_AUTHS + 1; i++) {
+            br.setSeriesPaint(i, AUTH_COLORS[i]);
+            mDatasetsSyncHist[i] = new TimePeriodValues(AUTH_NAMES[i]);
+            tpvc.addSeries(mDatasetsSyncHist[i]);
+            mTimePeriodMap[i] = new HashMap<SimpleTimePeriod, Integer>();
+
+        }
+    }
+
+    /**
+     * Callback to process a sync event.
+     *
+     * @param event      The sync event
+     * @param startTime Start time (ms) of events
+     * @param stopTime Stop time (ms) of events
+     * @param details Details associated with the event.
+     * @param newEvent True if this event is a new sync event.  False if this event
+     * @param syncSource
+     */
+    @Override
+    void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime,
+            String details, boolean newEvent, int syncSource) {
+        if (newEvent) {
+            if (details.indexOf('x') >= 0 || details.indexOf('X') >= 0) {
+                auth = ERRORS;
+            }
+            double delta = (stopTime - startTime) * 100. / 1000 / 3600; // Percent of hour
+            addHistEvent(0, auth, delta);
+        } else {
+            // sync_details arrived for an event that has already been graphed.
+            if (details.indexOf('x') >= 0 || details.indexOf('X') >= 0) {
+                // Item turns out to be in error, so transfer time from old auth to error.
+                double delta = (stopTime - startTime) * 100. / 1000 / 3600; // Percent of hour
+                addHistEvent(0, auth, -delta);
+                addHistEvent(0, ERRORS, delta);
+            }
+        }
+    }
+
+    /**
+     * Helper to add an event to the data series.
+     * Also updates error series if appropriate (x or X in details).
+     * @param stopTime Time event ends
+     * @param auth Sync authority
+     * @param value Value to graph for event
+     */
+    private void addHistEvent(long stopTime, int auth, double value) {
+        SimpleTimePeriod hour = getTimePeriod(stopTime, mHistWidth);
+
+        // Loop over all datasets to do the stacking.
+        for (int i = auth; i <= ERRORS; i++) {
+            addToPeriod(mDatasetsSyncHist, i, hour, value);
+        }
+    }
+
+    private void addToPeriod(TimePeriodValues tpv[], int auth, SimpleTimePeriod period,
+            double value) {
+        int index;
+        if (mTimePeriodMap[auth].containsKey(period)) {
+            index = mTimePeriodMap[auth].get(period);
+            double oldValue = tpv[auth].getValue(index).doubleValue();
+            tpv[auth].update(index, oldValue + value);
+        } else {
+            index = tpv[auth].getItemCount();
+            mTimePeriodMap[auth].put(period, index);
+            tpv[auth].add(period, value);
+        }
+    }
+
+    /**
+     * Creates a multiple-hour time period for the histogram.
+     * @param time Time in milliseconds.
+     * @param numHoursWide: should divide into a day.
+     * @return SimpleTimePeriod covering the number of hours and containing time.
+     */
+    private SimpleTimePeriod getTimePeriod(long time, long numHoursWide) {
+        Date date = new Date(time);
+        TimeZone zone = RegularTimePeriod.DEFAULT_TIME_ZONE;
+        Calendar calendar = Calendar.getInstance(zone);
+        calendar.setTime(date);
+        long hoursOfYear = calendar.get(Calendar.HOUR_OF_DAY) +
+                calendar.get(Calendar.DAY_OF_YEAR) * 24;
+        int year = calendar.get(Calendar.YEAR);
+        hoursOfYear = (hoursOfYear / numHoursWide) * numHoursWide;
+        calendar.clear();
+        calendar.set(year, 0, 1, 0, 0); // Jan 1
+        long start = calendar.getTimeInMillis() + hoursOfYear * 3600 * 1000;
+        return new SimpleTimePeriod(start, start + numHoursWide * 3600 * 1000);
+    }
+
+    /**
+     * Gets display type
+     *
+     * @return display type as an integer
+     */
+    @Override
+    int getDisplayType() {
+        return DISPLAY_TYPE_SYNC_HIST;
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncPerf.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncPerf.java
new file mode 100644
index 0000000..10176e3
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncPerf.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2009 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.ddmuilib.log.event;
+
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventLogParser;
+import com.android.ddmlib.log.InvalidTypeException;
+
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.jfree.chart.labels.CustomXYToolTipGenerator;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.XYBarRenderer;
+import org.jfree.data.time.SimpleTimePeriod;
+import org.jfree.data.time.TimePeriodValues;
+import org.jfree.data.time.TimePeriodValuesCollection;
+
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.List;
+
+public class DisplaySyncPerf extends SyncCommon {
+
+    CustomXYToolTipGenerator mTooltipGenerator;
+
+    List<String> mTooltips[];
+
+    // The series number for each graphed item.
+    // sync authorities are 0-3
+    private static final int DB_QUERY = 4;
+    private static final int DB_WRITE = 5;
+    private static final int HTTP_NETWORK = 6;
+    private static final int HTTP_PROCESSING = 7;
+    private static final int NUM_SERIES = (HTTP_PROCESSING + 1);
+    private static final String SERIES_NAMES[] = {"Calendar", "Gmail", "Feeds", "Contacts",
+            "DB Query", "DB Write", "HTTP Response", "HTTP Processing",};
+    private static final Color SERIES_COLORS[] = {Color.MAGENTA, Color.GREEN, Color.BLUE,
+            Color.ORANGE, Color.RED, Color.CYAN, Color.PINK, Color.DARK_GRAY};
+    private static final double SERIES_YCOORD[] = {0, 0, 0, 0, 1, 1, 2, 2};
+
+    // Values from data/etc/event-log-tags
+    private static final int EVENT_DB_OPERATION = 52000;
+    private static final int EVENT_HTTP_STATS = 52001;
+    // op types for EVENT_DB_OPERATION
+    final int EVENT_DB_QUERY = 0;
+    final int EVENT_DB_WRITE = 1;
+
+    // Information to graph for each authority
+    private TimePeriodValues mDatasets[];
+
+    /**
+     * TimePeriodValuesCollection that supports Y intervals.  This allows the
+     * creation of "floating" bars, rather than bars rooted to the axis.
+     */
+    class YIntervalTimePeriodValuesCollection extends TimePeriodValuesCollection {
+        /** default serial UID */
+        private static final long serialVersionUID = 1L;
+
+        private double yheight;
+
+        /**
+         * Constructs a collection of bars with a fixed Y height.
+         *
+         * @param yheight The height of the bars.
+         */
+        YIntervalTimePeriodValuesCollection(double yheight) {
+            this.yheight = yheight;
+        }
+
+        /**
+         * Returns ending Y value that is a fixed amount greater than the starting value.
+         *
+         * @param series the series (zero-based index).
+         * @param item   the item (zero-based index).
+         * @return The ending Y value for the specified series and item.
+         */
+        @Override
+        public Number getEndY(int series, int item) {
+            return getY(series, item).doubleValue() + yheight;
+        }
+    }
+
+    /**
+     * Constructs a graph of network and database stats.
+     *
+     * @param name The name of this graph in the graph list.
+     */
+    public DisplaySyncPerf(String name) {
+        super(name);
+    }
+
+    /**
+     * Creates the UI for the event display.
+     *
+     * @param parent    the parent composite.
+     * @param logParser the current log parser.
+     * @return the created control (which may have children).
+     */
+    @Override
+    public Control createComposite(final Composite parent, EventLogParser logParser,
+            final ILogColumnListener listener) {
+        Control composite = createCompositeChart(parent, logParser, "Sync Performance");
+        resetUI();
+        return composite;
+    }
+
+    /**
+     * Resets the display.
+     */
+    @Override
+    void resetUI() {
+        super.resetUI();
+        XYPlot xyPlot = mChart.getXYPlot();
+        xyPlot.getRangeAxis().setVisible(false);
+        mTooltipGenerator = new CustomXYToolTipGenerator();
+
+        @SuppressWarnings("unchecked")
+        List<String>[] mTooltipsTmp = new List[NUM_SERIES];
+        mTooltips = mTooltipsTmp;
+
+        XYBarRenderer br = new XYBarRenderer();
+        br.setUseYInterval(true);
+        mDatasets = new TimePeriodValues[NUM_SERIES];
+
+        TimePeriodValuesCollection tpvc = new YIntervalTimePeriodValuesCollection(1);
+        xyPlot.setDataset(tpvc);
+        xyPlot.setRenderer(br);
+
+        for (int i = 0; i < NUM_SERIES; i++) {
+            br.setSeriesPaint(i, SERIES_COLORS[i]);
+            mDatasets[i] = new TimePeriodValues(SERIES_NAMES[i]);
+            tpvc.addSeries(mDatasets[i]);
+            mTooltips[i] = new ArrayList<String>();
+            mTooltipGenerator.addToolTipSeries(mTooltips[i]);
+            br.setSeriesToolTipGenerator(i, mTooltipGenerator);
+        }
+    }
+
+    /**
+     * Updates the display with a new event.
+     *
+     * @param event     The event
+     * @param logParser The parser providing the event.
+     */
+    @Override
+    void newEvent(EventContainer event, EventLogParser logParser) {
+        super.newEvent(event, logParser); // Handle sync operation
+        try {
+            if (event.mTag == EVENT_DB_OPERATION) {
+                // 52000 db_operation (name|3),(op_type|1|5),(time|2|3)
+                String tip = event.getValueAsString(0);
+                long endTime = event.sec * 1000L + (event.nsec / 1000000L);
+                int opType = Integer.parseInt(event.getValueAsString(1));
+                long duration = Long.parseLong(event.getValueAsString(2));
+
+                if (opType == EVENT_DB_QUERY) {
+                    mDatasets[DB_QUERY].add(new SimpleTimePeriod(endTime - duration, endTime),
+                            SERIES_YCOORD[DB_QUERY]);
+                    mTooltips[DB_QUERY].add(tip);
+                } else if (opType == EVENT_DB_WRITE) {
+                    mDatasets[DB_WRITE].add(new SimpleTimePeriod(endTime - duration, endTime),
+                            SERIES_YCOORD[DB_WRITE]);
+                    mTooltips[DB_WRITE].add(tip);
+                }
+            } else if (event.mTag == EVENT_HTTP_STATS) {
+                // 52001 http_stats (useragent|3),(response|2|3),(processing|2|3),(tx|1|2),(rx|1|2)
+                String tip = event.getValueAsString(0) + ", tx:" + event.getValueAsString(3) +
+                        ", rx: " + event.getValueAsString(4);
+                long endTime = event.sec * 1000L + (event.nsec / 1000000L);
+                long netEndTime = endTime - Long.parseLong(event.getValueAsString(2));
+                long netStartTime = netEndTime - Long.parseLong(event.getValueAsString(1));
+                mDatasets[HTTP_NETWORK].add(new SimpleTimePeriod(netStartTime, netEndTime),
+                        SERIES_YCOORD[HTTP_NETWORK]);
+                mDatasets[HTTP_PROCESSING].add(new SimpleTimePeriod(netEndTime, endTime),
+                        SERIES_YCOORD[HTTP_PROCESSING]);
+                mTooltips[HTTP_NETWORK].add(tip);
+                mTooltips[HTTP_PROCESSING].add(tip);
+            }
+        } catch (NumberFormatException e) {
+            // This can happen when parsing events from froyo+ where the event with id 52000
+            // as a completely different format. For now, skip this event if this happens.
+        } catch (InvalidTypeException e) {
+        }
+    }
+
+    /**
+     * Callback from super.newEvent to process a sync event.
+     *
+     * @param event      The sync event
+     * @param startTime  Start time (ms) of events
+     * @param stopTime   Stop time (ms) of events
+     * @param details    Details associated with the event.
+     * @param newEvent   True if this event is a new sync event.  False if this event
+     * @param syncSource
+     */
+    @Override
+    void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime,
+            String details, boolean newEvent, int syncSource) {
+        if (newEvent) {
+            mDatasets[auth].add(new SimpleTimePeriod(startTime, stopTime), SERIES_YCOORD[auth]);
+        }
+    }
+
+    /**
+     * Gets display type
+     *
+     * @return display type as an integer
+     */
+    @Override
+    int getDisplayType() {
+        return DISPLAY_TYPE_SYNC_PERF;
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplay.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplay.java
new file mode 100644
index 0000000..d0d2789
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplay.java
@@ -0,0 +1,975 @@
+/*
+ * Copyright (C) 2008 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.ddmuilib.log.event;
+
+import com.android.ddmlib.Log;
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventContainer.CompareMethod;
+import com.android.ddmlib.log.EventContainer.EventValueType;
+import com.android.ddmlib.log.EventLogParser;
+import com.android.ddmlib.log.EventValueDescription.ValueType;
+import com.android.ddmlib.log.InvalidTypeException;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.event.ChartChangeEvent;
+import org.jfree.chart.event.ChartChangeEventType;
+import org.jfree.chart.event.ChartChangeListener;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.title.TextTitle;
+import org.jfree.data.time.Millisecond;
+import org.jfree.data.time.TimeSeries;
+import org.jfree.data.time.TimeSeriesCollection;
+import org.jfree.experimental.chart.swt.ChartComposite;
+import org.jfree.experimental.swt.SWTUtils;
+
+import java.security.InvalidParameterException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * Represents a custom display of one or more events.
+ */
+abstract class EventDisplay {
+
+    private final static String DISPLAY_DATA_STORAGE_SEPARATOR = ":"; //$NON-NLS-1$
+    private final static String PID_STORAGE_SEPARATOR = ","; //$NON-NLS-1$
+    private final static String DESCRIPTOR_STORAGE_SEPARATOR = "$"; //$NON-NLS-1$
+    private final static String DESCRIPTOR_DATA_STORAGE_SEPARATOR = "!"; //$NON-NLS-1$
+
+    private final static String FILTER_VALUE_NULL = "<null>"; //$NON-NLS-1$
+
+    public final static int DISPLAY_TYPE_LOG_ALL = 0;
+    public final static int DISPLAY_TYPE_FILTERED_LOG = 1;
+    public final static int DISPLAY_TYPE_GRAPH = 2;
+    public final static int DISPLAY_TYPE_SYNC = 3;
+    public final static int DISPLAY_TYPE_SYNC_HIST = 4;
+    public final static int DISPLAY_TYPE_SYNC_PERF = 5;
+
+    private final static int EVENT_CHECK_FAILED = 0;
+    protected final static int EVENT_CHECK_SAME_TAG = 1;
+    protected final static int EVENT_CHECK_SAME_VALUE = 2;
+
+    /**
+     * Creates the appropriate EventDisplay subclass.
+     *
+     * @param type the type of display (DISPLAY_TYPE_LOG_ALL, etc)
+     * @param name the name of the display
+     * @return the created object
+     */
+    public static EventDisplay eventDisplayFactory(int type, String name) {
+        switch (type) {
+            case DISPLAY_TYPE_LOG_ALL:
+                return new DisplayLog(name);
+            case DISPLAY_TYPE_FILTERED_LOG:
+                return new DisplayFilteredLog(name);
+            case DISPLAY_TYPE_SYNC:
+                return new DisplaySync(name);
+            case DISPLAY_TYPE_SYNC_HIST:
+                return new DisplaySyncHistogram(name);
+            case DISPLAY_TYPE_GRAPH:
+                return new DisplayGraph(name);
+            case DISPLAY_TYPE_SYNC_PERF:
+                return new DisplaySyncPerf(name);
+            default:
+                throw new InvalidParameterException("Unknown Display Type " + type); //$NON-NLS-1$
+        }
+    }
+
+    /**
+     * Adds event to the display.
+     * @param event The event
+     * @param logParser The log parser.
+     */
+    abstract void newEvent(EventContainer event, EventLogParser logParser);
+
+    /**
+     * Resets the display.
+     */
+    abstract void resetUI();
+
+    /**
+     * Gets display type
+     *
+     * @return display type as an integer
+     */
+    abstract int getDisplayType();
+
+    /**
+     * Creates the UI for the event display.
+     *
+     * @param parent    the parent composite.
+     * @param logParser the current log parser.
+     * @return the created control (which may have children).
+     */
+    abstract Control createComposite(final Composite parent, EventLogParser logParser,
+            final ILogColumnListener listener);
+
+    interface ILogColumnListener {
+        void columnResized(int index, TableColumn sourceColumn);
+    }
+
+    /**
+     * Describes an event to be displayed.
+     */
+    static class OccurrenceDisplayDescriptor {
+
+        int eventTag = -1;
+        int seriesValueIndex = -1;
+        boolean includePid = false;
+        int filterValueIndex = -1;
+        CompareMethod filterCompareMethod = CompareMethod.EQUAL_TO;
+        Object filterValue = null;
+
+        OccurrenceDisplayDescriptor() {
+        }
+
+        OccurrenceDisplayDescriptor(OccurrenceDisplayDescriptor descriptor) {
+            replaceWith(descriptor);
+        }
+
+        OccurrenceDisplayDescriptor(int eventTag) {
+            this.eventTag = eventTag;
+        }
+
+        OccurrenceDisplayDescriptor(int eventTag, int seriesValueIndex) {
+            this.eventTag = eventTag;
+            this.seriesValueIndex = seriesValueIndex;
+        }
+
+        void replaceWith(OccurrenceDisplayDescriptor descriptor) {
+            eventTag = descriptor.eventTag;
+            seriesValueIndex = descriptor.seriesValueIndex;
+            includePid = descriptor.includePid;
+            filterValueIndex = descriptor.filterValueIndex;
+            filterCompareMethod = descriptor.filterCompareMethod;
+            filterValue = descriptor.filterValue;
+        }
+
+        /**
+         * Loads the descriptor parameter from a storage string. The storage string must have
+         * been generated with {@link #getStorageString()}.
+         *
+         * @param storageString the storage string
+         */
+        final void loadFrom(String storageString) {
+            String[] values = storageString.split(Pattern.quote(DESCRIPTOR_DATA_STORAGE_SEPARATOR));
+            loadFrom(values, 0);
+        }
+
+        /**
+         * Loads the parameters from an array of strings.
+         *
+         * @param storageStrings the strings representing each parameter.
+         * @param index          the starting index in the array of strings.
+         * @return the new index in the array.
+         */
+        protected int loadFrom(String[] storageStrings, int index) {
+            eventTag = Integer.parseInt(storageStrings[index++]);
+            seriesValueIndex = Integer.parseInt(storageStrings[index++]);
+            includePid = Boolean.parseBoolean(storageStrings[index++]);
+            filterValueIndex = Integer.parseInt(storageStrings[index++]);
+            try {
+                filterCompareMethod = CompareMethod.valueOf(storageStrings[index++]);
+            } catch (IllegalArgumentException e) {
+                // if the name does not match any known CompareMethod, we init it to the default one
+                filterCompareMethod = CompareMethod.EQUAL_TO;
+            }
+            String value = storageStrings[index++];
+            if (filterValueIndex != -1 && FILTER_VALUE_NULL.equals(value) == false) {
+                filterValue = EventValueType.getObjectFromStorageString(value);
+            }
+
+            return index;
+        }
+
+        /**
+         * Returns the storage string for the receiver.
+         */
+        String getStorageString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append(eventTag);
+            sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
+            sb.append(seriesValueIndex);
+            sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
+            sb.append(Boolean.toString(includePid));
+            sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
+            sb.append(filterValueIndex);
+            sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
+            sb.append(filterCompareMethod.name());
+            sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
+            if (filterValue != null) {
+                String value = EventValueType.getStorageString(filterValue);
+                if (value != null) {
+                    sb.append(value);
+                } else {
+                    sb.append(FILTER_VALUE_NULL);
+                }
+            } else {
+                sb.append(FILTER_VALUE_NULL);
+            }
+
+            return sb.toString();
+        }
+    }
+
+    /**
+     * Describes an event value to be displayed.
+     */
+    static final class ValueDisplayDescriptor extends OccurrenceDisplayDescriptor {
+        String valueName;
+        int valueIndex = -1;
+
+        ValueDisplayDescriptor() {
+            super();
+        }
+
+        ValueDisplayDescriptor(ValueDisplayDescriptor descriptor) {
+            super();
+            replaceWith(descriptor);
+        }
+
+        ValueDisplayDescriptor(int eventTag, String valueName, int valueIndex) {
+            super(eventTag);
+            this.valueName = valueName;
+            this.valueIndex = valueIndex;
+        }
+
+        ValueDisplayDescriptor(int eventTag, String valueName, int valueIndex,
+                int seriesValueIndex) {
+            super(eventTag, seriesValueIndex);
+            this.valueName = valueName;
+            this.valueIndex = valueIndex;
+        }
+
+        @Override
+        void replaceWith(OccurrenceDisplayDescriptor descriptor) {
+            super.replaceWith(descriptor);
+            if (descriptor instanceof ValueDisplayDescriptor) {
+                ValueDisplayDescriptor valueDescriptor = (ValueDisplayDescriptor) descriptor;
+                valueName = valueDescriptor.valueName;
+                valueIndex = valueDescriptor.valueIndex;
+            }
+        }
+
+        /**
+         * Loads the parameters from an array of strings.
+         *
+         * @param storageStrings the strings representing each parameter.
+         * @param index          the starting index in the array of strings.
+         * @return the new index in the array.
+         */
+        @Override
+        protected int loadFrom(String[] storageStrings, int index) {
+            index = super.loadFrom(storageStrings, index);
+            valueName = storageStrings[index++];
+            valueIndex = Integer.parseInt(storageStrings[index++]);
+            return index;
+        }
+
+        /**
+         * Returns the storage string for the receiver.
+         */
+        @Override
+        String getStorageString() {
+            String superStorage = super.getStorageString();
+
+            StringBuilder sb = new StringBuilder();
+            sb.append(superStorage);
+            sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
+            sb.append(valueName);
+            sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
+            sb.append(valueIndex);
+
+            return sb.toString();
+        }
+    }
+
+    /* ==================
+     * Event Display parameters.
+     * ================== */
+    protected String mName;
+
+    private boolean mPidFiltering = false;
+
+    private ArrayList<Integer> mPidFilterList = null;
+
+    protected final ArrayList<ValueDisplayDescriptor> mValueDescriptors =
+            new ArrayList<ValueDisplayDescriptor>();
+    private final ArrayList<OccurrenceDisplayDescriptor> mOccurrenceDescriptors =
+            new ArrayList<OccurrenceDisplayDescriptor>();
+
+    /* ==================
+     * Event Display members for display purpose.
+     * ================== */
+    // chart objects
+    /**
+     * This is a map of (descriptor, map2) where map2 is a map of (pid, chart-series)
+     */
+    protected final HashMap<ValueDisplayDescriptor, HashMap<Integer, TimeSeries>> mValueDescriptorSeriesMap =
+            new HashMap<ValueDisplayDescriptor, HashMap<Integer, TimeSeries>>();
+    /**
+     * This is a map of (descriptor, map2) where map2 is a map of (pid, chart-series)
+     */
+    protected final HashMap<OccurrenceDisplayDescriptor, HashMap<Integer, TimeSeries>> mOcurrenceDescriptorSeriesMap =
+            new HashMap<OccurrenceDisplayDescriptor, HashMap<Integer, TimeSeries>>();
+
+    /**
+     * This is a map of (ValueType, dataset)
+     */
+    protected final HashMap<ValueType, TimeSeriesCollection> mValueTypeDataSetMap =
+            new HashMap<ValueType, TimeSeriesCollection>();
+
+    protected JFreeChart mChart;
+    protected TimeSeriesCollection mOccurrenceDataSet;
+    protected int mDataSetCount;
+    private ChartComposite mChartComposite;
+    protected long mMaximumChartItemAge = -1;
+    protected long mHistWidth = 1;
+
+    // log objects.
+    protected Table mLogTable;
+
+    /* ==================
+     * Misc data.
+     * ================== */
+    protected int mValueDescriptorCheck = EVENT_CHECK_FAILED;
+
+    EventDisplay(String name) {
+        mName = name;
+    }
+
+    static EventDisplay clone(EventDisplay from) {
+        EventDisplay ed = eventDisplayFactory(from.getDisplayType(), from.getName());
+        ed.mName = from.mName;
+        ed.mPidFiltering = from.mPidFiltering;
+        ed.mMaximumChartItemAge = from.mMaximumChartItemAge;
+        ed.mHistWidth = from.mHistWidth;
+
+        if (from.mPidFilterList != null) {
+            ed.mPidFilterList = new ArrayList<Integer>();
+            ed.mPidFilterList.addAll(from.mPidFilterList);
+        }
+
+        for (ValueDisplayDescriptor desc : from.mValueDescriptors) {
+            ed.mValueDescriptors.add(new ValueDisplayDescriptor(desc));
+        }
+        ed.mValueDescriptorCheck = from.mValueDescriptorCheck;
+
+        for (OccurrenceDisplayDescriptor desc : from.mOccurrenceDescriptors) {
+            ed.mOccurrenceDescriptors.add(new OccurrenceDisplayDescriptor(desc));
+        }
+        return ed;
+    }
+
+    /**
+     * Returns the parameters of the receiver as a single String for storage.
+     */
+    String getStorageString() {
+        StringBuilder sb = new StringBuilder();
+
+        sb.append(mName);
+        sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
+        sb.append(getDisplayType());
+        sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
+        sb.append(Boolean.toString(mPidFiltering));
+        sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
+        sb.append(getPidStorageString());
+        sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
+        sb.append(getDescriptorStorageString(mValueDescriptors));
+        sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
+        sb.append(getDescriptorStorageString(mOccurrenceDescriptors));
+        sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
+        sb.append(mMaximumChartItemAge);
+        sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
+        sb.append(mHistWidth);
+        sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
+
+        return sb.toString();
+    }
+
+    void setName(String name) {
+        mName = name;
+    }
+
+    String getName() {
+        return mName;
+    }
+
+    void setPidFiltering(boolean filterByPid) {
+        mPidFiltering = filterByPid;
+    }
+
+    boolean getPidFiltering() {
+        return mPidFiltering;
+    }
+
+    void setPidFilterList(ArrayList<Integer> pids) {
+        if (mPidFiltering == false) {
+            throw new InvalidParameterException();
+        }
+
+        mPidFilterList = pids;
+    }
+
+    ArrayList<Integer> getPidFilterList() {
+        return mPidFilterList;
+    }
+
+    void addPidFiler(int pid) {
+        if (mPidFiltering == false) {
+            throw new InvalidParameterException();
+        }
+
+        if (mPidFilterList == null) {
+            mPidFilterList = new ArrayList<Integer>();
+        }
+
+        mPidFilterList.add(pid);
+    }
+
+    /**
+     * Returns an iterator to the list of {@link ValueDisplayDescriptor}.
+     */
+    Iterator<ValueDisplayDescriptor> getValueDescriptors() {
+        return mValueDescriptors.iterator();
+    }
+
+    /**
+     * Update checks on the descriptors. Must be called whenever a descriptor is modified outside
+     * of this class.
+     */
+    void updateValueDescriptorCheck() {
+        mValueDescriptorCheck = checkDescriptors();
+    }
+
+    /**
+     * Returns an iterator to the list of {@link OccurrenceDisplayDescriptor}.
+     */
+    Iterator<OccurrenceDisplayDescriptor> getOccurrenceDescriptors() {
+        return mOccurrenceDescriptors.iterator();
+    }
+
+    /**
+     * Adds a descriptor. This can be a {@link OccurrenceDisplayDescriptor} or a
+     * {@link ValueDisplayDescriptor}.
+     *
+     * @param descriptor the descriptor to be added.
+     */
+    void addDescriptor(OccurrenceDisplayDescriptor descriptor) {
+        if (descriptor instanceof ValueDisplayDescriptor) {
+            mValueDescriptors.add((ValueDisplayDescriptor) descriptor);
+            mValueDescriptorCheck = checkDescriptors();
+        } else {
+            mOccurrenceDescriptors.add(descriptor);
+        }
+    }
+
+    /**
+     * Returns a descriptor by index and class (extending {@link OccurrenceDisplayDescriptor}).
+     *
+     * @param descriptorClass the class of the descriptor to return.
+     * @param index           the index of the descriptor to return.
+     * @return either a {@link OccurrenceDisplayDescriptor} or a {@link ValueDisplayDescriptor}
+     *         or <code>null</code> if <code>descriptorClass</code> is another class.
+     */
+    OccurrenceDisplayDescriptor getDescriptor(
+            Class<? extends OccurrenceDisplayDescriptor> descriptorClass, int index) {
+
+        if (descriptorClass == OccurrenceDisplayDescriptor.class) {
+            return mOccurrenceDescriptors.get(index);
+        } else if (descriptorClass == ValueDisplayDescriptor.class) {
+            return mValueDescriptors.get(index);
+        }
+
+        return null;
+    }
+
+    /**
+     * Removes a descriptor based on its class and index.
+     *
+     * @param descriptorClass the class of the descriptor.
+     * @param index           the index of the descriptor to be removed.
+     */
+    void removeDescriptor(Class<? extends OccurrenceDisplayDescriptor> descriptorClass, int index) {
+        if (descriptorClass == OccurrenceDisplayDescriptor.class) {
+            mOccurrenceDescriptors.remove(index);
+        } else if (descriptorClass == ValueDisplayDescriptor.class) {
+            mValueDescriptors.remove(index);
+            mValueDescriptorCheck = checkDescriptors();
+        }
+    }
+
+    Control createCompositeChart(final Composite parent, EventLogParser logParser,
+            String title) {
+        mChart = ChartFactory.createTimeSeriesChart(
+                null,
+                null /* timeAxisLabel */,
+                null /* valueAxisLabel */,
+                null, /* dataset. set below */
+                true /* legend */,
+                false /* tooltips */,
+                false /* urls */);
+
+        // get the font to make a proper title. We need to convert the swt font,
+        // into an awt font.
+        Font f = parent.getFont();
+        FontData[] fData = f.getFontData();
+
+        // event though on Mac OS there could be more than one fontData, we'll only use
+        // the first one.
+        FontData firstFontData = fData[0];
+
+        java.awt.Font awtFont = SWTUtils.toAwtFont(parent.getDisplay(),
+                firstFontData, true /* ensureSameSize */);
+
+
+        mChart.setTitle(new TextTitle(title, awtFont));
+
+        final XYPlot xyPlot = mChart.getXYPlot();
+        xyPlot.setRangeCrosshairVisible(true);
+        xyPlot.setRangeCrosshairLockedOnData(true);
+        xyPlot.setDomainCrosshairVisible(true);
+        xyPlot.setDomainCrosshairLockedOnData(true);
+
+        mChart.addChangeListener(new ChartChangeListener() {
+            @Override
+            public void chartChanged(ChartChangeEvent event) {
+                ChartChangeEventType type = event.getType();
+                if (type == ChartChangeEventType.GENERAL) {
+                    // because the value we need (rangeCrosshair and domainCrosshair) are
+                    // updated on the draw, but the notification happens before the draw,
+                    // we process the click in a future runnable!
+                    parent.getDisplay().asyncExec(new Runnable() {
+                        @Override
+                        public void run() {
+                            processClick(xyPlot);
+                        }
+                    });
+                }
+            }
+        });
+
+        mChartComposite = new ChartComposite(parent, SWT.BORDER, mChart,
+                ChartComposite.DEFAULT_WIDTH,
+                ChartComposite.DEFAULT_HEIGHT,
+                ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH,
+                ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT,
+                3000, // max draw width. We don't want it to zoom, so we put a big number
+                3000, // max draw height. We don't want it to zoom, so we put a big number
+                true,  // off-screen buffer
+                true,  // properties
+                true,  // save
+                true,  // print
+                true,  // zoom
+                true);   // tooltips
+
+        mChartComposite.addDisposeListener(new DisposeListener() {
+            @Override
+            public void widgetDisposed(DisposeEvent e) {
+                mValueTypeDataSetMap.clear();
+                mDataSetCount = 0;
+                mOccurrenceDataSet = null;
+                mChart = null;
+                mChartComposite = null;
+                mValueDescriptorSeriesMap.clear();
+                mOcurrenceDescriptorSeriesMap.clear();
+            }
+        });
+
+        return mChartComposite;
+
+    }
+
+    private void processClick(XYPlot xyPlot) {
+        double rangeValue = xyPlot.getRangeCrosshairValue();
+        if (rangeValue != 0) {
+            double domainValue = xyPlot.getDomainCrosshairValue();
+
+            Millisecond msec = new Millisecond(new Date((long) domainValue));
+
+            // look for values in the dataset that contains data at this TimePeriod
+            Set<ValueDisplayDescriptor> descKeys = mValueDescriptorSeriesMap.keySet();
+
+            for (ValueDisplayDescriptor descKey : descKeys) {
+                HashMap<Integer, TimeSeries> map = mValueDescriptorSeriesMap.get(descKey);
+
+                Set<Integer> pidKeys = map.keySet();
+
+                for (Integer pidKey : pidKeys) {
+                    TimeSeries series = map.get(pidKey);
+
+                    Number value = series.getValue(msec);
+                    if (value != null) {
+                        // found a match. lets check against the actual value.
+                        if (value.doubleValue() == rangeValue) {
+
+                            return;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+
+    /**
+     * Resizes the <code>index</code>-th column of the log {@link Table} (if applicable).
+     * Subclasses can override if necessary.
+     * <p/>
+     * This does nothing if the <code>Table</code> object is <code>null</code> (because the display
+     * type does not use a column) or if the <code>index</code>-th column is in fact the originating
+     * column passed as argument.
+     *
+     * @param index        the index of the column to resize
+     * @param sourceColumn the original column that was resize, and on which we need to sync the
+     *                     index-th column width.
+     */
+    void resizeColumn(int index, TableColumn sourceColumn) {
+    }
+
+    /**
+     * Sets the current {@link EventLogParser} object.
+     * Subclasses can override if necessary.
+     */
+    protected void setNewLogParser(EventLogParser logParser) {
+    }
+
+    /**
+     * Prepares the {@link EventDisplay} for a multi event display.
+     */
+    void startMultiEventDisplay() {
+        if (mLogTable != null) {
+            mLogTable.setRedraw(false);
+        }
+    }
+
+    /**
+     * Finalizes the {@link EventDisplay} after a multi event display.
+     */
+    void endMultiEventDisplay() {
+        if (mLogTable != null) {
+            mLogTable.setRedraw(true);
+        }
+    }
+
+    /**
+     * Returns the {@link Table} object used to display events, if any.
+     *
+     * @return a Table object or <code>null</code>.
+     */
+    Table getTable() {
+        return mLogTable;
+    }
+
+    /**
+     * Loads a new {@link EventDisplay} from a storage string. The string must have been created
+     * with {@link #getStorageString()}.
+     *
+     * @param storageString the storage string
+     * @return a new {@link EventDisplay} or null if the load failed.
+     */
+    static EventDisplay load(String storageString) {
+        if (storageString.length() > 0) {
+            // the storage string is separated by ':'
+            String[] values = storageString.split(Pattern.quote(DISPLAY_DATA_STORAGE_SEPARATOR));
+
+            try {
+                int index = 0;
+
+                String name = values[index++];
+                int displayType = Integer.parseInt(values[index++]);
+                boolean pidFiltering = Boolean.parseBoolean(values[index++]);
+
+                EventDisplay ed = eventDisplayFactory(displayType, name);
+                ed.setPidFiltering(pidFiltering);
+
+                // because empty sections are removed by String.split(), we have to check
+                // the index for those.
+                if (index < values.length) {
+                    ed.loadPidFilters(values[index++]);
+                }
+
+                if (index < values.length) {
+                    ed.loadValueDescriptors(values[index++]);
+                }
+
+                if (index < values.length) {
+                    ed.loadOccurrenceDescriptors(values[index++]);
+                }
+
+                ed.updateValueDescriptorCheck();
+
+                if (index < values.length) {
+                    ed.mMaximumChartItemAge = Long.parseLong(values[index++]);
+                }
+
+                if (index < values.length) {
+                    ed.mHistWidth = Long.parseLong(values[index++]);
+                }
+
+                return ed;
+            } catch (RuntimeException re) {
+                // we'll return null below.
+                Log.e("ddms", re);
+            }
+        }
+
+        return null;
+    }
+
+    private String getPidStorageString() {
+        if (mPidFilterList != null) {
+            StringBuilder sb = new StringBuilder();
+            boolean first = true;
+            for (Integer i : mPidFilterList) {
+                if (first == false) {
+                    sb.append(PID_STORAGE_SEPARATOR);
+                } else {
+                    first = false;
+                }
+                sb.append(i);
+            }
+
+            return sb.toString();
+        }
+        return ""; //$NON-NLS-1$
+    }
+
+
+    private void loadPidFilters(String storageString) {
+        if (storageString.length() > 0) {
+            String[] values = storageString.split(Pattern.quote(PID_STORAGE_SEPARATOR));
+
+            for (String value : values) {
+                if (mPidFilterList == null) {
+                    mPidFilterList = new ArrayList<Integer>();
+                }
+                mPidFilterList.add(Integer.parseInt(value));
+            }
+        }
+    }
+
+    private String getDescriptorStorageString(
+            ArrayList<? extends OccurrenceDisplayDescriptor> descriptorList) {
+        StringBuilder sb = new StringBuilder();
+        boolean first = true;
+
+        for (OccurrenceDisplayDescriptor descriptor : descriptorList) {
+            if (first == false) {
+                sb.append(DESCRIPTOR_STORAGE_SEPARATOR);
+            } else {
+                first = false;
+            }
+            sb.append(descriptor.getStorageString());
+        }
+
+        return sb.toString();
+    }
+
+    private void loadOccurrenceDescriptors(String storageString) {
+        if (storageString.length() == 0) {
+            return;
+        }
+
+        String[] values = storageString.split(Pattern.quote(DESCRIPTOR_STORAGE_SEPARATOR));
+
+        for (String value : values) {
+            OccurrenceDisplayDescriptor desc = new OccurrenceDisplayDescriptor();
+            desc.loadFrom(value);
+            mOccurrenceDescriptors.add(desc);
+        }
+    }
+
+    private void loadValueDescriptors(String storageString) {
+        if (storageString.length() == 0) {
+            return;
+        }
+
+        String[] values = storageString.split(Pattern.quote(DESCRIPTOR_STORAGE_SEPARATOR));
+
+        for (String value : values) {
+            ValueDisplayDescriptor desc = new ValueDisplayDescriptor();
+            desc.loadFrom(value);
+            mValueDescriptors.add(desc);
+        }
+    }
+
+    /**
+     * Fills a list with {@link OccurrenceDisplayDescriptor} (or a subclass of it) from another
+     * list if they are configured to display the {@link EventContainer}
+     *
+     * @param event    the event container
+     * @param fullList the list with all the descriptors.
+     * @param outList  the list to fill.
+     */
+    @SuppressWarnings("unchecked")
+    private void getDescriptors(EventContainer event,
+            ArrayList<? extends OccurrenceDisplayDescriptor> fullList,
+            ArrayList outList) {
+        for (OccurrenceDisplayDescriptor descriptor : fullList) {
+            try {
+                // first check the event tag.
+                if (descriptor.eventTag == event.mTag) {
+                    // now check if we have a filter on a value
+                    if (descriptor.filterValueIndex == -1 ||
+                            event.testValue(descriptor.filterValueIndex, descriptor.filterValue,
+                                    descriptor.filterCompareMethod)) {
+                        outList.add(descriptor);
+                    }
+                }
+            } catch (InvalidTypeException ite) {
+                // if the filter for the descriptor was incorrect, we ignore the descriptor.
+            } catch (ArrayIndexOutOfBoundsException aioobe) {
+                // if the index was wrong (the event content may have changed since we setup the
+                // display), we do nothing but log the error
+                Log.e("Event Log", String.format(
+                        "ArrayIndexOutOfBoundsException occured when checking %1$d-th value of event %2$d", //$NON-NLS-1$
+                        descriptor.filterValueIndex, descriptor.eventTag));
+            }
+        }
+    }
+
+    /**
+     * Filters the {@link com.android.ddmlib.log.EventContainer}, and fills two list of {@link com.android.ddmuilib.log.event.EventDisplay.ValueDisplayDescriptor}
+     * and {@link com.android.ddmuilib.log.event.EventDisplay.OccurrenceDisplayDescriptor} configured to display the event.
+     *
+     * @param event
+     * @param valueDescriptors
+     * @param occurrenceDescriptors
+     * @return true if the event should be displayed.
+     */
+
+    protected boolean filterEvent(EventContainer event,
+            ArrayList<ValueDisplayDescriptor> valueDescriptors,
+            ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors) {
+
+        // test the pid first (if needed)
+        if (mPidFiltering && mPidFilterList != null) {
+            boolean found = false;
+            for (int pid : mPidFilterList) {
+                if (pid == event.pid) {
+                    found = true;
+                    break;
+                }
+            }
+
+            if (found == false) {
+                return false;
+            }
+        }
+
+        // now get the list of matching descriptors
+        getDescriptors(event, mValueDescriptors, valueDescriptors);
+        getDescriptors(event, mOccurrenceDescriptors, occurrenceDescriptors);
+
+        // and return whether there is at least one match in either list.
+        return (valueDescriptors.size() > 0 || occurrenceDescriptors.size() > 0);
+    }
+
+    /**
+     * Checks all the {@link ValueDisplayDescriptor} for similarity.
+     * If all the event values are from the same tag, the method will return EVENT_CHECK_SAME_TAG.
+     * If all the event/value are the same, the method will return EVENT_CHECK_SAME_VALUE
+     *
+     * @return flag as described above
+     */
+    private int checkDescriptors() {
+        if (mValueDescriptors.size() < 2) {
+            return EVENT_CHECK_SAME_VALUE;
+        }
+
+        int tag = -1;
+        int index = -1;
+        for (ValueDisplayDescriptor display : mValueDescriptors) {
+            if (tag == -1) {
+                tag = display.eventTag;
+                index = display.valueIndex;
+            } else {
+                if (tag != display.eventTag) {
+                    return EVENT_CHECK_FAILED;
+                } else {
+                    if (index != -1) {
+                        if (index != display.valueIndex) {
+                            index = -1;
+                        }
+                    }
+                }
+            }
+        }
+
+        if (index == -1) {
+            return EVENT_CHECK_SAME_TAG;
+        }
+
+        return EVENT_CHECK_SAME_VALUE;
+    }
+
+    /**
+     * Resets the time limit on the chart to be infinite.
+     */
+    void resetChartTimeLimit() {
+        mMaximumChartItemAge = -1;
+    }
+
+    /**
+     * Sets the time limit on the charts.
+     *
+     * @param timeLimit the time limit in seconds.
+     */
+    void setChartTimeLimit(long timeLimit) {
+        mMaximumChartItemAge = timeLimit;
+    }
+
+    long getChartTimeLimit() {
+        return mMaximumChartItemAge;
+    }
+
+    /**
+     * m
+     * Resets the histogram width
+     */
+    void resetHistWidth() {
+        mHistWidth = 1;
+    }
+
+    /**
+     * Sets the histogram width
+     *
+     * @param histWidth the width in hours
+     */
+    void setHistWidth(long histWidth) {
+        mHistWidth = histWidth;
+    }
+
+    long getHistWidth() {
+        return mHistWidth;
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplayOptions.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplayOptions.java
new file mode 100644
index 0000000..b13f3f4
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplayOptions.java
@@ -0,0 +1,961 @@
+/*
+ * Copyright (C) 2008 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.ddmuilib.log.event;
+
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventLogParser;
+import com.android.ddmlib.log.EventValueDescription;
+import com.android.ddmuilib.DdmUiPreferences;
+import com.android.ddmuilib.ImageLoader;
+import com.android.ddmuilib.log.event.EventDisplay.OccurrenceDisplayDescriptor;
+import com.android.ddmuilib.log.event.EventDisplay.ValueDisplayDescriptor;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.List;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Map;
+
+class EventDisplayOptions  extends Dialog {
+    private static final int DLG_WIDTH = 700;
+    private static final int DLG_HEIGHT = 700;
+
+    private Shell mParent;
+    private Shell mShell;
+
+    private boolean mEditStatus = false;
+    private final ArrayList<EventDisplay> mDisplayList = new ArrayList<EventDisplay>();
+
+    /* LEFT LIST */
+    private List mEventDisplayList;
+    private Button mEventDisplayNewButton;
+    private Button mEventDisplayDeleteButton;
+    private Button mEventDisplayUpButton;
+    private Button mEventDisplayDownButton;
+    private Text mDisplayWidthText;
+    private Text mDisplayHeightText;
+
+    /* WIDGETS ON THE RIGHT */
+    private Text mDisplayNameText;
+    private Combo mDisplayTypeCombo;
+    private Group mChartOptions;
+    private Group mHistOptions;
+    private Button mPidFilterCheckBox;
+    private Text mPidText;
+
+    /** Map with (event-tag, event name) */
+    private Map<Integer, String> mEventTagMap;
+
+    /** Map with (event-tag, array of value info for the event) */
+    private Map<Integer, EventValueDescription[]> mEventDescriptionMap;
+
+    /** list of current pids */
+    private ArrayList<Integer> mPidList;
+
+    private EventLogParser mLogParser;
+
+    private Group mInfoGroup;
+
+    private static class SelectionWidgets {
+        private List mList;
+        private Button mNewButton;
+        private Button mEditButton;
+        private Button mDeleteButton;
+
+        private void setEnabled(boolean enable) {
+            mList.setEnabled(enable);
+            mNewButton.setEnabled(enable);
+            mEditButton.setEnabled(enable);
+            mDeleteButton.setEnabled(enable);
+        }
+    }
+
+    private SelectionWidgets mValueSelection;
+    private SelectionWidgets mOccurrenceSelection;
+
+    /** flag to temporarly disable processing of {@link Text} changes, so that
+     * {@link Text#setText(String)} can be called safely. */
+    private boolean mProcessTextChanges = true;
+    private Text mTimeLimitText;
+    private Text mHistWidthText;
+
+    EventDisplayOptions(Shell parent) {
+        super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL);
+    }
+
+    /**
+     * Opens the display option dialog, to edit the {@link EventDisplay} objects provided in the
+     * list.
+     * @param logParser
+     * @param displayList
+     * @param eventList
+     * @return true if the list of {@link EventDisplay} objects was updated.
+     */
+    boolean open(EventLogParser logParser, ArrayList<EventDisplay> displayList,
+            ArrayList<EventContainer> eventList) {
+        mLogParser = logParser;
+
+        if (logParser != null) {
+            // we need 2 things from the parser.
+            // the event tag / event name map
+            mEventTagMap = logParser.getTagMap();
+
+            // the event info map
+            mEventDescriptionMap = logParser.getEventInfoMap();
+        }
+
+        // make a copy of the EventDisplay list since we'll use working copies.
+        duplicateEventDisplay(displayList);
+
+        // build a list of pid from the list of events.
+        buildPidList(eventList);
+
+        createUI();
+
+        if (mParent == null || mShell == null) {
+            return false;
+        }
+
+        // Set the dialog size.
+        mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT);
+        Rectangle r = mParent.getBounds();
+        // get the center new top left.
+        int cx = r.x + r.width/2;
+        int x = cx - DLG_WIDTH / 2;
+        int cy = r.y + r.height/2;
+        int y = cy - DLG_HEIGHT / 2;
+        mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT);
+
+        mShell.layout();
+
+        // actually open the dialog
+        mShell.open();
+
+        // event loop until the dialog is closed.
+        Display display = mParent.getDisplay();
+        while (!mShell.isDisposed()) {
+            if (!display.readAndDispatch())
+                display.sleep();
+        }
+
+        return mEditStatus;
+    }
+
+    ArrayList<EventDisplay> getEventDisplays() {
+        return mDisplayList;
+    }
+
+    private void createUI() {
+        mParent = getParent();
+        mShell = new Shell(mParent, getStyle());
+        mShell.setText("Event Display Configuration");
+
+        mShell.setLayout(new GridLayout(1, true));
+
+        final Composite topPanel = new Composite(mShell, SWT.NONE);
+        topPanel.setLayoutData(new GridData(GridData.FILL_BOTH));
+        topPanel.setLayout(new GridLayout(2, false));
+
+        // create the tree on the left and the controls on the right.
+        Composite leftPanel = new Composite(topPanel, SWT.NONE);
+        Composite rightPanel = new Composite(topPanel, SWT.NONE);
+
+        createLeftPanel(leftPanel);
+        createRightPanel(rightPanel);
+
+        mShell.addListener(SWT.Close, new Listener() {
+            @Override
+            public void handleEvent(Event event) {
+                event.doit = true;
+            }
+        });
+
+        Label separator = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL);
+        separator.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        Composite bottomButtons = new Composite(mShell, SWT.NONE);
+        bottomButtons.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        GridLayout gl;
+        bottomButtons.setLayout(gl = new GridLayout(2, true));
+        gl.marginHeight = gl.marginWidth = 0;
+
+        Button okButton = new Button(bottomButtons, SWT.PUSH);
+        okButton.setText("OK");
+        okButton.addSelectionListener(new SelectionAdapter() {
+            /* (non-Javadoc)
+             * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+             */
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                mShell.close();
+            }
+        });
+
+        Button cancelButton = new Button(bottomButtons, SWT.PUSH);
+        cancelButton.setText("Cancel");
+        cancelButton.addSelectionListener(new SelectionAdapter() {
+            /* (non-Javadoc)
+             * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+             */
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                // cancel the modification flag.
+                mEditStatus = false;
+
+                // and close
+                mShell.close();
+            }
+        });
+
+        enable(false);
+
+        // fill the list with the current display
+        fillEventDisplayList();
+    }
+
+    private void createLeftPanel(Composite leftPanel) {
+        final IPreferenceStore store = DdmUiPreferences.getStore();
+
+        GridLayout gl;
+
+        leftPanel.setLayoutData(new GridData(GridData.FILL_VERTICAL));
+        leftPanel.setLayout(gl = new GridLayout(1, false));
+        gl.verticalSpacing = 1;
+
+        mEventDisplayList = new List(leftPanel,
+                SWT.BORDER | SWT.SINGLE | SWT.V_SCROLL | SWT.FULL_SELECTION);
+        mEventDisplayList.setLayoutData(new GridData(GridData.FILL_BOTH));
+        mEventDisplayList.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                handleEventDisplaySelection();
+            }
+        });
+
+        Composite bottomControls = new Composite(leftPanel, SWT.NONE);
+        bottomControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        bottomControls.setLayout(gl = new GridLayout(5, false));
+        gl.marginHeight = gl.marginWidth = 0;
+        gl.verticalSpacing = 0;
+        gl.horizontalSpacing = 0;
+
+        ImageLoader loader = ImageLoader.getDdmUiLibLoader();
+        mEventDisplayNewButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT);
+        mEventDisplayNewButton.setImage(loader.loadImage("add.png", //$NON-NLS-1$
+                leftPanel.getDisplay()));
+        mEventDisplayNewButton.setToolTipText("Adds a new event display");
+        mEventDisplayNewButton.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER));
+        mEventDisplayNewButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                createNewEventDisplay();
+            }
+        });
+
+        mEventDisplayDeleteButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT);
+        mEventDisplayDeleteButton.setImage(loader.loadImage("delete.png", //$NON-NLS-1$
+                leftPanel.getDisplay()));
+        mEventDisplayDeleteButton.setToolTipText("Deletes the selected event display");
+        mEventDisplayDeleteButton.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER));
+        mEventDisplayDeleteButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                deleteEventDisplay();
+            }
+        });
+
+        mEventDisplayUpButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT);
+        mEventDisplayUpButton.setImage(loader.loadImage("up.png", //$NON-NLS-1$
+                leftPanel.getDisplay()));
+        mEventDisplayUpButton.setToolTipText("Moves the selected event display up");
+        mEventDisplayUpButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                // get current selection.
+                int selection = mEventDisplayList.getSelectionIndex();
+                if (selection > 0) {
+                    // update the list of EventDisplay.
+                    EventDisplay display = mDisplayList.remove(selection);
+                    mDisplayList.add(selection - 1, display);
+
+                    // update the list widget
+                    mEventDisplayList.remove(selection);
+                    mEventDisplayList.add(display.getName(), selection - 1);
+
+                    // update the selection and reset the ui.
+                    mEventDisplayList.select(selection - 1);
+                    handleEventDisplaySelection();
+                    mEventDisplayList.showSelection();
+
+                    setModified();
+                }
+            }
+        });
+
+        mEventDisplayDownButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT);
+        mEventDisplayDownButton.setImage(loader.loadImage("down.png", //$NON-NLS-1$
+                leftPanel.getDisplay()));
+        mEventDisplayDownButton.setToolTipText("Moves the selected event display down");
+        mEventDisplayDownButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                // get current selection.
+                int selection = mEventDisplayList.getSelectionIndex();
+                if (selection != -1 && selection < mEventDisplayList.getItemCount() - 1) {
+                    // update the list of EventDisplay.
+                    EventDisplay display = mDisplayList.remove(selection);
+                    mDisplayList.add(selection + 1, display);
+
+                    // update the list widget
+                    mEventDisplayList.remove(selection);
+                    mEventDisplayList.add(display.getName(), selection + 1);
+
+                    // update the selection and reset the ui.
+                    mEventDisplayList.select(selection + 1);
+                    handleEventDisplaySelection();
+                    mEventDisplayList.showSelection();
+
+                    setModified();
+                }
+            }
+        });
+
+        Group sizeGroup = new Group(leftPanel, SWT.NONE);
+        sizeGroup.setText("Display Size:");
+        sizeGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        sizeGroup.setLayout(new GridLayout(2, false));
+
+        Label l = new Label(sizeGroup, SWT.NONE);
+        l.setText("Width:");
+
+        mDisplayWidthText = new Text(sizeGroup, SWT.LEFT | SWT.SINGLE | SWT.BORDER);
+        mDisplayWidthText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mDisplayWidthText.setText(Integer.toString(
+                store.getInt(EventLogPanel.PREFS_DISPLAY_WIDTH)));
+        mDisplayWidthText.addModifyListener(new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent e) {
+                String text = mDisplayWidthText.getText().trim();
+                try {
+                    store.setValue(EventLogPanel.PREFS_DISPLAY_WIDTH, Integer.parseInt(text));
+                    setModified();
+                } catch (NumberFormatException nfe) {
+                    // do something?
+                }
+            }
+        });
+
+        l = new Label(sizeGroup, SWT.NONE);
+        l.setText("Height:");
+
+        mDisplayHeightText = new Text(sizeGroup, SWT.LEFT | SWT.SINGLE | SWT.BORDER);
+        mDisplayHeightText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mDisplayHeightText.setText(Integer.toString(
+                store.getInt(EventLogPanel.PREFS_DISPLAY_HEIGHT)));
+        mDisplayHeightText.addModifyListener(new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent e) {
+                String text = mDisplayHeightText.getText().trim();
+                try {
+                    store.setValue(EventLogPanel.PREFS_DISPLAY_HEIGHT, Integer.parseInt(text));
+                    setModified();
+                } catch (NumberFormatException nfe) {
+                    // do something?
+                }
+            }
+        });
+    }
+
+    private void createRightPanel(Composite rightPanel) {
+        rightPanel.setLayout(new GridLayout(1, true));
+        rightPanel.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        mInfoGroup = new Group(rightPanel, SWT.NONE);
+        mInfoGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mInfoGroup.setLayout(new GridLayout(2, false));
+
+        Label nameLabel = new Label(mInfoGroup, SWT.LEFT);
+        nameLabel.setText("Name:");
+
+        mDisplayNameText = new Text(mInfoGroup, SWT.BORDER | SWT.LEFT | SWT.SINGLE);
+        mDisplayNameText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mDisplayNameText.addModifyListener(new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent e) {
+                if (mProcessTextChanges) {
+                    EventDisplay eventDisplay = getCurrentEventDisplay();
+                    if (eventDisplay != null) {
+                        eventDisplay.setName(mDisplayNameText.getText());
+                        int index = mEventDisplayList.getSelectionIndex();
+                        mEventDisplayList.remove(index);
+                        mEventDisplayList.add(eventDisplay.getName(), index);
+                        mEventDisplayList.select(index);
+                        handleEventDisplaySelection();
+                        setModified();
+                    }
+                }
+            }
+        });
+
+        Label displayLabel = new Label(mInfoGroup, SWT.LEFT);
+        displayLabel.setText("Type:");
+
+        mDisplayTypeCombo = new Combo(mInfoGroup, SWT.READ_ONLY | SWT.DROP_DOWN);
+        mDisplayTypeCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        // add the combo values. This must match the values EventDisplay.DISPLAY_TYPE_*
+        mDisplayTypeCombo.add("Log All");
+        mDisplayTypeCombo.add("Filtered Log");
+        mDisplayTypeCombo.add("Graph");
+        mDisplayTypeCombo.add("Sync");
+        mDisplayTypeCombo.add("Sync Histogram");
+        mDisplayTypeCombo.add("Sync Performance");
+        mDisplayTypeCombo.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                EventDisplay eventDisplay = getCurrentEventDisplay();
+                if (eventDisplay != null && eventDisplay.getDisplayType() != mDisplayTypeCombo.getSelectionIndex()) {
+                    /* Replace the EventDisplay object with a different subclass */
+                    setModified();
+                    String name = eventDisplay.getName();
+                    EventDisplay newEventDisplay = EventDisplay.eventDisplayFactory(mDisplayTypeCombo.getSelectionIndex(), name);
+                    setCurrentEventDisplay(newEventDisplay);
+                    fillUiWith(newEventDisplay);
+                }
+            }
+        });
+
+        mChartOptions = new Group(mInfoGroup, SWT.NONE);
+        mChartOptions.setText("Chart Options");
+        GridData gd;
+        mChartOptions.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+        gd.horizontalSpan = 2;
+        mChartOptions.setLayout(new GridLayout(2, false));
+
+        Label l = new Label(mChartOptions, SWT.NONE);
+        l.setText("Time Limit (seconds):");
+
+        mTimeLimitText = new Text(mChartOptions, SWT.BORDER);
+        mTimeLimitText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mTimeLimitText.addModifyListener(new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent arg0) {
+                String text = mTimeLimitText.getText().trim();
+                EventDisplay eventDisplay = getCurrentEventDisplay();
+                if (eventDisplay != null) {
+                    try {
+                        if (text.length() == 0) {
+                            eventDisplay.resetChartTimeLimit();
+                        } else {
+                            eventDisplay.setChartTimeLimit(Long.parseLong(text));
+                        }
+                    } catch (NumberFormatException nfe) {
+                        eventDisplay.resetChartTimeLimit();
+                    } finally {
+                        setModified();
+                    }
+                }
+            }
+        });
+
+        mHistOptions = new Group(mInfoGroup, SWT.NONE);
+        mHistOptions.setText("Histogram Options");
+        GridData gdh;
+        mHistOptions.setLayoutData(gdh = new GridData(GridData.FILL_HORIZONTAL));
+        gdh.horizontalSpan = 2;
+        mHistOptions.setLayout(new GridLayout(2, false));
+
+        Label lh = new Label(mHistOptions, SWT.NONE);
+        lh.setText("Histogram width (hours):");
+
+        mHistWidthText = new Text(mHistOptions, SWT.BORDER);
+        mHistWidthText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mHistWidthText.addModifyListener(new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent arg0) {
+                String text = mHistWidthText.getText().trim();
+                EventDisplay eventDisplay = getCurrentEventDisplay();
+                if (eventDisplay != null) {
+                    try {
+                        if (text.length() == 0) {
+                            eventDisplay.resetHistWidth();
+                        } else {
+                            eventDisplay.setHistWidth(Long.parseLong(text));
+                        }
+                    } catch (NumberFormatException nfe) {
+                        eventDisplay.resetHistWidth();
+                    } finally {
+                        setModified();
+                    }
+                }
+            }
+        });
+
+        mPidFilterCheckBox = new Button(mInfoGroup, SWT.CHECK);
+        mPidFilterCheckBox.setText("Enable filtering by pid");
+        mPidFilterCheckBox.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+        gd.horizontalSpan = 2;
+        mPidFilterCheckBox.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                EventDisplay eventDisplay = getCurrentEventDisplay();
+                if (eventDisplay != null) {
+                    eventDisplay.setPidFiltering(mPidFilterCheckBox.getSelection());
+                    mPidText.setEnabled(mPidFilterCheckBox.getSelection());
+                    setModified();
+                }
+            }
+        });
+
+        Label pidLabel = new Label(mInfoGroup, SWT.NONE);
+        pidLabel.setText("Pid Filter:");
+        pidLabel.setToolTipText("Enter all pids, separated by commas");
+
+        mPidText = new Text(mInfoGroup, SWT.BORDER);
+        mPidText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mPidText.addModifyListener(new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent e) {
+                if (mProcessTextChanges) {
+                    EventDisplay eventDisplay = getCurrentEventDisplay();
+                    if (eventDisplay != null && eventDisplay.getPidFiltering()) {
+                        String pidText = mPidText.getText().trim();
+                        String[] pids = pidText.split("\\s*,\\s*"); //$NON-NLS-1$
+
+                        ArrayList<Integer> list = new ArrayList<Integer>();
+                        for (String pid : pids) {
+                            try {
+                                list.add(Integer.valueOf(pid));
+                            } catch (NumberFormatException nfe) {
+                                // just ignore non valid pid
+                            }
+                        }
+
+                        eventDisplay.setPidFilterList(list);
+                        setModified();
+                    }
+                }
+            }
+        });
+
+        /* ------------------
+         * EVENT VALUE/OCCURRENCE SELECTION
+         * ------------------ */
+        mValueSelection = createEventSelection(rightPanel, ValueDisplayDescriptor.class,
+                "Event Value Display");
+        mOccurrenceSelection = createEventSelection(rightPanel, OccurrenceDisplayDescriptor.class,
+                "Event Occurrence Display");
+    }
+
+    private SelectionWidgets createEventSelection(Composite rightPanel,
+            final Class<? extends OccurrenceDisplayDescriptor> descriptorClass,
+            String groupMessage) {
+
+        Group eventSelectionPanel = new Group(rightPanel, SWT.NONE);
+        eventSelectionPanel.setLayoutData(new GridData(GridData.FILL_BOTH));
+        GridLayout gl;
+        eventSelectionPanel.setLayout(gl = new GridLayout(2, false));
+        gl.marginHeight = gl.marginWidth = 0;
+        eventSelectionPanel.setText(groupMessage);
+
+        final SelectionWidgets widgets = new SelectionWidgets();
+
+        widgets.mList = new List(eventSelectionPanel, SWT.BORDER | SWT.SINGLE | SWT.V_SCROLL);
+        widgets.mList.setLayoutData(new GridData(GridData.FILL_BOTH));
+        widgets.mList.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                int index = widgets.mList.getSelectionIndex();
+                if (index != -1) {
+                    widgets.mDeleteButton.setEnabled(true);
+                    widgets.mEditButton.setEnabled(true);
+                } else {
+                    widgets.mDeleteButton.setEnabled(false);
+                    widgets.mEditButton.setEnabled(false);
+                }
+            }
+        });
+
+        Composite rightControls = new Composite(eventSelectionPanel, SWT.NONE);
+        rightControls.setLayoutData(new GridData(GridData.FILL_VERTICAL));
+        rightControls.setLayout(gl = new GridLayout(1, false));
+        gl.marginHeight = gl.marginWidth = 0;
+        gl.verticalSpacing = 0;
+        gl.horizontalSpacing = 0;
+
+        widgets.mNewButton = new Button(rightControls, SWT.PUSH | SWT.FLAT);
+        widgets.mNewButton.setText("New...");
+        widgets.mNewButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        widgets.mNewButton.setEnabled(false);
+        widgets.mNewButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                // current event
+                try {
+                    EventDisplay eventDisplay = getCurrentEventDisplay();
+                    if (eventDisplay != null) {
+                        EventValueSelector dialog = new EventValueSelector(mShell);
+                        if (dialog.open(descriptorClass, mLogParser)) {
+                            eventDisplay.addDescriptor(dialog.getDescriptor());
+                            fillUiWith(eventDisplay);
+                            setModified();
+                        }
+                    }
+                } catch (Exception e1) {
+                    e1.printStackTrace();
+                }
+            }
+        });
+
+        widgets.mEditButton = new Button(rightControls, SWT.PUSH | SWT.FLAT);
+        widgets.mEditButton.setText("Edit...");
+        widgets.mEditButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        widgets.mEditButton.setEnabled(false);
+        widgets.mEditButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                // current event
+                EventDisplay eventDisplay = getCurrentEventDisplay();
+                if (eventDisplay != null) {
+                    // get the current descriptor index
+                    int index = widgets.mList.getSelectionIndex();
+                    if (index != -1) {
+                        // get the descriptor itself
+                        OccurrenceDisplayDescriptor descriptor = eventDisplay.getDescriptor(
+                                descriptorClass, index);
+
+                        // open the edit dialog.
+                        EventValueSelector dialog = new EventValueSelector(mShell);
+                        if (dialog.open(descriptor, mLogParser)) {
+                            descriptor.replaceWith(dialog.getDescriptor());
+                            eventDisplay.updateValueDescriptorCheck();
+                            fillUiWith(eventDisplay);
+
+                            // reselect the item since fillUiWith remove the selection.
+                            widgets.mList.select(index);
+                            widgets.mList.notifyListeners(SWT.Selection, null);
+
+                            setModified();
+                        }
+                    }
+                }
+            }
+        });
+
+        widgets.mDeleteButton = new Button(rightControls, SWT.PUSH | SWT.FLAT);
+        widgets.mDeleteButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        widgets.mDeleteButton.setText("Delete");
+        widgets.mDeleteButton.setEnabled(false);
+        widgets.mDeleteButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                // current event
+                EventDisplay eventDisplay = getCurrentEventDisplay();
+                if (eventDisplay != null) {
+                    // get the current descriptor index
+                    int index = widgets.mList.getSelectionIndex();
+                    if (index != -1) {
+                        eventDisplay.removeDescriptor(descriptorClass, index);
+                        fillUiWith(eventDisplay);
+                        setModified();
+                    }
+                }
+            }
+        });
+
+        return widgets;
+    }
+
+
+    private void duplicateEventDisplay(ArrayList<EventDisplay> displayList) {
+        for (EventDisplay eventDisplay : displayList) {
+            mDisplayList.add(EventDisplay.clone(eventDisplay));
+        }
+    }
+
+    private void buildPidList(ArrayList<EventContainer> eventList) {
+        mPidList = new ArrayList<Integer>();
+        for (EventContainer event : eventList) {
+            if (mPidList.indexOf(event.pid) == -1) {
+                mPidList.add(event.pid);
+            }
+        }
+    }
+
+    private void setModified() {
+        mEditStatus = true;
+    }
+
+
+    private void enable(boolean status) {
+        mEventDisplayDeleteButton.setEnabled(status);
+
+        // enable up/down
+        int selection = mEventDisplayList.getSelectionIndex();
+        int count = mEventDisplayList.getItemCount();
+        mEventDisplayUpButton.setEnabled(status && selection > 0);
+        mEventDisplayDownButton.setEnabled(status && selection != -1 && selection < count - 1);
+
+        mDisplayNameText.setEnabled(status);
+        mDisplayTypeCombo.setEnabled(status);
+        mPidFilterCheckBox.setEnabled(status);
+
+        mValueSelection.setEnabled(status);
+        mOccurrenceSelection.setEnabled(status);
+        mValueSelection.mNewButton.setEnabled(status);
+        mOccurrenceSelection.mNewButton.setEnabled(status);
+        if (status == false) {
+            mPidText.setEnabled(false);
+        }
+    }
+
+    private void fillEventDisplayList() {
+        for (EventDisplay eventDisplay : mDisplayList) {
+            mEventDisplayList.add(eventDisplay.getName());
+        }
+    }
+
+    private void createNewEventDisplay() {
+        int count = mDisplayList.size();
+
+        String name = String.format("display %1$d", count + 1);
+
+        EventDisplay eventDisplay = EventDisplay.eventDisplayFactory(0 /* type*/, name);
+
+        mDisplayList.add(eventDisplay);
+        mEventDisplayList.add(name);
+
+        mEventDisplayList.select(count);
+        handleEventDisplaySelection();
+        mEventDisplayList.showSelection();
+
+        setModified();
+    }
+
+    private void deleteEventDisplay() {
+        int selection = mEventDisplayList.getSelectionIndex();
+        if (selection != -1) {
+            mDisplayList.remove(selection);
+            mEventDisplayList.remove(selection);
+            if (mDisplayList.size() < selection) {
+                selection--;
+            }
+            mEventDisplayList.select(selection);
+            handleEventDisplaySelection();
+
+            setModified();
+        }
+    }
+
+    private EventDisplay getCurrentEventDisplay() {
+        int selection = mEventDisplayList.getSelectionIndex();
+        if (selection != -1) {
+            return mDisplayList.get(selection);
+        }
+
+        return null;
+    }
+
+    private void setCurrentEventDisplay(EventDisplay eventDisplay) {
+        int selection = mEventDisplayList.getSelectionIndex();
+        if (selection != -1) {
+            mDisplayList.set(selection, eventDisplay);
+        }
+    }
+
+    private void handleEventDisplaySelection() {
+        EventDisplay eventDisplay = getCurrentEventDisplay();
+        if (eventDisplay != null) {
+            // enable the UI
+            enable(true);
+
+            // and fill it
+            fillUiWith(eventDisplay);
+        } else {
+            // disable the UI
+            enable(false);
+
+            // and empty it.
+            emptyUi();
+        }
+    }
+
+    private void emptyUi() {
+        mDisplayNameText.setText("");
+        mDisplayTypeCombo.clearSelection();
+        mValueSelection.mList.removeAll();
+        mOccurrenceSelection.mList.removeAll();
+    }
+
+    private void fillUiWith(EventDisplay eventDisplay) {
+        mProcessTextChanges = false;
+
+        mDisplayNameText.setText(eventDisplay.getName());
+        int displayMode = eventDisplay.getDisplayType();
+        mDisplayTypeCombo.select(displayMode);
+        if (displayMode == EventDisplay.DISPLAY_TYPE_GRAPH) {
+            GridData gd = (GridData) mChartOptions.getLayoutData();
+            gd.exclude = false;
+            mChartOptions.setVisible(!gd.exclude);
+            long limit = eventDisplay.getChartTimeLimit();
+            if (limit != -1) {
+                mTimeLimitText.setText(Long.toString(limit));
+            } else {
+                mTimeLimitText.setText(""); //$NON-NLS-1$
+            }
+        } else {
+            GridData gd = (GridData) mChartOptions.getLayoutData();
+            gd.exclude = true;
+            mChartOptions.setVisible(!gd.exclude);
+            mTimeLimitText.setText(""); //$NON-NLS-1$
+        }
+
+        if (displayMode == EventDisplay.DISPLAY_TYPE_SYNC_HIST) {
+            GridData gd = (GridData) mHistOptions.getLayoutData();
+            gd.exclude = false;
+            mHistOptions.setVisible(!gd.exclude);
+            long limit = eventDisplay.getHistWidth();
+            if (limit != -1) {
+                mHistWidthText.setText(Long.toString(limit));
+            } else {
+                mHistWidthText.setText(""); //$NON-NLS-1$
+            }
+        } else {
+            GridData gd = (GridData) mHistOptions.getLayoutData();
+            gd.exclude = true;
+            mHistOptions.setVisible(!gd.exclude);
+            mHistWidthText.setText(""); //$NON-NLS-1$
+        }
+        mInfoGroup.layout(true);
+        mShell.layout(true);
+        mShell.pack();
+
+        if (eventDisplay.getPidFiltering()) {
+            mPidFilterCheckBox.setSelection(true);
+            mPidText.setEnabled(true);
+
+            // build the pid list.
+            ArrayList<Integer> list = eventDisplay.getPidFilterList();
+            if (list != null) {
+                StringBuilder sb = new StringBuilder();
+                int count = list.size();
+                for (int i = 0 ; i < count ; i++) {
+                    sb.append(list.get(i));
+                    if (i < count - 1) {
+                        sb.append(", ");//$NON-NLS-1$
+                    }
+                }
+                mPidText.setText(sb.toString());
+            } else {
+                mPidText.setText(""); //$NON-NLS-1$
+            }
+        } else {
+            mPidFilterCheckBox.setSelection(false);
+            mPidText.setEnabled(false);
+            mPidText.setText(""); //$NON-NLS-1$
+        }
+
+        mProcessTextChanges = true;
+
+        mValueSelection.mList.removeAll();
+        mOccurrenceSelection.mList.removeAll();
+
+        if (eventDisplay.getDisplayType() == EventDisplay.DISPLAY_TYPE_FILTERED_LOG ||
+                eventDisplay.getDisplayType() == EventDisplay.DISPLAY_TYPE_GRAPH) {
+            mOccurrenceSelection.setEnabled(true);
+            mValueSelection.setEnabled(true);
+
+            Iterator<ValueDisplayDescriptor> valueIterator = eventDisplay.getValueDescriptors();
+
+            while (valueIterator.hasNext()) {
+                ValueDisplayDescriptor descriptor = valueIterator.next();
+                mValueSelection.mList.add(String.format("%1$s: %2$s [%3$s]%4$s",
+                        mEventTagMap.get(descriptor.eventTag), descriptor.valueName,
+                        getSeriesLabelDescription(descriptor), getFilterDescription(descriptor)));
+            }
+
+            Iterator<OccurrenceDisplayDescriptor> occurrenceIterator =
+                eventDisplay.getOccurrenceDescriptors();
+
+            while (occurrenceIterator.hasNext()) {
+                OccurrenceDisplayDescriptor descriptor = occurrenceIterator.next();
+
+                mOccurrenceSelection.mList.add(String.format("%1$s [%2$s]%3$s",
+                        mEventTagMap.get(descriptor.eventTag),
+                        getSeriesLabelDescription(descriptor),
+                        getFilterDescription(descriptor)));
+            }
+
+            mValueSelection.mList.notifyListeners(SWT.Selection, null);
+            mOccurrenceSelection.mList.notifyListeners(SWT.Selection, null);
+        } else {
+            mOccurrenceSelection.setEnabled(false);
+            mValueSelection.setEnabled(false);
+        }
+
+    }
+
+    /**
+     * Returns a String describing what is used as the series label
+     * @param descriptor the descriptor of the display.
+     */
+    private String getSeriesLabelDescription(OccurrenceDisplayDescriptor descriptor) {
+        if (descriptor.seriesValueIndex != -1) {
+            if (descriptor.includePid) {
+                return String.format("%1$s + pid",
+                        mEventDescriptionMap.get(
+                                descriptor.eventTag)[descriptor.seriesValueIndex].getName());
+            } else {
+                return mEventDescriptionMap.get(descriptor.eventTag)[descriptor.seriesValueIndex]
+                                                                     .getName();
+            }
+        }
+        return "pid";
+    }
+
+    private String getFilterDescription(OccurrenceDisplayDescriptor descriptor) {
+        if (descriptor.filterValueIndex != -1) {
+            return String.format(" [%1$s %2$s %3$s]",
+                    mEventDescriptionMap.get(
+                            descriptor.eventTag)[descriptor.filterValueIndex].getName(),
+                            descriptor.filterCompareMethod.testString(),
+                            descriptor.filterValue != null ?
+                                    descriptor.filterValue.toString() : "?"); //$NON-NLS-1$
+        }
+        return ""; //$NON-NLS-1$
+    }
+
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogImporter.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogImporter.java
new file mode 100644
index 0000000..011bcf1
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogImporter.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2008 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.ddmuilib.log.event;
+
+import com.android.ddmlib.Log;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+
+/**
+ * Imports a textual event log.  Gets tags from build path.
+ */
+public class EventLogImporter {
+
+    private String[] mTags;
+    private String[] mLog;
+
+    public EventLogImporter(String filePath) throws FileNotFoundException {
+        String top = System.getenv("ANDROID_BUILD_TOP");
+        if (top == null) {
+            throw new FileNotFoundException();
+        }
+        final String tagFile = top + "/system/core/logcat/event-log-tags";
+        BufferedReader tagReader = new BufferedReader(
+                new InputStreamReader(new FileInputStream(tagFile)));
+        BufferedReader eventReader = new BufferedReader(
+                new InputStreamReader(new FileInputStream(filePath)));
+        try {
+            readTags(tagReader);
+            readLog(eventReader);
+        } catch (IOException e) {
+        } finally {
+            if (tagReader != null) {
+                try {
+                    tagReader.close();
+                } catch (IOException ignore) {
+                }
+            }
+            if (eventReader != null) {
+                try {
+                    eventReader.close();
+                } catch (IOException ignore) {
+                }
+            }
+        }
+    }
+
+    public String[] getTags() {
+        return mTags;
+    }
+
+    public String[] getLog() {
+        return mLog;
+    }
+
+    private void readTags(BufferedReader reader) throws IOException {
+        String line;
+
+        ArrayList<String> content = new ArrayList<String>();
+        while ((line = reader.readLine()) != null) {
+            content.add(line);
+        }
+        mTags = content.toArray(new String[content.size()]);
+    }
+
+    private void readLog(BufferedReader reader) throws IOException {
+        String line;
+
+        ArrayList<String> content = new ArrayList<String>();
+        while ((line = reader.readLine()) != null) {
+            content.add(line);
+        }
+
+        mLog = content.toArray(new String[content.size()]);
+    }
+
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogPanel.java
new file mode 100644
index 0000000..937ee40
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogPanel.java
@@ -0,0 +1,938 @@
+/*
+ * Copyright (C) 2008 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.ddmuilib.log.event;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventLogParser;
+import com.android.ddmlib.log.LogReceiver;
+import com.android.ddmlib.log.LogReceiver.ILogListener;
+import com.android.ddmlib.log.LogReceiver.LogEntry;
+import com.android.ddmuilib.DdmUiPreferences;
+import com.android.ddmuilib.TablePanel;
+import com.android.ddmuilib.actions.ICommonAction;
+import com.android.ddmuilib.annotation.UiThread;
+import com.android.ddmuilib.annotation.WorkerThread;
+import com.android.ddmuilib.log.event.EventDisplay.ILogColumnListener;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.RowData;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.regex.Pattern;
+
+/**
+ * Event log viewer
+ */
+public class EventLogPanel extends TablePanel implements ILogListener,
+        ILogColumnListener {
+
+    private final static String TAG_FILE_EXT = ".tag"; //$NON-NLS-1$
+
+    private final static String PREFS_EVENT_DISPLAY = "EventLogPanel.eventDisplay"; //$NON-NLS-1$
+    private final static String EVENT_DISPLAY_STORAGE_SEPARATOR = "|"; //$NON-NLS-1$
+
+    static final String PREFS_DISPLAY_WIDTH = "EventLogPanel.width"; //$NON-NLS-1$
+    static final String PREFS_DISPLAY_HEIGHT = "EventLogPanel.height"; //$NON-NLS-1$
+
+    private final static int DEFAULT_DISPLAY_WIDTH = 500;
+    private final static int DEFAULT_DISPLAY_HEIGHT = 400;
+
+    private IDevice mCurrentLoggedDevice;
+    private String mCurrentLogFile;
+    private LogReceiver mCurrentLogReceiver;
+    private EventLogParser mCurrentEventLogParser;
+
+    private Object mLock = new Object();
+
+    /** list of all the events. */
+    private final ArrayList<EventContainer> mEvents = new ArrayList<EventContainer>();
+
+    /** list of all the new events, that have yet to be displayed by the ui */
+    private final ArrayList<EventContainer> mNewEvents = new ArrayList<EventContainer>();
+    /** indicates a pending ui thread display */
+    private boolean mPendingDisplay = false;
+
+    /** list of all the custom event displays */
+    private final ArrayList<EventDisplay> mEventDisplays = new ArrayList<EventDisplay>();
+
+    private final NumberFormat mFormatter = NumberFormat.getInstance();
+    private Composite mParent;
+    private ScrolledComposite mBottomParentPanel;
+    private Composite mBottomPanel;
+    private ICommonAction mOptionsAction;
+    private ICommonAction mClearAction;
+    private ICommonAction mSaveAction;
+    private ICommonAction mLoadAction;
+    private ICommonAction mImportAction;
+
+    /** file containing the current log raw data. */
+    private File mTempFile = null;
+
+    public EventLogPanel() {
+        super();
+        mFormatter.setGroupingUsed(true);
+    }
+
+    /**
+     * Sets the external actions.
+     * <p/>This method sets up the {@link ICommonAction} objects to execute the proper code
+     * when triggered by using {@link ICommonAction#setRunnable(Runnable)}.
+     * <p/>It will also make sure they are enabled only when possible.
+     * @param optionsAction
+     * @param clearAction
+     * @param saveAction
+     * @param loadAction
+     * @param importAction
+     */
+    public void setActions(ICommonAction optionsAction, ICommonAction clearAction,
+            ICommonAction saveAction, ICommonAction loadAction, ICommonAction importAction) {
+        mOptionsAction = optionsAction;
+        mOptionsAction.setRunnable(new Runnable() {
+            @Override
+            public void run() {
+                openOptionPanel();
+            }
+        });
+
+        mClearAction = clearAction;
+        mClearAction.setRunnable(new Runnable() {
+            @Override
+            public void run() {
+                clearLog();
+            }
+        });
+
+        mSaveAction = saveAction;
+        mSaveAction.setRunnable(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.SAVE);
+
+                    fileDialog.setText("Save Event Log");
+                    fileDialog.setFileName("event.log");
+
+                    String fileName = fileDialog.open();
+                    if (fileName != null) {
+                        saveLog(fileName);
+                    }
+                } catch (IOException e1) {
+                }
+            }
+        });
+
+        mLoadAction = loadAction;
+        mLoadAction.setRunnable(new Runnable() {
+            @Override
+            public void run() {
+                FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN);
+
+                fileDialog.setText("Load Event Log");
+
+                String fileName = fileDialog.open();
+                if (fileName != null) {
+                    loadLog(fileName);
+                }
+            }
+        });
+
+        mImportAction = importAction;
+        mImportAction.setRunnable(new Runnable() {
+            @Override
+            public void run() {
+                FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN);
+
+                fileDialog.setText("Import Bug Report");
+
+                String fileName = fileDialog.open();
+                if (fileName != null) {
+                    importBugReport(fileName);
+                }
+            }
+        });
+
+        mOptionsAction.setEnabled(false);
+        mClearAction.setEnabled(false);
+        mSaveAction.setEnabled(false);
+    }
+
+    /**
+     * Opens the option panel.
+     * </p>
+     * <b>This must be called from the UI thread</b>
+     */
+    @UiThread
+    public void openOptionPanel() {
+        try {
+            EventDisplayOptions dialog = new EventDisplayOptions(mParent.getShell());
+            if (dialog.open(mCurrentEventLogParser, mEventDisplays, mEvents)) {
+                synchronized (mLock) {
+                    // get the new EventDisplay list
+                    mEventDisplays.clear();
+                    mEventDisplays.addAll(dialog.getEventDisplays());
+
+                    // since the list of EventDisplay changed, we store it.
+                    saveEventDisplays();
+
+                    rebuildUi();
+                }
+            }
+        } catch (SWTException e) {
+            Log.e("EventLog", e); //$NON-NLS-1$
+        }
+    }
+
+    /**
+     * Clears the log.
+     * <p/>
+     * <b>This must be called from the UI thread</b>
+     */
+    public void clearLog() {
+        try {
+            synchronized (mLock) {
+                mEvents.clear();
+                mNewEvents.clear();
+                mPendingDisplay = false;
+                for (EventDisplay eventDisplay : mEventDisplays) {
+                    eventDisplay.resetUI();
+                }
+            }
+        } catch (SWTException e) {
+            Log.e("EventLog", e); //$NON-NLS-1$
+        }
+    }
+
+    /**
+     * Saves the content of the event log into a file. The log is saved in the same
+     * binary format than on the device.
+     * @param filePath
+     * @throws IOException
+     */
+    public void saveLog(String filePath) throws IOException {
+        if (mCurrentLoggedDevice != null && mCurrentEventLogParser != null) {
+            File destFile = new File(filePath);
+            destFile.createNewFile();
+            FileInputStream fis = new FileInputStream(mTempFile);
+            FileOutputStream fos = new FileOutputStream(destFile);
+            byte[] buffer = new byte[1024];
+
+            int count;
+
+            while ((count = fis.read(buffer)) != -1) {
+                fos.write(buffer, 0, count);
+            }
+
+            fos.close();
+            fis.close();
+
+            // now we save the tag file
+            filePath = filePath + TAG_FILE_EXT;
+            mCurrentEventLogParser.saveTags(filePath);
+        }
+    }
+
+    /**
+     * Loads a binary event log (if has associated .tag file) or
+     * otherwise loads a textual event log.
+     * @param filePath Event log path (and base of potential tag file)
+     */
+    public void loadLog(String filePath) {
+        if ((new File(filePath + TAG_FILE_EXT)).exists()) {
+            startEventLogFromFiles(filePath);
+        } else {
+            try {
+                EventLogImporter importer = new EventLogImporter(filePath);
+                String[] tags = importer.getTags();
+                String[] log = importer.getLog();
+                startEventLogFromContent(tags, log);
+            } catch (FileNotFoundException e) {
+                // If this fails, display the error message from startEventLogFromFiles,
+                // and pretend we never tried EventLogImporter
+                Log.logAndDisplay(Log.LogLevel.ERROR, "EventLog",
+                        String.format("Failure to read %1$s", filePath + TAG_FILE_EXT));
+            }
+
+        }
+    }
+
+    public void importBugReport(String filePath) {
+        try {
+            BugReportImporter importer = new BugReportImporter(filePath);
+
+            String[] tags = importer.getTags();
+            String[] log = importer.getLog();
+
+            startEventLogFromContent(tags, log);
+
+        } catch (FileNotFoundException e) {
+            Log.logAndDisplay(LogLevel.ERROR, "Import",
+                    "Unable to import bug report: " + e.getMessage());
+        }
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.ddmuilib.SelectionDependentPanel#clientSelected()
+     */
+    @Override
+    public void clientSelected() {
+        // pass
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.ddmuilib.SelectionDependentPanel#deviceSelected()
+     */
+    @Override
+    public void deviceSelected() {
+        startEventLog(getCurrentDevice());
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.android.ddmlib.AndroidDebugBridge.IClientChangeListener#clientChanged(com.android.ddmlib.Client, int)
+     */
+    @Override
+    public void clientChanged(Client client, int changeMask) {
+        // pass
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.ddmuilib.Panel#createControl(org.eclipse.swt.widgets.Composite)
+     */
+    @Override
+    protected Control createControl(Composite parent) {
+        mParent = parent;
+        mParent.addDisposeListener(new DisposeListener() {
+            @Override
+            public void widgetDisposed(DisposeEvent e) {
+                synchronized (mLock) {
+                    if (mCurrentLogReceiver != null) {
+                        mCurrentLogReceiver.cancel();
+                        mCurrentLogReceiver = null;
+                        mCurrentEventLogParser = null;
+                        mCurrentLoggedDevice = null;
+                        mEventDisplays.clear();
+                        mEvents.clear();
+                    }
+                }
+            }
+        });
+
+        final IPreferenceStore store = DdmUiPreferences.getStore();
+
+        // init some store stuff
+        store.setDefault(PREFS_DISPLAY_WIDTH, DEFAULT_DISPLAY_WIDTH);
+        store.setDefault(PREFS_DISPLAY_HEIGHT, DEFAULT_DISPLAY_HEIGHT);
+
+        mBottomParentPanel = new ScrolledComposite(parent, SWT.V_SCROLL);
+        mBottomParentPanel.setLayoutData(new GridData(GridData.FILL_BOTH));
+        mBottomParentPanel.setExpandHorizontal(true);
+        mBottomParentPanel.setExpandVertical(true);
+
+        mBottomParentPanel.addControlListener(new ControlAdapter() {
+            @Override
+            public void controlResized(ControlEvent e) {
+                if (mBottomPanel != null) {
+                    Rectangle r = mBottomParentPanel.getClientArea();
+                    mBottomParentPanel.setMinSize(mBottomPanel.computeSize(r.width,
+                        SWT.DEFAULT));
+                }
+            }
+        });
+
+        prepareDisplayUi();
+
+        // load the EventDisplay from storage.
+        loadEventDisplays();
+
+        // create the ui
+        createDisplayUi();
+
+        return mBottomParentPanel;
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.ddmuilib.Panel#postCreation()
+     */
+    @Override
+    protected void postCreation() {
+        // pass
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.ddmuilib.Panel#setFocus()
+     */
+    @Override
+    public void setFocus() {
+        mBottomParentPanel.setFocus();
+    }
+
+    /**
+     * Starts a new logcat and set mCurrentLogCat as the current receiver.
+     * @param device the device to connect logcat to.
+     */
+    private void startEventLog(final IDevice device) {
+        if (device == mCurrentLoggedDevice) {
+            return;
+        }
+
+        // if we have a logcat already running
+        if (mCurrentLogReceiver != null) {
+            stopEventLog(false);
+        }
+        mCurrentLoggedDevice = null;
+        mCurrentLogFile = null;
+
+        if (device != null) {
+            // create a new output receiver
+            mCurrentLogReceiver = new LogReceiver(this);
+
+            // start the logcat in a different thread
+            new Thread("EventLog")  { //$NON-NLS-1$
+                @Override
+                public void run() {
+                    while (device.isOnline() == false &&
+                            mCurrentLogReceiver != null &&
+                            mCurrentLogReceiver.isCancelled() == false) {
+                        try {
+                            sleep(2000);
+                        } catch (InterruptedException e) {
+                            return;
+                        }
+                    }
+
+                    if (mCurrentLogReceiver == null || mCurrentLogReceiver.isCancelled()) {
+                        // logcat was stopped/cancelled before the device became ready.
+                        return;
+                    }
+
+                    try {
+                        mCurrentLoggedDevice = device;
+                        synchronized (mLock) {
+                            mCurrentEventLogParser = new EventLogParser();
+                            mCurrentEventLogParser.init(device);
+                        }
+
+                        // update the event display with the new parser.
+                        updateEventDisplays();
+
+                        // prepare the temp file that will contain the raw data
+                        mTempFile = File.createTempFile("android-event-", ".log");
+
+                        device.runEventLogService(mCurrentLogReceiver);
+                    } catch (Exception e) {
+                        Log.e("EventLog", e);
+                    } finally {
+                    }
+                }
+            }.start();
+        }
+    }
+
+    private void startEventLogFromFiles(final String fileName) {
+        // if we have a logcat already running
+        if (mCurrentLogReceiver != null) {
+            stopEventLog(false);
+        }
+        mCurrentLoggedDevice = null;
+        mCurrentLogFile = null;
+
+        // create a new output receiver
+        mCurrentLogReceiver = new LogReceiver(this);
+
+        mSaveAction.setEnabled(false);
+
+        // start the logcat in a different thread
+        new Thread("EventLog")  { //$NON-NLS-1$
+            @Override
+            public void run() {
+                try {
+                    mCurrentLogFile = fileName;
+                    synchronized (mLock) {
+                        mCurrentEventLogParser = new EventLogParser();
+                        if (mCurrentEventLogParser.init(fileName + TAG_FILE_EXT) == false) {
+                            mCurrentEventLogParser = null;
+                            Log.logAndDisplay(LogLevel.ERROR, "EventLog",
+                                    String.format("Failure to read %1$s", fileName + TAG_FILE_EXT));
+                            return;
+                        }
+                    }
+
+                    // update the event display with the new parser.
+                    updateEventDisplays();
+
+                    runLocalEventLogService(fileName, mCurrentLogReceiver);
+                } catch (Exception e) {
+                    Log.e("EventLog", e);
+                } finally {
+                }
+            }
+        }.start();
+    }
+
+    private void startEventLogFromContent(final String[] tags, final String[] log) {
+        // if we have a logcat already running
+        if (mCurrentLogReceiver != null) {
+            stopEventLog(false);
+        }
+        mCurrentLoggedDevice = null;
+        mCurrentLogFile = null;
+
+        // create a new output receiver
+        mCurrentLogReceiver = new LogReceiver(this);
+
+        mSaveAction.setEnabled(false);
+
+        // start the logcat in a different thread
+        new Thread("EventLog")  { //$NON-NLS-1$
+            @Override
+            public void run() {
+                try {
+                    synchronized (mLock) {
+                        mCurrentEventLogParser = new EventLogParser();
+                        if (mCurrentEventLogParser.init(tags) == false) {
+                            mCurrentEventLogParser = null;
+                            return;
+                        }
+                    }
+
+                    // update the event display with the new parser.
+                    updateEventDisplays();
+
+                    runLocalEventLogService(log, mCurrentLogReceiver);
+                } catch (Exception e) {
+                    Log.e("EventLog", e);
+                } finally {
+                }
+            }
+        }.start();
+    }
+
+
+    public void stopEventLog(boolean inUiThread) {
+        if (mCurrentLogReceiver != null) {
+            mCurrentLogReceiver.cancel();
+
+            // when the thread finishes, no one will reference that object
+            // and it'll be destroyed
+            synchronized (mLock) {
+                mCurrentLogReceiver = null;
+                mCurrentEventLogParser = null;
+
+                mCurrentLoggedDevice = null;
+                mEvents.clear();
+                mNewEvents.clear();
+                mPendingDisplay = false;
+            }
+
+            resetUI(inUiThread);
+        }
+
+        if (mTempFile != null) {
+            mTempFile.delete();
+            mTempFile = null;
+        }
+    }
+
+    private void resetUI(boolean inUiThread) {
+        mEvents.clear();
+
+        // the ui is static we just empty it.
+        if (inUiThread) {
+            resetUiFromUiThread();
+        } else {
+            try {
+                Display d = mBottomParentPanel.getDisplay();
+
+                // run sync as we need to update right now.
+                d.syncExec(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (mBottomParentPanel.isDisposed() == false) {
+                            resetUiFromUiThread();
+                        }
+                    }
+                });
+            } catch (SWTException e) {
+                // display is disposed, we're quitting. Do nothing.
+            }
+        }
+    }
+
+    private void resetUiFromUiThread() {
+        synchronized (mLock) {
+            for (EventDisplay eventDisplay : mEventDisplays) {
+                eventDisplay.resetUI();
+            }
+        }
+        mOptionsAction.setEnabled(false);
+        mClearAction.setEnabled(false);
+        mSaveAction.setEnabled(false);
+    }
+
+    private void prepareDisplayUi() {
+        mBottomPanel = new Composite(mBottomParentPanel, SWT.NONE);
+        mBottomParentPanel.setContent(mBottomPanel);
+    }
+
+    private void createDisplayUi() {
+        RowLayout rowLayout = new RowLayout();
+        rowLayout.wrap = true;
+        rowLayout.pack = false;
+        rowLayout.justify = true;
+        rowLayout.fill = true;
+        rowLayout.type = SWT.HORIZONTAL;
+        mBottomPanel.setLayout(rowLayout);
+
+        IPreferenceStore store = DdmUiPreferences.getStore();
+        int displayWidth = store.getInt(PREFS_DISPLAY_WIDTH);
+        int displayHeight = store.getInt(PREFS_DISPLAY_HEIGHT);
+
+        for (EventDisplay eventDisplay : mEventDisplays) {
+            Control c = eventDisplay.createComposite(mBottomPanel, mCurrentEventLogParser, this);
+            if (c != null) {
+                RowData rd = new RowData();
+                rd.height = displayHeight;
+                rd.width = displayWidth;
+                c.setLayoutData(rd);
+            }
+
+            Table table = eventDisplay.getTable();
+            if (table != null) {
+                addTableToFocusListener(table);
+            }
+        }
+
+        mBottomPanel.layout();
+        mBottomParentPanel.setMinSize(mBottomPanel.computeSize(SWT.DEFAULT, SWT.DEFAULT));
+        mBottomParentPanel.layout();
+    }
+
+    /**
+     * Rebuild the display ui.
+     */
+    @UiThread
+    private void rebuildUi() {
+        synchronized (mLock) {
+            // we need to rebuild the ui. First we get rid of it.
+            mBottomPanel.dispose();
+            mBottomPanel = null;
+
+            prepareDisplayUi();
+            createDisplayUi();
+
+            // and fill it
+
+            boolean start_event = false;
+            synchronized (mNewEvents) {
+                mNewEvents.addAll(0, mEvents);
+
+                if (mPendingDisplay == false) {
+                    mPendingDisplay = true;
+                    start_event = true;
+                }
+            }
+
+            if (start_event) {
+                scheduleUIEventHandler();
+            }
+
+            Rectangle r = mBottomParentPanel.getClientArea();
+            mBottomParentPanel.setMinSize(mBottomPanel.computeSize(r.width,
+                SWT.DEFAULT));
+        }
+    }
+
+
+    /**
+     * Processes a new {@link LogEntry} by parsing it with {@link EventLogParser} and displaying it.
+     * @param entry The new log entry
+     * @see LogReceiver.ILogListener#newEntry(LogEntry)
+     */
+    @Override
+    @WorkerThread
+    public void newEntry(LogEntry entry) {
+        synchronized (mLock) {
+            if (mCurrentEventLogParser != null) {
+                EventContainer event = mCurrentEventLogParser.parse(entry);
+                if (event != null) {
+                    handleNewEvent(event);
+                }
+            }
+        }
+    }
+
+    @WorkerThread
+    private void handleNewEvent(EventContainer event) {
+        // add the event to the generic list
+        mEvents.add(event);
+
+        // add to the list of events that needs to be displayed, and trigger a
+        // new display if needed.
+        boolean start_event = false;
+        synchronized (mNewEvents) {
+            mNewEvents.add(event);
+
+            if (mPendingDisplay == false) {
+                mPendingDisplay = true;
+                start_event = true;
+            }
+        }
+
+        if (start_event == false) {
+            // we're done
+            return;
+        }
+
+        scheduleUIEventHandler();
+    }
+
+    /**
+     * Schedules the UI thread to execute a {@link Runnable} calling {@link #displayNewEvents()}.
+     */
+    private void scheduleUIEventHandler() {
+        try  {
+            Display d = mBottomParentPanel.getDisplay();
+            d.asyncExec(new Runnable() {
+                @Override
+                public void run() {
+                    if (mBottomParentPanel.isDisposed() == false) {
+                        if (mCurrentEventLogParser != null) {
+                            displayNewEvents();
+                        }
+                    }
+                }
+            });
+        } catch (SWTException e) {
+            // if the ui is disposed, do nothing
+        }
+    }
+
+    /**
+     * Processes raw data coming from the log service.
+     * @see LogReceiver.ILogListener#newData(byte[], int, int)
+     */
+    @Override
+    public void newData(byte[] data, int offset, int length) {
+        if (mTempFile != null) {
+            try {
+                FileOutputStream fos = new FileOutputStream(mTempFile, true /* append */);
+                fos.write(data, offset, length);
+                fos.close();
+            } catch (FileNotFoundException e) {
+            } catch (IOException e) {
+            }
+        }
+    }
+
+    @UiThread
+    private void displayNewEvents() {
+        // never display more than 1,000 events in this loop. We can't do too much in the UI thread.
+        int count = 0;
+
+        // prepare the displays
+        for (EventDisplay eventDisplay : mEventDisplays) {
+            eventDisplay.startMultiEventDisplay();
+        }
+
+        // display the new events
+        EventContainer event = null;
+        boolean need_to_reloop = false;
+        do {
+            // get the next event to display.
+            synchronized (mNewEvents) {
+                if (mNewEvents.size() > 0) {
+                    if (count > 200) {
+                        // there are still events to be displayed, but we don't want to hog the
+                        // UI thread for too long, so we stop this runnable, but launch a new
+                        // one to keep going.
+                        need_to_reloop = true;
+                        event = null;
+                    } else {
+                        event = mNewEvents.remove(0);
+                        count++;
+                    }
+                } else {
+                    // we're done.
+                    event = null;
+                    mPendingDisplay = false;
+                }
+            }
+
+            if (event != null) {
+                // notify the event display
+                for (EventDisplay eventDisplay : mEventDisplays) {
+                    eventDisplay.newEvent(event, mCurrentEventLogParser);
+                }
+            }
+        } while (event != null);
+
+        // we're done displaying events.
+        for (EventDisplay eventDisplay : mEventDisplays) {
+            eventDisplay.endMultiEventDisplay();
+        }
+
+        // if needed, ask the UI thread to re-run this method.
+        if (need_to_reloop) {
+            scheduleUIEventHandler();
+        }
+    }
+
+    /**
+     * Loads the {@link EventDisplay}s from the preference store.
+     */
+    private void loadEventDisplays() {
+        IPreferenceStore store = DdmUiPreferences.getStore();
+        String storage = store.getString(PREFS_EVENT_DISPLAY);
+
+        if (storage.length() > 0) {
+            String[] values = storage.split(Pattern.quote(EVENT_DISPLAY_STORAGE_SEPARATOR));
+
+            for (String value : values) {
+                EventDisplay eventDisplay = EventDisplay.load(value);
+                if (eventDisplay != null) {
+                    mEventDisplays.add(eventDisplay);
+                }
+            }
+        }
+    }
+
+    /**
+     * Saves the {@link EventDisplay}s into the {@link DdmUiPreferences} store.
+     */
+    private void saveEventDisplays() {
+        IPreferenceStore store = DdmUiPreferences.getStore();
+
+        boolean first = true;
+        StringBuilder sb = new StringBuilder();
+
+        for (EventDisplay eventDisplay : mEventDisplays) {
+            String storage = eventDisplay.getStorageString();
+            if (storage != null) {
+                if (first == false) {
+                    sb.append(EVENT_DISPLAY_STORAGE_SEPARATOR);
+                } else {
+                    first = false;
+                }
+
+                sb.append(storage);
+            }
+        }
+
+        store.setValue(PREFS_EVENT_DISPLAY, sb.toString());
+    }
+
+    /**
+     * Updates the {@link EventDisplay} with the new {@link EventLogParser}.
+     * <p/>
+     * This will run asynchronously in the UI thread.
+     */
+    @WorkerThread
+    private void updateEventDisplays() {
+        try {
+            Display d = mBottomParentPanel.getDisplay();
+
+            d.asyncExec(new Runnable() {
+                @Override
+                public void run() {
+                    if (mBottomParentPanel.isDisposed() == false) {
+                        for (EventDisplay eventDisplay : mEventDisplays) {
+                            eventDisplay.setNewLogParser(mCurrentEventLogParser);
+                        }
+
+                        mOptionsAction.setEnabled(true);
+                        mClearAction.setEnabled(true);
+                        if (mCurrentLogFile == null) {
+                            mSaveAction.setEnabled(true);
+                        } else {
+                            mSaveAction.setEnabled(false);
+                        }
+                    }
+                }
+            });
+        } catch (SWTException e) {
+            // display is disposed: do nothing.
+        }
+    }
+
+    @Override
+    @UiThread
+    public void columnResized(int index, TableColumn sourceColumn) {
+        for (EventDisplay eventDisplay : mEventDisplays) {
+            eventDisplay.resizeColumn(index, sourceColumn);
+        }
+    }
+
+    /**
+     * Runs an event log service out of a local file.
+     * @param fileName the full file name of the local file containing the event log.
+     * @param logReceiver the receiver that will handle the log
+     * @throws IOException
+     */
+    @WorkerThread
+    private void runLocalEventLogService(String fileName, LogReceiver logReceiver)
+            throws IOException {
+        byte[] buffer = new byte[256];
+
+        FileInputStream fis = new FileInputStream(fileName);
+        try {
+            int count;
+            while ((count = fis.read(buffer)) != -1) {
+                logReceiver.parseNewData(buffer, 0, count);
+            }
+        } finally {
+            fis.close();
+        }
+    }
+
+    @WorkerThread
+    private void runLocalEventLogService(String[] log, LogReceiver currentLogReceiver) {
+        synchronized (mLock) {
+            for (String line : log) {
+                EventContainer event = mCurrentEventLogParser.parse(line);
+                if (event != null) {
+                    handleNewEvent(event);
+                }
+            }
+        }
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventValueSelector.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventValueSelector.java
new file mode 100644
index 0000000..e7c5196
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventValueSelector.java
@@ -0,0 +1,630 @@
+/*
+ * Copyright (C) 2008 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.ddmuilib.log.event;
+
+import com.android.ddmlib.log.EventContainer.CompareMethod;
+import com.android.ddmlib.log.EventContainer.EventValueType;
+import com.android.ddmlib.log.EventLogParser;
+import com.android.ddmlib.log.EventValueDescription;
+import com.android.ddmuilib.log.event.EventDisplay.OccurrenceDisplayDescriptor;
+import com.android.ddmuilib.log.event.EventDisplay.ValueDisplayDescriptor;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.Set;
+
+final class EventValueSelector extends Dialog {
+    private static final int DLG_WIDTH = 400;
+    private static final int DLG_HEIGHT = 300;
+
+    private Shell mParent;
+    private Shell mShell;
+    private boolean mEditStatus;
+    private Combo mEventCombo;
+    private Combo mValueCombo;
+    private Combo mSeriesCombo;
+    private Button mDisplayPidCheckBox;
+    private Combo mFilterCombo;
+    private Combo mFilterMethodCombo;
+    private Text mFilterValue;
+    private Button mOkButton;
+
+    private EventLogParser mLogParser;
+    private OccurrenceDisplayDescriptor mDescriptor;
+
+    /** list of event integer in the order of the combo. */
+    private Integer[] mEventTags;
+
+    /** list of indices in the {@link EventValueDescription} array of the current event
+     * that are of type string. This lets us get back the {@link EventValueDescription} from the
+     * index in the Series {@link Combo}.
+     */
+    private final ArrayList<Integer> mSeriesIndices = new ArrayList<Integer>();
+
+    public EventValueSelector(Shell parent) {
+        super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL);
+    }
+
+    /**
+     * Opens the display option dialog to edit a new descriptor.
+     * @param decriptorClass the class of the object to instantiate. Must extend
+     * {@link OccurrenceDisplayDescriptor}
+     * @param logParser
+     * @return true if the object is to be created, false if the creation was canceled.
+     */
+    boolean open(Class<? extends OccurrenceDisplayDescriptor> descriptorClass,
+            EventLogParser logParser) {
+        try {
+            OccurrenceDisplayDescriptor descriptor = descriptorClass.newInstance();
+            setModified();
+            return open(descriptor, logParser);
+        } catch (InstantiationException e) {
+            return false;
+        } catch (IllegalAccessException e) {
+            return false;
+        }
+    }
+
+    /**
+     * Opens the display option dialog, to edit a {@link OccurrenceDisplayDescriptor} object or
+     * a {@link ValueDisplayDescriptor} object.
+     * @param descriptor The descriptor to edit.
+     * @return true if the object was modified.
+     */
+    boolean open(OccurrenceDisplayDescriptor descriptor, EventLogParser logParser) {
+        // make a copy of the descriptor as we'll use a working copy.
+        if (descriptor instanceof ValueDisplayDescriptor) {
+            mDescriptor = new ValueDisplayDescriptor((ValueDisplayDescriptor)descriptor);
+        } else if (descriptor instanceof OccurrenceDisplayDescriptor) {
+            mDescriptor = new OccurrenceDisplayDescriptor(descriptor);
+        } else {
+            return false;
+        }
+
+        mLogParser = logParser;
+
+        createUI();
+
+        if (mParent == null || mShell == null) {
+            return false;
+        }
+
+        loadValueDescriptor();
+
+        checkValidity();
+
+        // Set the dialog size.
+        try {
+            mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT);
+            Rectangle r = mParent.getBounds();
+            // get the center new top left.
+            int cx = r.x + r.width/2;
+            int x = cx - DLG_WIDTH / 2;
+            int cy = r.y + r.height/2;
+            int y = cy - DLG_HEIGHT / 2;
+            mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        mShell.layout();
+
+        // actually open the dialog
+        mShell.open();
+
+        // event loop until the dialog is closed.
+        Display display = mParent.getDisplay();
+        while (!mShell.isDisposed()) {
+            if (!display.readAndDispatch())
+                display.sleep();
+        }
+
+        return mEditStatus;
+    }
+
+    OccurrenceDisplayDescriptor getDescriptor() {
+        return mDescriptor;
+    }
+
+    private void createUI() {
+        GridData gd;
+
+        mParent = getParent();
+        mShell = new Shell(mParent, getStyle());
+        mShell.setText("Event Display Configuration");
+
+        mShell.setLayout(new GridLayout(2, false));
+
+        Label l = new Label(mShell, SWT.NONE);
+        l.setText("Event:");
+
+        mEventCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY);
+        mEventCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        // the event tag / event name map
+        Map<Integer, String> eventTagMap = mLogParser.getTagMap();
+        Map<Integer, EventValueDescription[]> eventInfoMap = mLogParser.getEventInfoMap();
+        Set<Integer> keys = eventTagMap.keySet();
+        ArrayList<Integer> list = new ArrayList<Integer>();
+        for (Integer i : keys) {
+            if (eventInfoMap.get(i) != null) {
+                String eventName = eventTagMap.get(i);
+                mEventCombo.add(eventName);
+
+                list.add(i);
+            }
+        }
+        mEventTags = list.toArray(new Integer[list.size()]);
+
+        mEventCombo.addSelectionListener(new SelectionAdapter() {
+            /* (non-Javadoc)
+             * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+             */
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                handleEventComboSelection();
+                setModified();
+            }
+        });
+
+        l = new Label(mShell, SWT.NONE);
+        l.setText("Value:");
+
+        mValueCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY);
+        mValueCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mValueCombo.addSelectionListener(new SelectionAdapter() {
+            /* (non-Javadoc)
+             * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+             */
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                handleValueComboSelection();
+                setModified();
+            }
+        });
+
+        l = new Label(mShell, SWT.NONE);
+        l.setText("Series Name:");
+
+        mSeriesCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY);
+        mSeriesCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mSeriesCombo.addSelectionListener(new SelectionAdapter() {
+            /* (non-Javadoc)
+             * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+             */
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                handleSeriesComboSelection();
+                setModified();
+            }
+        });
+
+        // empty comp
+        new Composite(mShell, SWT.NONE).setLayoutData(gd = new GridData());
+        gd.heightHint = gd.widthHint = 0;
+
+        mDisplayPidCheckBox = new Button(mShell, SWT.CHECK);
+        mDisplayPidCheckBox.setText("Also Show pid");
+        mDisplayPidCheckBox.setEnabled(false);
+        mDisplayPidCheckBox.addSelectionListener(new SelectionAdapter() {
+            /* (non-Javadoc)
+             * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+             */
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                mDescriptor.includePid = mDisplayPidCheckBox.getSelection();
+                setModified();
+            }
+        });
+
+        l = new Label(mShell, SWT.NONE);
+        l.setText("Filter By:");
+
+        mFilterCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY);
+        mFilterCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mFilterCombo.addSelectionListener(new SelectionAdapter() {
+            /* (non-Javadoc)
+             * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+             */
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                handleFilterComboSelection();
+                setModified();
+            }
+        });
+
+        l = new Label(mShell, SWT.NONE);
+        l.setText("Filter Method:");
+
+        mFilterMethodCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY);
+        mFilterMethodCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        for (CompareMethod method : CompareMethod.values()) {
+            mFilterMethodCombo.add(method.toString());
+        }
+        mFilterMethodCombo.select(0);
+        mFilterMethodCombo.addSelectionListener(new SelectionAdapter() {
+            /* (non-Javadoc)
+             * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+             */
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                handleFilterMethodComboSelection();
+                setModified();
+            }
+        });
+
+        l = new Label(mShell, SWT.NONE);
+        l.setText("Filter Value:");
+
+        mFilterValue = new Text(mShell, SWT.BORDER | SWT.SINGLE);
+        mFilterValue.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mFilterValue.addModifyListener(new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent e) {
+                if (mDescriptor.filterValueIndex != -1) {
+                    // get the current selection in the event combo
+                    int index = mEventCombo.getSelectionIndex();
+
+                    if (index != -1) {
+                        // match it to an event
+                        int eventTag = mEventTags[index];
+                        mDescriptor.eventTag = eventTag;
+
+                        // get the EventValueDescription for this tag
+                        EventValueDescription valueDesc = mLogParser.getEventInfoMap()
+                            .get(eventTag)[mDescriptor.filterValueIndex];
+
+                        // let the EventValueDescription convert the String value into an object
+                        // of the proper type.
+                        mDescriptor.filterValue = valueDesc.getObjectFromString(
+                                mFilterValue.getText().trim());
+                        setModified();
+                    }
+                }
+            }
+        });
+
+        // add a separator spanning the 2 columns
+
+        l = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL);
+        gd = new GridData(GridData.FILL_HORIZONTAL);
+        gd.horizontalSpan = 2;
+        l.setLayoutData(gd);
+
+        // add a composite to hold the ok/cancel button, no matter what the columns size are.
+        Composite buttonComp = new Composite(mShell, SWT.NONE);
+        gd = new GridData(GridData.FILL_HORIZONTAL);
+        gd.horizontalSpan = 2;
+        buttonComp.setLayoutData(gd);
+        GridLayout gl;
+        buttonComp.setLayout(gl = new GridLayout(6, true));
+        gl.marginHeight = gl.marginWidth = 0;
+
+        Composite padding = new Composite(mShell, SWT.NONE);
+        padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        mOkButton = new Button(buttonComp, SWT.PUSH);
+        mOkButton.setText("OK");
+        mOkButton.setLayoutData(new GridData(GridData.CENTER));
+        mOkButton.addSelectionListener(new SelectionAdapter() {
+            /* (non-Javadoc)
+             * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+             */
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                mShell.close();
+            }
+        });
+
+        padding = new Composite(mShell, SWT.NONE);
+        padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        padding = new Composite(mShell, SWT.NONE);
+        padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        Button cancelButton = new Button(buttonComp, SWT.PUSH);
+        cancelButton.setText("Cancel");
+        cancelButton.setLayoutData(new GridData(GridData.CENTER));
+        cancelButton.addSelectionListener(new SelectionAdapter() {
+            /* (non-Javadoc)
+             * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent)
+             */
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                // cancel the edit
+                mEditStatus = false;
+                mShell.close();
+            }
+        });
+
+        padding = new Composite(mShell, SWT.NONE);
+        padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        mShell.addListener(SWT.Close, new Listener() {
+            @Override
+            public void handleEvent(Event event) {
+                event.doit = true;
+            }
+        });
+    }
+
+    private void setModified() {
+        mEditStatus = true;
+    }
+
+    private void handleEventComboSelection() {
+        // get the current selection in the event combo
+        int index = mEventCombo.getSelectionIndex();
+
+        if (index != -1) {
+            // match it to an event
+            int eventTag = mEventTags[index];
+            mDescriptor.eventTag = eventTag;
+
+            // get the EventValueDescription for this tag
+            EventValueDescription[] values = mLogParser.getEventInfoMap().get(eventTag);
+
+            // fill the combo for the values
+            mValueCombo.removeAll();
+            if (values != null) {
+                if (mDescriptor instanceof ValueDisplayDescriptor) {
+                    ValueDisplayDescriptor valueDescriptor = (ValueDisplayDescriptor)mDescriptor;
+
+                    mValueCombo.setEnabled(true);
+                    for (EventValueDescription value : values) {
+                        mValueCombo.add(value.toString());
+                    }
+
+                    if (valueDescriptor.valueIndex != -1) {
+                        mValueCombo.select(valueDescriptor.valueIndex);
+                    } else {
+                        mValueCombo.clearSelection();
+                    }
+                } else {
+                    mValueCombo.setEnabled(false);
+                }
+
+                // fill the axis combo
+                mSeriesCombo.removeAll();
+                mSeriesCombo.setEnabled(false);
+                mSeriesIndices.clear();
+                int axisIndex = 0;
+                int selectionIndex = -1;
+                for (EventValueDescription value : values) {
+                    if (value.getEventValueType() == EventValueType.STRING) {
+                        mSeriesCombo.add(value.getName());
+                        mSeriesCombo.setEnabled(true);
+                        mSeriesIndices.add(axisIndex);
+
+                        if (mDescriptor.seriesValueIndex != -1 &&
+                                mDescriptor.seriesValueIndex == axisIndex) {
+                            selectionIndex = axisIndex;
+                        }
+                    }
+                    axisIndex++;
+                }
+
+                if (mSeriesCombo.isEnabled()) {
+                    mSeriesCombo.add("default (pid)", 0 /* index */);
+                    mSeriesIndices.add(0 /* index */, -1 /* value */);
+
+                    // +1 because we added another item at index 0
+                    mSeriesCombo.select(selectionIndex + 1);
+
+                    if (selectionIndex >= 0) {
+                        mDisplayPidCheckBox.setSelection(mDescriptor.includePid);
+                        mDisplayPidCheckBox.setEnabled(true);
+                    } else {
+                        mDisplayPidCheckBox.setEnabled(false);
+                        mDisplayPidCheckBox.setSelection(false);
+                    }
+                } else {
+                    mDisplayPidCheckBox.setSelection(false);
+                    mDisplayPidCheckBox.setEnabled(false);
+                }
+
+                // fill the filter combo
+                mFilterCombo.setEnabled(true);
+                mFilterCombo.removeAll();
+                mFilterCombo.add("(no filter)");
+                for (EventValueDescription value : values) {
+                    mFilterCombo.add(value.toString());
+                }
+
+                // select the current filter
+                mFilterCombo.select(mDescriptor.filterValueIndex + 1);
+                mFilterMethodCombo.select(getFilterMethodIndex(mDescriptor.filterCompareMethod));
+
+                // fill the current filter value
+                if (mDescriptor.filterValueIndex != -1) {
+                    EventValueDescription valueInfo = values[mDescriptor.filterValueIndex];
+                    if (valueInfo.checkForType(mDescriptor.filterValue)) {
+                        mFilterValue.setText(mDescriptor.filterValue.toString());
+                    } else {
+                        mFilterValue.setText("");
+                    }
+                } else {
+                    mFilterValue.setText("");
+                }
+            } else {
+                disableSubCombos();
+            }
+        } else {
+            disableSubCombos();
+        }
+
+        checkValidity();
+    }
+
+    /**
+     *
+     */
+    private void disableSubCombos() {
+        mValueCombo.removeAll();
+        mValueCombo.clearSelection();
+        mValueCombo.setEnabled(false);
+
+        mSeriesCombo.removeAll();
+        mSeriesCombo.clearSelection();
+        mSeriesCombo.setEnabled(false);
+
+        mDisplayPidCheckBox.setEnabled(false);
+        mDisplayPidCheckBox.setSelection(false);
+
+        mFilterCombo.removeAll();
+        mFilterCombo.clearSelection();
+        mFilterCombo.setEnabled(false);
+
+        mFilterValue.setEnabled(false);
+        mFilterValue.setText("");
+        mFilterMethodCombo.setEnabled(false);
+    }
+
+    private void handleValueComboSelection() {
+        ValueDisplayDescriptor valueDescriptor = (ValueDisplayDescriptor)mDescriptor;
+
+        // get the current selection in the value combo
+        int index = mValueCombo.getSelectionIndex();
+        valueDescriptor.valueIndex = index;
+
+        // for now set the built-in name
+
+        // get the current selection in the event combo
+        int eventIndex = mEventCombo.getSelectionIndex();
+
+        // match it to an event
+        int eventTag = mEventTags[eventIndex];
+
+        // get the EventValueDescription for this tag
+        EventValueDescription[] values = mLogParser.getEventInfoMap().get(eventTag);
+
+        valueDescriptor.valueName = values[index].getName();
+
+        checkValidity();
+    }
+
+    private void handleSeriesComboSelection() {
+        // get the current selection in the axis combo
+        int index = mSeriesCombo.getSelectionIndex();
+
+        // get the actual value index from the list.
+        int valueIndex = mSeriesIndices.get(index);
+
+        mDescriptor.seriesValueIndex = valueIndex;
+
+        if (index > 0) {
+            mDisplayPidCheckBox.setEnabled(true);
+            mDisplayPidCheckBox.setSelection(mDescriptor.includePid);
+        } else {
+            mDisplayPidCheckBox.setSelection(false);
+            mDisplayPidCheckBox.setEnabled(false);
+        }
+    }
+
+    private void handleFilterComboSelection() {
+        // get the current selection in the axis combo
+        int index = mFilterCombo.getSelectionIndex();
+
+        // decrement index by 1 since the item 0 means
+        // no filter (index = -1), and the rest is offset by 1
+        index--;
+
+        mDescriptor.filterValueIndex = index;
+
+        if (index != -1) {
+            mFilterValue.setEnabled(true);
+            mFilterMethodCombo.setEnabled(true);
+            if (mDescriptor.filterValue instanceof String) {
+                mFilterValue.setText((String)mDescriptor.filterValue);
+            }
+        } else {
+            mFilterValue.setText("");
+            mFilterValue.setEnabled(false);
+            mFilterMethodCombo.setEnabled(false);
+        }
+    }
+
+    private void handleFilterMethodComboSelection() {
+        // get the current selection in the axis combo
+        int index = mFilterMethodCombo.getSelectionIndex();
+        CompareMethod method = CompareMethod.values()[index];
+
+        mDescriptor.filterCompareMethod = method;
+    }
+
+    /**
+     * Returns the index of the filter method
+     * @param filterCompareMethod the {@link CompareMethod} enum.
+     */
+    private int getFilterMethodIndex(CompareMethod filterCompareMethod) {
+        CompareMethod[] values = CompareMethod.values();
+        for (int i = 0 ; i < values.length ; i++) {
+            if (values[i] == filterCompareMethod) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+
+    private void loadValueDescriptor() {
+        // get the index from the eventTag.
+        int eventIndex = 0;
+        int comboIndex = -1;
+        for (int i : mEventTags) {
+            if (i == mDescriptor.eventTag) {
+                comboIndex = eventIndex;
+                break;
+            }
+            eventIndex++;
+        }
+
+        if (comboIndex == -1) {
+            mEventCombo.clearSelection();
+        } else {
+            mEventCombo.select(comboIndex);
+        }
+
+        // get the event from the descriptor
+        handleEventComboSelection();
+    }
+
+    private void checkValidity() {
+        mOkButton.setEnabled(mEventCombo.getSelectionIndex() != -1 &&
+                (((mDescriptor instanceof ValueDisplayDescriptor) == false) ||
+                        mValueCombo.getSelectionIndex() != -1));
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/OccurrenceRenderer.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/OccurrenceRenderer.java
new file mode 100644
index 0000000..3af1447
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/OccurrenceRenderer.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2008 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.ddmuilib.log.event;
+
+import org.jfree.chart.axis.ValueAxis;
+import org.jfree.chart.plot.CrosshairState;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.plot.PlotRenderingInfo;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.XYItemRendererState;
+import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
+import org.jfree.data.time.TimeSeriesCollection;
+import org.jfree.data.xy.XYDataset;
+import org.jfree.ui.RectangleEdge;
+
+import java.awt.Graphics2D;
+import java.awt.Paint;
+import java.awt.Stroke;
+import java.awt.geom.Line2D;
+import java.awt.geom.Rectangle2D;
+
+/**
+ * Custom renderer to render event occurrence. This rendered ignores the y value, and simply
+ * draws a line from min to max at the time of the item.
+ */
+public class OccurrenceRenderer extends XYLineAndShapeRenderer {
+
+    private static final long serialVersionUID = 1L;
+
+    @Override
+    public void drawItem(Graphics2D g2, 
+                         XYItemRendererState state,
+                         Rectangle2D dataArea,
+                         PlotRenderingInfo info,
+                         XYPlot plot, 
+                         ValueAxis domainAxis, 
+                         ValueAxis rangeAxis,
+                         XYDataset dataset, 
+                         int series, 
+                         int item,
+                         CrosshairState crosshairState, 
+                         int pass) {
+        TimeSeriesCollection timeDataSet = (TimeSeriesCollection)dataset;
+        
+        // get the x value for the series/item.
+        double x = timeDataSet.getX(series, item).doubleValue();
+
+        // get the min/max of the range axis
+        double yMin = rangeAxis.getLowerBound();
+        double yMax = rangeAxis.getUpperBound();
+
+        RectangleEdge domainEdge = plot.getDomainAxisEdge();
+        RectangleEdge rangeEdge = plot.getRangeAxisEdge();
+
+        // convert the coordinates to java2d.
+        double x2D = domainAxis.valueToJava2D(x, dataArea, domainEdge);
+        double yMin2D = rangeAxis.valueToJava2D(yMin, dataArea, rangeEdge);
+        double yMax2D = rangeAxis.valueToJava2D(yMax, dataArea, rangeEdge);
+
+        // get the paint information for the series/item
+        Paint p = getItemPaint(series, item);
+        Stroke s = getItemStroke(series, item);
+        
+        Line2D line = null;
+        PlotOrientation orientation = plot.getOrientation();
+        if (orientation == PlotOrientation.HORIZONTAL) {
+            line = new Line2D.Double(yMin2D, x2D, yMax2D, x2D);
+        }
+        else if (orientation == PlotOrientation.VERTICAL) {
+            line = new Line2D.Double(x2D, yMin2D, x2D, yMax2D);
+        }
+        g2.setPaint(p);
+        g2.setStroke(s);
+        g2.draw(line);
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/SyncCommon.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/SyncCommon.java
new file mode 100644
index 0000000..0fa6f28
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/SyncCommon.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2009 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.ddmuilib.log.event;
+
+import com.android.ddmlib.log.EventContainer;
+import com.android.ddmlib.log.EventLogParser;
+import com.android.ddmlib.log.InvalidTypeException;
+
+import java.awt.Color;
+
+abstract public class SyncCommon extends EventDisplay {
+
+    // State information while processing the event stream
+    private int mLastState; // 0 if event started, 1 if event stopped
+    private long mLastStartTime; // ms
+    private long mLastStopTime; //ms
+    private String mLastDetails;
+    private int mLastSyncSource; // poll, server, user, etc.
+
+    // Some common variables for sync display.  These define the sync backends
+    //and how they should be displayed.
+    protected static final int CALENDAR = 0;
+    protected static final int GMAIL = 1;
+    protected static final int FEEDS = 2;
+    protected static final int CONTACTS = 3;
+    protected static final int ERRORS = 4;
+    protected static final int NUM_AUTHS = (CONTACTS + 1);
+    protected static final String AUTH_NAMES[] = {"Calendar", "Gmail", "Feeds", "Contacts",
+            "Errors"};
+    protected static final Color AUTH_COLORS[] = {Color.MAGENTA, Color.GREEN, Color.BLUE,
+            Color.ORANGE, Color.RED};
+
+    // Values from data/etc/event-log-tags
+    final int EVENT_SYNC = 2720;
+    final int EVENT_TICKLE = 2742;
+    final int EVENT_SYNC_DETAILS = 2743;
+    final int EVENT_CONTACTS_AGGREGATION = 2747;
+
+    protected SyncCommon(String name) {
+        super(name);
+    }
+
+    /**
+     * Resets the display.
+     */
+    @Override
+    void resetUI() {
+        mLastStartTime = 0;
+        mLastStopTime = 0;
+        mLastState = -1;
+        mLastSyncSource = -1;
+        mLastDetails = "";
+    }
+
+    /**
+     * Updates the display with a new event.  This is the main entry point for
+     * each event.  This method has the logic to tie together the start event,
+     * stop event, and details event into one graph item.  The combined sync event
+     * is handed to the subclass via processSycnEvent.  Note that the details
+     * can happen before or after the stop event.
+     *
+     * @param event     The event
+     * @param logParser The parser providing the event.
+     */
+    @Override
+    void newEvent(EventContainer event, EventLogParser logParser) {
+        try {
+            if (event.mTag == EVENT_SYNC) {
+                int state = Integer.parseInt(event.getValueAsString(1));
+                if (state == 0) { // start
+                    mLastStartTime = (long) event.sec * 1000L + (event.nsec / 1000000L);
+                    mLastState = 0;
+                    mLastSyncSource = Integer.parseInt(event.getValueAsString(2));                    
+                    mLastDetails = "";
+                } else if (state == 1) { // stop
+                    if (mLastState == 0) {
+                        mLastStopTime = (long) event.sec * 1000L + (event.nsec / 1000000L);
+                        if (mLastStartTime == 0) {
+                            // Log starts with a stop event
+                            mLastStartTime = mLastStopTime;
+                        }
+                        int auth = getAuth(event.getValueAsString(0));
+                        processSyncEvent(event, auth, mLastStartTime, mLastStopTime, mLastDetails,
+                                true, mLastSyncSource);
+                        mLastState = 1;
+                    }
+                }
+            } else if (event.mTag == EVENT_SYNC_DETAILS) {
+                mLastDetails = event.getValueAsString(3);
+                if (mLastState != 0) { // Not inside event
+                    long updateTime = (long) event.sec * 1000L + (event.nsec / 1000000L);
+                    if (updateTime - mLastStopTime <= 250) {
+                        // Got details within 250ms after event, so delete and re-insert
+                        // Details later than 250ms (arbitrary) are discarded as probably
+                        // unrelated.
+                        int auth = getAuth(event.getValueAsString(0));
+                        processSyncEvent(event, auth, mLastStartTime, mLastStopTime, mLastDetails,
+                                false, mLastSyncSource);
+                    }
+                }
+            } else if (event.mTag == EVENT_CONTACTS_AGGREGATION) {
+                long stopTime = (long) event.sec * 1000L + (event.nsec / 1000000L);
+                long startTime = stopTime - Long.parseLong(event.getValueAsString(0));
+                String details;
+                int count = Integer.parseInt(event.getValueAsString(1));
+                if (count < 0) {
+                    details = "g" + (-count);
+                } else {
+                    details = "G" + count;
+                }
+                processSyncEvent(event, CONTACTS, startTime, stopTime, details,
+                        true /* newEvent */, mLastSyncSource);
+            }
+        } catch (InvalidTypeException e) {
+        }
+    }
+
+    /**
+     * Callback hook for subclass to process a sync event.  newEvent has the logic
+     * to combine start and stop events and passes a processed event to the
+     * subclass.
+     *
+     * @param event     The sync event
+     * @param auth      The sync authority
+     * @param startTime Start time (ms) of events
+     * @param stopTime  Stop time (ms) of events
+     * @param details   Details associated with the event.
+     * @param newEvent  True if this event is a new sync event.  False if this event
+     * @param syncSource Poll, user, server, etc.
+     */
+    abstract void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime,
+            String details, boolean newEvent, int syncSource);
+     
+    /**
+     * Converts authority name to auth number.
+     *
+     * @param authname "calendar", etc.
+     * @return number series number associated with the authority
+     */
+    protected int getAuth(String authname) throws InvalidTypeException {
+        if ("calendar".equals(authname) || "cl".equals(authname) ||
+                "com.android.calendar".equals(authname)) {
+            return CALENDAR;
+        } else if ("contacts".equals(authname) || "cp".equals(authname) ||
+                "com.android.contacts".equals(authname)) {
+            return CONTACTS;
+        } else if ("subscribedfeeds".equals(authname)) {
+            return FEEDS;
+        } else if ("gmail-ls".equals(authname) || "mail".equals(authname)) {
+            return GMAIL;
+        } else if ("gmail-live".equals(authname)) {
+            return GMAIL;
+        } else if ("unknown".equals(authname)) {
+            return -1; // Unknown tickles; discard
+        } else {
+            throw new InvalidTypeException("Unknown authname " + authname);
+        }
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/EditFilterDialog.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/EditFilterDialog.java
new file mode 100644
index 0000000..0e302ce
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/EditFilterDialog.java
@@ -0,0 +1,397 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib.logcat;
+
+import com.android.ddmuilib.ImageLoader;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+/**
+ * Small dialog box to edit a static port number.
+ */
+public class EditFilterDialog extends Dialog {
+
+    private static final int DLG_WIDTH = 400;
+    private static final int DLG_HEIGHT = 260;
+
+    private static final String IMAGE_WARNING = "warning.png"; //$NON-NLS-1$
+    private static final String IMAGE_EMPTY = "empty.png"; //$NON-NLS-1$
+
+    private Shell mParent;
+
+    private Shell mShell;
+
+    private boolean mOk = false;
+
+    /**
+     * Filter being edited or created
+     */
+    private LogFilter mFilter;
+
+    private String mName;
+    private String mTag;
+    private String mPid;
+
+    /** Log level as an index of the drop-down combo
+     * @see getLogLevel
+     * @see getComboIndex
+     */
+    private int mLogLevel;
+
+    private Button mOkButton;
+
+    private Label mNameWarning;
+    private Label mTagWarning;
+    private Label mPidWarning;
+
+    public EditFilterDialog(Shell parent) {
+        super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL);
+    }
+
+    public EditFilterDialog(Shell shell, LogFilter filter) {
+        this(shell);
+        mFilter = filter;
+    }
+
+    /**
+     * Opens the dialog. The method will return when the user closes the dialog
+     * somehow.
+     *
+     * @return true if ok was pressed, false if cancelled.
+     */
+    public boolean open() {
+        createUI();
+
+        if (mParent == null || mShell == null) {
+            return false;
+        }
+
+        mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT);
+        Rectangle r = mParent.getBounds();
+        // get the center new top left.
+        int cx = r.x + r.width/2;
+        int x = cx - DLG_WIDTH / 2;
+        int cy = r.y + r.height/2;
+        int y = cy - DLG_HEIGHT / 2;
+        mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT);
+
+        mShell.open();
+
+        Display display = mParent.getDisplay();
+        while (!mShell.isDisposed()) {
+            if (!display.readAndDispatch())
+                display.sleep();
+        }
+
+        // we're quitting with OK.
+        // Lets update the filter if needed
+        if (mOk) {
+            // if it was a "Create filter" action we need to create it first.
+            if (mFilter == null) {
+                mFilter = new LogFilter(mName);
+            }
+
+            // setup the filter
+            mFilter.setTagMode(mTag);
+
+            if (mPid != null && mPid.length() > 0) {
+                mFilter.setPidMode(Integer.parseInt(mPid));
+            } else {
+                mFilter.setPidMode(-1);
+            }
+
+            mFilter.setLogLevel(getLogLevel(mLogLevel));
+        }
+
+        return mOk;
+    }
+
+    public LogFilter getFilter() {
+        return mFilter;
+    }
+
+    private void createUI() {
+        mParent = getParent();
+        mShell = new Shell(mParent, getStyle());
+        mShell.setText("Log Filter");
+
+        mShell.setLayout(new GridLayout(1, false));
+
+        mShell.addListener(SWT.Close, new Listener() {
+            @Override
+            public void handleEvent(Event event) {
+            }
+        });
+
+        // top part with the filter name
+        Composite nameComposite = new Composite(mShell, SWT.NONE);
+        nameComposite.setLayoutData(new GridData(GridData.FILL_BOTH));
+        nameComposite.setLayout(new GridLayout(3, false));
+
+        Label l = new Label(nameComposite, SWT.NONE);
+        l.setText("Filter Name:");
+
+        final Text filterNameText = new Text(nameComposite,
+                SWT.SINGLE | SWT.BORDER);
+        if (mFilter != null) {
+            mName = mFilter.getName();
+            if (mName != null) {
+                filterNameText.setText(mName);
+            }
+        }
+        filterNameText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        filterNameText.addModifyListener(new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent e) {
+                mName = filterNameText.getText().trim();
+                validate();
+            }
+        });
+
+        mNameWarning = new Label(nameComposite, SWT.NONE);
+        mNameWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_EMPTY,
+                mShell.getDisplay()));
+
+        // separator
+        l = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL);
+        l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+
+        // center part with the filter parameters
+        Composite main = new Composite(mShell, SWT.NONE);
+        main.setLayoutData(new GridData(GridData.FILL_BOTH));
+        main.setLayout(new GridLayout(3, false));
+
+        l = new Label(main, SWT.NONE);
+        l.setText("by Log Tag:");
+
+        final Text tagText = new Text(main, SWT.SINGLE | SWT.BORDER);
+        if (mFilter != null) {
+            mTag = mFilter.getTagFilter();
+            if (mTag != null) {
+                tagText.setText(mTag);
+            }
+        }
+
+        tagText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        tagText.addModifyListener(new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent e) {
+                mTag = tagText.getText().trim();
+                validate();
+            }
+        });
+
+        mTagWarning = new Label(main, SWT.NONE);
+        mTagWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_EMPTY,
+                mShell.getDisplay()));
+
+        l = new Label(main, SWT.NONE);
+        l.setText("by pid:");
+
+        final Text pidText = new Text(main, SWT.SINGLE | SWT.BORDER);
+        if (mFilter != null) {
+            if (mFilter.getPidFilter() != -1) {
+                mPid = Integer.toString(mFilter.getPidFilter());
+            } else {
+                mPid = "";
+            }
+            pidText.setText(mPid);
+        }
+        pidText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        pidText.addModifyListener(new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent e) {
+                mPid = pidText.getText().trim();
+                validate();
+            }
+        });
+
+        mPidWarning = new Label(main, SWT.NONE);
+        mPidWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_EMPTY,
+                mShell.getDisplay()));
+
+        l = new Label(main, SWT.NONE);
+        l.setText("by Log level:");
+
+        final Combo logCombo = new Combo(main, SWT.DROP_DOWN | SWT.READ_ONLY);
+        GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+        gd.horizontalSpan = 2;
+        logCombo.setLayoutData(gd);
+
+        // add the labels
+        logCombo.add("<none>");
+        logCombo.add("Error");
+        logCombo.add("Warning");
+        logCombo.add("Info");
+        logCombo.add("Debug");
+        logCombo.add("Verbose");
+
+        if (mFilter != null) {
+            mLogLevel = getComboIndex(mFilter.getLogLevel());
+            logCombo.select(mLogLevel);
+        } else {
+            logCombo.select(0);
+        }
+
+        logCombo.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                // get the selection
+                mLogLevel = logCombo.getSelectionIndex();
+                validate();
+            }
+        });
+
+        // separator
+        l = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL);
+        l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        // bottom part with the ok/cancel
+        Composite bottomComp = new Composite(mShell, SWT.NONE);
+        bottomComp
+                .setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER));
+        bottomComp.setLayout(new GridLayout(2, true));
+
+        mOkButton = new Button(bottomComp, SWT.NONE);
+        mOkButton.setText("OK");
+        mOkButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                mOk = true;
+                mShell.close();
+            }
+        });
+        mOkButton.setEnabled(false);
+        mShell.setDefaultButton(mOkButton);
+
+        Button cancelButton = new Button(bottomComp, SWT.NONE);
+        cancelButton.setText("Cancel");
+        cancelButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                mShell.close();
+            }
+        });
+
+        validate();
+    }
+
+    /**
+     * Returns the log level from a combo index.
+     * @param index the Combo index
+     * @return a log level valid for the Log class.
+     */
+    protected int getLogLevel(int index) {
+        if (index == 0) {
+            return -1;
+        }
+
+        return 7 - index;
+    }
+
+    /**
+     * Returns the index in the combo that matches the log level
+     * @param logLevel The Log level.
+     * @return the combo index
+     */
+    private int getComboIndex(int logLevel) {
+        if (logLevel == -1) {
+            return 0;
+        }
+
+        return 7 - logLevel;
+    }
+
+    /**
+     * Validates the content of the 2 text fields and enable/disable "ok", while
+     * setting up the warning/error message.
+     */
+    private void validate() {
+
+        boolean result = true;
+
+        // then we check it only contains digits.
+        if (mPid != null) {
+            if (mPid.matches("[0-9]*") == false) { //$NON-NLS-1$
+                mPidWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage(
+                        IMAGE_WARNING,
+                        mShell.getDisplay()));
+                mPidWarning.setToolTipText("PID must be a number"); //$NON-NLS-1$
+                result = false;
+            } else {
+                mPidWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage(
+                        IMAGE_EMPTY,
+                        mShell.getDisplay()));
+                mPidWarning.setToolTipText(null);
+            }
+        }
+
+        // then we check it not contains character | or :
+        if (mTag != null) {
+            if (mTag.matches(".*[:|].*") == true) { //$NON-NLS-1$
+                mTagWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage(
+                        IMAGE_WARNING,
+                        mShell.getDisplay()));
+                mTagWarning.setToolTipText("Tag cannot contain | or :"); //$NON-NLS-1$
+                result = false;
+            } else {
+                mTagWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage(
+                        IMAGE_EMPTY,
+                        mShell.getDisplay()));
+                mTagWarning.setToolTipText(null);
+            }
+        }
+
+        // then we check it not contains character | or :
+        if (mName != null && mName.length() > 0) {
+            if (mName.matches(".*[:|].*") == true) { //$NON-NLS-1$
+                mNameWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage(
+                        IMAGE_WARNING,
+                        mShell.getDisplay()));
+                mNameWarning.setToolTipText("Name cannot contain | or :"); //$NON-NLS-1$
+                result = false;
+            } else {
+                mNameWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage(
+                        IMAGE_EMPTY,
+                        mShell.getDisplay()));
+                mNameWarning.setToolTipText(null);
+            }
+        } else {
+            result = false;
+        }
+
+        mOkButton.setEnabled(result);
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatBufferChangeListener.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatBufferChangeListener.java
new file mode 100644
index 0000000..2804629
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatBufferChangeListener.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmlib.logcat.LogCatMessage;
+
+import java.util.List;
+
+/**
+ * Listeners interested in changes in the logcat buffer should implement this interface.
+ */
+public interface ILogCatBufferChangeListener {
+    /**
+     * Called when the logcat buffer changes.
+     * @param addedMessages list of messages that were added to the logcat buffer
+     * @param deletedMessages list of messages that were removed from the logcat buffer
+     */
+    void bufferChanged(List<LogCatMessage> addedMessages, List<LogCatMessage> deletedMessages);
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatMessageSelectionListener.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatMessageSelectionListener.java
new file mode 100644
index 0000000..728b518
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatMessageSelectionListener.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmlib.logcat.LogCatMessage;
+
+/**
+ * Classes interested in listening to user selection of logcat
+ * messages should implement this interface.
+ */
+public interface ILogCatMessageSelectionListener {
+    void messageDoubleClicked(LogCatMessage m);
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterContentProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterContentProvider.java
new file mode 100644
index 0000000..629b0e0
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterContentProvider.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmlib.logcat.LogCatFilter;
+
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+import java.util.List;
+
+/**
+ * A JFace content provider for logcat filter list, used in {@link LogCatPanel}.
+ */
+public final class LogCatFilterContentProvider implements IStructuredContentProvider {
+    @Override
+    public void dispose() {
+    }
+
+    @Override
+    public void inputChanged(Viewer arg0, Object arg1, Object arg2) {
+    }
+
+    /**
+     * Obtain the list of filters currently in use.
+     * @param model list of {@link LogCatFilter}'s
+     * @return array of {@link LogCatFilter} objects, or null.
+     */
+    @Override
+    public Object[] getElements(Object model) {
+        return ((List<?>) model).toArray();
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterData.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterData.java
new file mode 100644
index 0000000..dbc34d8
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterData.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2013 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.ddmuilib.logcat;
+
+import com.android.ddmlib.logcat.LogCatFilter;
+import com.android.ddmlib.logcat.LogCatMessage;
+
+import java.util.List;
+
+public class LogCatFilterData {
+    private final LogCatFilter mFilter;
+
+    /** Indicates the number of messages that match this filter, but have not
+     * yet been read by the user. This is really metadata about this filter
+     * necessary for the UI. If we ever end up needing to store more metadata,
+     * then it is probably better to move it out into a separate class. */
+    private int mUnreadCount;
+
+    /** Indicates that this filter is transient, and should not be persisted
+     * across Eclipse sessions. */
+    private boolean mTransient;
+
+    public LogCatFilterData(LogCatFilter f) {
+        mFilter = f;
+
+        // By default, all filters are persistent. Transient filters should explicitly
+        // mark it so by calling setTransient.
+        mTransient = false;
+    }
+
+    /**
+     * Update the unread count based on new messages received. The unread count
+     * is incremented by the count of messages in the received list that will be
+     * accepted by this filter.
+     * @param newMessages list of new messages.
+     */
+    public void updateUnreadCount(List<LogCatMessage> newMessages) {
+        for (LogCatMessage m : newMessages) {
+            if (mFilter.matches(m)) {
+                mUnreadCount++;
+            }
+        }
+    }
+
+    /**
+     * Reset count of unread messages.
+     */
+    public void resetUnreadCount() {
+        mUnreadCount = 0;
+    }
+
+    /**
+     * Get current value for the unread message counter.
+     */
+    public int getUnreadCount() {
+        return mUnreadCount;
+    }
+
+    /** Make this filter transient: It will not be persisted across sessions. */
+    public void setTransient() {
+        mTransient = true;
+    }
+
+    public boolean isTransient() {
+        return mTransient;
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterLabelProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterLabelProvider.java
new file mode 100644
index 0000000..fe24ddd
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterLabelProvider.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmlib.logcat.LogCatFilter;
+
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.swt.graphics.Image;
+
+import java.util.Map;
+
+/**
+ * A JFace label provider for the LogCat filters. It expects elements of type
+ * {@link LogCatFilter}.
+ */
+public final class LogCatFilterLabelProvider extends LabelProvider implements ITableLabelProvider {
+    private Map<LogCatFilter, LogCatFilterData> mFilterData;
+
+    public LogCatFilterLabelProvider(Map<LogCatFilter, LogCatFilterData> filterData) {
+        mFilterData = filterData;
+    }
+
+    @Override
+    public Image getColumnImage(Object arg0, int arg1) {
+        return null;
+    }
+
+    /**
+     * Implements {@link ITableLabelProvider#getColumnText(Object, int)}.
+     * @param element an instance of {@link LogCatFilter}
+     * @param index index of the column
+     * @return text to use in the column
+     */
+    @Override
+    public String getColumnText(Object element, int index) {
+        if (!(element instanceof LogCatFilter)) {
+            return null;
+        }
+
+        LogCatFilter f = (LogCatFilter) element;
+        LogCatFilterData fd = mFilterData.get(f);
+
+        if (fd != null && fd.getUnreadCount() > 0) {
+            return String.format("%s (%d)", f.getName(), fd.getUnreadCount());
+        } else {
+            return f.getName();
+        }
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsDialog.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsDialog.java
new file mode 100644
index 0000000..39b3fa9
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsDialog.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmlib.Log.LogLevel;
+
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.dialogs.TitleAreaDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * Dialog used to create or edit settings for a logcat filter.
+ */
+public final class LogCatFilterSettingsDialog extends TitleAreaDialog {
+    private static final String TITLE = "Logcat Message Filter Settings";
+    private static final String DEFAULT_MESSAGE =
+            "Filter logcat messages by the source's tag, pid or minimum log level.\n"
+            + "Empty fields will match all messages.";
+
+    private String mFilterName;
+    private String mTag;
+    private String mText;
+    private String mPid;
+    private String mAppName;
+    private String mLogLevel;
+
+    private Text mFilterNameText;
+    private Text mTagFilterText;
+    private Text mTextFilterText;
+    private Text mPidFilterText;
+    private Text mAppNameFilterText;
+    private Combo mLogLevelCombo;
+    private Button mOkButton;
+
+    /**
+     * Construct the filter settings dialog with default values for all fields.
+     * @param parentShell .
+     */
+    public LogCatFilterSettingsDialog(Shell parentShell) {
+        super(parentShell);
+        setDefaults("", "", "", "", "", LogLevel.VERBOSE);
+    }
+
+    /**
+     * Set the default values to show when the dialog is opened.
+     * @param filterName name for the filter.
+     * @param tag value for filter by tag
+     * @param text value for filter by text
+     * @param pid value for filter by pid
+     * @param appName value for filter by app name
+     * @param level value for filter by log level
+     */
+    public void setDefaults(String filterName, String tag, String text, String pid, String appName,
+            LogLevel level) {
+        mFilterName = filterName;
+        mTag = tag;
+        mText = text;
+        mPid = pid;
+        mAppName = appName;
+        mLogLevel = level.getStringValue();
+    }
+
+    @Override
+    protected Control createDialogArea(Composite shell) {
+        setTitle(TITLE);
+        setMessage(DEFAULT_MESSAGE);
+
+        Composite parent = (Composite) super.createDialogArea(shell);
+        Composite c = new Composite(parent, SWT.BORDER);
+        c.setLayout(new GridLayout(2, false));
+        c.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        createLabel(c, "Filter Name:");
+        mFilterNameText = new Text(c, SWT.BORDER);
+        mFilterNameText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mFilterNameText.setText(mFilterName);
+
+        createSeparator(c);
+
+        createLabel(c, "by Log Tag:");
+        mTagFilterText = new Text(c, SWT.BORDER);
+        mTagFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mTagFilterText.setText(mTag);
+
+        createLabel(c, "by Log Message:");
+        mTextFilterText = new Text(c, SWT.BORDER);
+        mTextFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mTextFilterText.setText(mText);
+
+        createLabel(c, "by PID:");
+        mPidFilterText = new Text(c, SWT.BORDER);
+        mPidFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mPidFilterText.setText(mPid);
+
+        createLabel(c, "by Application Name:");
+        mAppNameFilterText = new Text(c, SWT.BORDER);
+        mAppNameFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mAppNameFilterText.setText(mAppName);
+
+        createLabel(c, "by Log Level:");
+        mLogLevelCombo = new Combo(c, SWT.READ_ONLY | SWT.DROP_DOWN);
+        mLogLevelCombo.setItems(getLogLevels().toArray(new String[0]));
+        mLogLevelCombo.select(getLogLevels().indexOf(mLogLevel));
+
+        /* call validateDialog() whenever user modifies any text field */
+        ModifyListener m = new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent arg0) {
+                DialogStatus status = validateDialog();
+                mOkButton.setEnabled(status.valid);
+                setErrorMessage(status.message);
+            }
+        };
+        mFilterNameText.addModifyListener(m);
+        mTagFilterText.addModifyListener(m);
+        mTextFilterText.addModifyListener(m);
+        mPidFilterText.addModifyListener(m);
+        mAppNameFilterText.addModifyListener(m);
+
+        return c;
+    }
+
+
+    @Override
+    protected void createButtonsForButtonBar(Composite parent) {
+        super.createButtonsForButtonBar(parent);
+
+        mOkButton = getButton(IDialogConstants.OK_ID);
+
+        DialogStatus status = validateDialog();
+        mOkButton.setEnabled(status.valid);
+    }
+
+    /**
+     * A tuple that specifies whether the current state of the inputs
+     * on the dialog is valid or not. If it is not valid, the message
+     * field stores the reason why it isn't.
+     */
+    private static final class DialogStatus {
+        final boolean valid;
+        final String message;
+
+        private DialogStatus(boolean isValid, String errMessage) {
+            valid = isValid;
+            message = errMessage;
+        }
+    }
+
+    private DialogStatus validateDialog() {
+        /* check that there is some name for the filter */
+        if (mFilterNameText.getText().trim().equals("")) {
+            return new DialogStatus(false,
+                    "Please provide a name for this filter.");
+        }
+
+        /* if a pid is provided, it should be a +ve integer */
+        String pidText = mPidFilterText.getText().trim();
+        if (pidText.trim().length() > 0) {
+            int pid = 0;
+            try {
+                pid = Integer.parseInt(pidText);
+            } catch (NumberFormatException e) {
+                return new DialogStatus(false,
+                        "PID should be a positive integer.");
+            }
+
+            if (pid < 0) {
+                return new DialogStatus(false,
+                        "PID should be a positive integer.");
+            }
+        }
+
+        /* tag field must use a valid regex pattern */
+        String tagText = mTagFilterText.getText().trim();
+        if (tagText.trim().length() > 0) {
+            try {
+                Pattern.compile(tagText);
+            } catch (PatternSyntaxException e) {
+                return new DialogStatus(false,
+                        "Invalid regex used in tag field: " + e.getMessage());
+            }
+        }
+
+        /* text field must use a valid regex pattern */
+        String messageText = mTextFilterText.getText().trim();
+        if (messageText.trim().length() > 0) {
+            try {
+                Pattern.compile(messageText);
+            } catch (PatternSyntaxException e) {
+                return new DialogStatus(false,
+                        "Invalid regex used in text field: " + e.getMessage());
+            }
+        }
+
+        /* app name field must use a valid regex pattern */
+        String appNameText = mAppNameFilterText.getText().trim();
+        if (appNameText.trim().length() > 0) {
+            try {
+                Pattern.compile(appNameText);
+            } catch (PatternSyntaxException e) {
+                return new DialogStatus(false,
+                        "Invalid regex used in application name field: " + e.getMessage());
+            }
+        }
+
+        return new DialogStatus(true, null);
+    }
+
+    private void createSeparator(Composite c) {
+        Label l = new Label(c, SWT.SEPARATOR | SWT.HORIZONTAL);
+        GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+        gd.horizontalSpan = 2;
+        l.setLayoutData(gd);
+    }
+
+    private void createLabel(Composite c, String text) {
+        Label l = new Label(c, SWT.NONE);
+        l.setText(text);
+        GridData gd = new GridData();
+        gd.horizontalAlignment = SWT.RIGHT;
+        l.setLayoutData(gd);
+    }
+
+    @Override
+    protected void okPressed() {
+        /* save values from the widgets before the shell is closed. */
+        mFilterName = mFilterNameText.getText();
+        mTag = mTagFilterText.getText();
+        mText = mTextFilterText.getText();
+        mLogLevel = mLogLevelCombo.getText();
+        mPid = mPidFilterText.getText();
+        mAppName = mAppNameFilterText.getText();
+
+        super.okPressed();
+    }
+
+    /**
+     * Obtain the name for this filter.
+     * @return user provided filter name, maybe empty.
+     */
+    public String getFilterName() {
+        return mFilterName;
+    }
+
+    /**
+     * Obtain the tag regex to filter by.
+     * @return user provided tag regex, maybe empty.
+     */
+    public String getTag() {
+        return mTag;
+    }
+
+    /**
+     * Obtain the text regex to filter by.
+     * @return user provided tag regex, maybe empty.
+     */
+    public String getText() {
+        return mText;
+    }
+
+    /**
+     * Obtain user provided PID to filter by.
+     * @return user provided pid, maybe empty.
+     */
+    public String getPid() {
+        return mPid;
+    }
+
+    /**
+     * Obtain user provided application name to filter by.
+     * @return user provided app name regex, maybe empty
+     */
+    public String getAppName() {
+        return mAppName;
+    }
+
+    /**
+     * Obtain log level to filter by.
+     * @return log level string.
+     */
+    public String getLogLevel() {
+        return mLogLevel;
+    }
+
+    /**
+     * Obtain the string representation of all supported log levels.
+     * @return an array of strings, each representing a certain log level.
+     */
+    public static List<String> getLogLevels() {
+        List<String> logLevels = new ArrayList<String>();
+
+        for (LogLevel l : LogLevel.values()) {
+            logLevels.add(l.getStringValue());
+        }
+
+        return logLevels;
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializer.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializer.java
new file mode 100644
index 0000000..de35162
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializer.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmlib.logcat.LogCatFilter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Class to help save/restore user created filters.
+ *
+ * Users can create multiple filters in the logcat view. These filters could have regexes
+ * in their settings. All of the user created filters are saved into a single Eclipse
+ * preference. This class helps in generating the string to be saved given a list of
+ * {@link LogCatFilter}'s, and also does the reverse of creating the list of filters
+ * given the encoded string.
+ */
+public final class LogCatFilterSettingsSerializer {
+    private static final char SINGLE_QUOTE = '\'';
+    private static final char ESCAPE_CHAR = '\\';
+
+    private static final String ATTR_DELIM = ", ";
+    private static final String KW_DELIM = ": ";
+
+    private static final String KW_NAME = "name";
+    private static final String KW_TAG = "tag";
+    private static final String KW_TEXT = "text";
+    private static final String KW_PID = "pid";
+    private static final String KW_APP = "app";
+    private static final String KW_LOGLEVEL = "level";
+
+    /**
+     * Encode the settings from a list of {@link LogCatFilter}'s into a string for saving to
+     * the preference store. See
+     * {@link LogCatFilterSettingsSerializer#decodeFromPreferenceString(String)} for the
+     * reverse operation.
+     * @param filters list of filters to save.
+     * @param filterData mapping from filter to per filter UI data
+     * @return an encoded string that can be saved in Eclipse preference store. The encoded string
+     * is of a list of key:'value' pairs.
+     */
+    public String encodeToPreferenceString(List<LogCatFilter> filters,
+            Map<LogCatFilter, LogCatFilterData> filterData) {
+        StringBuffer sb = new StringBuffer();
+
+        for (LogCatFilter f : filters) {
+            LogCatFilterData fd = filterData.get(f);
+            if (fd != null && fd.isTransient()) {
+                // do not persist transient filters
+                continue;
+            }
+
+            sb.append(KW_NAME); sb.append(KW_DELIM); sb.append(quoteString(f.getName()));
+                                                                        sb.append(ATTR_DELIM);
+            sb.append(KW_TAG);  sb.append(KW_DELIM); sb.append(quoteString(f.getTag()));
+                                                                        sb.append(ATTR_DELIM);
+            sb.append(KW_TEXT); sb.append(KW_DELIM); sb.append(quoteString(f.getText()));
+                                                                        sb.append(ATTR_DELIM);
+            sb.append(KW_PID);  sb.append(KW_DELIM); sb.append(quoteString(f.getPid()));
+                                                                        sb.append(ATTR_DELIM);
+            sb.append(KW_APP);  sb.append(KW_DELIM); sb.append(quoteString(f.getAppName()));
+                                                                        sb.append(ATTR_DELIM);
+            sb.append(KW_LOGLEVEL); sb.append(KW_DELIM);
+                                       sb.append(quoteString(f.getLogLevel().getStringValue()));
+                                       sb.append(ATTR_DELIM);
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Decode an encoded string representing the settings of a list of logcat
+     * filters into a list of {@link LogCatFilter}'s.
+     * @param pref encoded preference string
+     * @return a list of {@link LogCatFilter}
+     */
+    public List<LogCatFilter> decodeFromPreferenceString(String pref) {
+        List<LogCatFilter> fs = new ArrayList<LogCatFilter>();
+
+        /* first split the string into a list of key, value pairs */
+        List<String> kv = getKeyValues(pref);
+        if (kv.size() == 0) {
+            return fs;
+        }
+
+        /* construct filter settings from the key value pairs */
+        int index = 0;
+        while (index < kv.size()) {
+            String name = "";
+            String tag = "";
+            String pid = "";
+            String app = "";
+            String text = "";
+            LogLevel level = LogLevel.VERBOSE;
+
+            assert kv.get(index).equals(KW_NAME);
+            name = kv.get(index + 1);
+
+            index += 2;
+            while (index < kv.size() && !kv.get(index).equals(KW_NAME)) {
+                String key = kv.get(index);
+                String value = kv.get(index + 1);
+                index += 2;
+
+                if (key.equals(KW_TAG)) {
+                    tag = value;
+                } else if (key.equals(KW_TEXT)) {
+                    text = value;
+                } else if (key.equals(KW_PID)) {
+                    pid = value;
+                } else if (key.equals(KW_APP)) {
+                    app = value;
+                } else if (key.equals(KW_LOGLEVEL)) {
+                    level = LogLevel.getByString(value);
+                }
+            }
+
+            fs.add(new LogCatFilter(name, tag, text, pid, app, level));
+        }
+
+        return fs;
+    }
+
+    private List<String> getKeyValues(String pref) {
+        List<String> kv = new ArrayList<String>();
+        int index = 0;
+        while (index < pref.length()) {
+            String kw = getKeyword(pref.substring(index));
+            if (kw == null) {
+                break;
+            }
+            index += kw.length() + KW_DELIM.length();
+
+            String value = getNextString(pref.substring(index));
+            index += value.length() + ATTR_DELIM.length();
+
+            value = unquoteString(value);
+
+            kv.add(kw);
+            kv.add(value);
+        }
+
+        return kv;
+    }
+
+    /**
+     * Enclose a string in quotes, escaping all the quotes within the string.
+     */
+    private String quoteString(String s) {
+        return SINGLE_QUOTE + s.replace(Character.toString(SINGLE_QUOTE), "\\'")
+                + SINGLE_QUOTE;
+    }
+
+    /**
+     * Recover original string from its escaped version created using
+     * {@link LogCatFilterSettingsSerializer#quoteString(String)}.
+     */
+    private String unquoteString(String s) {
+        s = s.substring(1, s.length() - 1); /* remove start and end QUOTES */
+        return s.replace("\\'", Character.toString(SINGLE_QUOTE));
+    }
+
+    private String getKeyword(String pref) {
+        int kwlen = pref.indexOf(KW_DELIM);
+        if (kwlen == -1) {
+            return null;
+        }
+
+        return pref.substring(0, kwlen);
+    }
+
+    /**
+     * Get the next quoted string from the input stream of characters.
+     */
+    private String getNextString(String s) {
+        assert s.charAt(0) == SINGLE_QUOTE;
+
+        StringBuffer sb = new StringBuffer();
+
+        int index = 0;
+        while (index < s.length()) {
+            sb.append(s.charAt(index));
+
+            if (index > 0
+                    && s.charAt(index) == SINGLE_QUOTE          // current char is a single quote
+                    && s.charAt(index - 1) != ESCAPE_CHAR) {    // prev char wasn't a backslash
+                /* break if an unescaped SINGLE QUOTE (end of string) is seen */
+                break;
+            }
+
+            index++;
+        }
+
+        return sb.toString();
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatMessageList.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatMessageList.java
new file mode 100644
index 0000000..c5cd548
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatMessageList.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmlib.logcat.LogCatMessage;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+
+/**
+ * Container for a list of log messages. The list of messages are
+ * maintained in a circular buffer (FIFO).
+ */
+public final class LogCatMessageList {
+    /** Preference key for size of the FIFO. */
+    public static final String MAX_MESSAGES_PREFKEY =
+            "logcat.messagelist.max.size";
+
+    /** Default value for max # of messages. */
+    public static final int MAX_MESSAGES_DEFAULT = 5000;
+
+    private int mFifoSize;
+    private BlockingQueue<LogCatMessage> mQ;
+
+    /**
+     * Construct an empty message list.
+     * @param maxMessages capacity of the circular buffer
+     */
+    public LogCatMessageList(int maxMessages) {
+        mFifoSize = maxMessages;
+
+        mQ = new ArrayBlockingQueue<LogCatMessage>(mFifoSize);
+    }
+
+    /**
+     * Resize the message list.
+     * @param n new size for the list
+     */
+    public synchronized void resize(int n) {
+        mFifoSize = n;
+
+        if (mFifoSize > mQ.size()) {
+            /* if resizing to a bigger fifo, we can copy over all elements from the current mQ */
+            mQ = new ArrayBlockingQueue<LogCatMessage>(mFifoSize, true, mQ);
+        } else {
+            /* for a smaller fifo, copy over the last n entries */
+            LogCatMessage[] curMessages = mQ.toArray(new LogCatMessage[mQ.size()]);
+            mQ = new ArrayBlockingQueue<LogCatMessage>(mFifoSize);
+            for (int i = curMessages.length - mFifoSize; i < curMessages.length; i++) {
+                mQ.offer(curMessages[i]);
+            }
+        }
+    }
+
+    /**
+     * Append a message to the list. If the list is full, the first
+     * message will be popped off of it.
+     * @param m log to be inserted
+     */
+    public synchronized void appendMessages(final List<LogCatMessage> messages) {
+        ensureSpace(messages.size());
+        for (LogCatMessage m: messages) {
+            mQ.offer(m);
+        }
+    }
+
+    /**
+     * Ensure that there is sufficient space for given number of messages.
+     * @return list of messages that were deleted to create additional space.
+     */
+    public synchronized List<LogCatMessage> ensureSpace(int messageCount) {
+        List<LogCatMessage> l = new ArrayList<LogCatMessage>(messageCount);
+
+        while (mQ.remainingCapacity() < messageCount) {
+            l.add(mQ.poll());
+        }
+
+        return l;
+    }
+
+    /**
+     * Returns the number of additional elements that this queue can
+     * ideally (in the absence of memory or resource constraints)
+     * accept without blocking.
+     * @return the remaining capacity
+     */
+    public synchronized int remainingCapacity() {
+        return mQ.remainingCapacity();
+    }
+
+    /** Clear all messages in the list. */
+    public synchronized void clear() {
+        mQ.clear();
+    }
+
+    /** Obtain a copy of the message list. */
+    public synchronized List<LogCatMessage> getAllMessages() {
+        return new ArrayList<LogCatMessage>(mQ);
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatPanel.java
new file mode 100644
index 0000000..bda742c
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatPanel.java
@@ -0,0 +1,1607 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmlib.DdmConstants;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmlib.logcat.LogCatFilter;
+import com.android.ddmlib.logcat.LogCatMessage;
+import com.android.ddmuilib.AbstractBufferFindTarget;
+import com.android.ddmuilib.FindDialog;
+import com.android.ddmuilib.ITableFocusListener;
+import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator;
+import com.android.ddmuilib.ImageLoader;
+import com.android.ddmuilib.SelectionDependentPanel;
+import com.android.ddmuilib.TableHelper;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.preference.PreferenceConverter;
+import org.eclipse.jface.util.IPropertyChangeListener;
+import org.eclipse.jface.util.PropertyChangeEvent;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.ScrollBar;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.TableItem;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.swt.widgets.ToolItem;
+
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * LogCatPanel displays a table listing the logcat messages.
+ */
+public final class LogCatPanel extends SelectionDependentPanel
+                        implements ILogCatBufferChangeListener {
+    /** Preference key to use for storing list of logcat filters. */
+    public static final String LOGCAT_FILTERS_LIST = "logcat.view.filters.list";
+
+    /** Preference key to use for storing font settings. */
+    public static final String LOGCAT_VIEW_FONT_PREFKEY = "logcat.view.font";
+
+    /** Preference key to use for deciding whether to automatically en/disable scroll lock. */
+    public static final String AUTO_SCROLL_LOCK_PREFKEY = "logcat.view.auto-scroll-lock";
+
+    // Preference keys for message colors based on severity level
+    private static final String MSG_COLOR_PREFKEY_PREFIX = "logcat.msg.color.";
+    public static final String VERBOSE_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "verbose"; //$NON-NLS-1$
+    public static final String DEBUG_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "debug"; //$NON-NLS-1$
+    public static final String INFO_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "info"; //$NON-NLS-1$
+    public static final String WARN_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "warn"; //$NON-NLS-1$
+    public static final String ERROR_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "error"; //$NON-NLS-1$
+    public static final String ASSERT_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "assert"; //$NON-NLS-1$
+
+    // Use a monospace font family
+    private static final String FONT_FAMILY =
+            DdmConstants.CURRENT_PLATFORM == DdmConstants.PLATFORM_DARWIN ? "Monaco":"Courier New";
+
+    // Use the default system font size
+    private static final FontData DEFAULT_LOGCAT_FONTDATA;
+    static {
+        int h = Display.getDefault().getSystemFont().getFontData()[0].getHeight();
+        DEFAULT_LOGCAT_FONTDATA = new FontData(FONT_FAMILY, h, SWT.NORMAL);
+    }
+
+    private static final String LOGCAT_VIEW_COLSIZE_PREFKEY_PREFIX = "logcat.view.colsize.";
+    private static final String DISPLAY_FILTERS_COLUMN_PREFKEY = "logcat.view.display.filters";
+
+    /** Default message to show in the message search field. */
+    private static final String DEFAULT_SEARCH_MESSAGE =
+            "Search for messages. Accepts Java regexes. "
+            + "Prefix with pid:, app:, tag: or text: to limit scope.";
+
+    /** Tooltip to show in the message search field. */
+    private static final String DEFAULT_SEARCH_TOOLTIP =
+            "Example search patterns:\n"
+          + "    sqlite (search for sqlite in text field)\n"
+          + "    app:browser (search for messages generated by the browser application)";
+
+    private static final String IMAGE_ADD_FILTER = "add.png"; //$NON-NLS-1$
+    private static final String IMAGE_DELETE_FILTER = "delete.png"; //$NON-NLS-1$
+    private static final String IMAGE_EDIT_FILTER = "edit.png"; //$NON-NLS-1$
+    private static final String IMAGE_SAVE_LOG_TO_FILE = "save.png"; //$NON-NLS-1$
+    private static final String IMAGE_CLEAR_LOG = "clear.png"; //$NON-NLS-1$
+    private static final String IMAGE_DISPLAY_FILTERS = "displayfilters.png"; //$NON-NLS-1$
+    private static final String IMAGE_SCROLL_LOCK = "scroll_lock.png"; //$NON-NLS-1$
+
+    private static final int[] WEIGHTS_SHOW_FILTERS = new int[] {15, 85};
+    private static final int[] WEIGHTS_LOGCAT_ONLY = new int[] {0, 100};
+
+    /** Index of the default filter in the saved filters column. */
+    private static final int DEFAULT_FILTER_INDEX = 0;
+
+    /* Text colors for the filter box */
+    private static final Color VALID_FILTER_REGEX_COLOR =
+            Display.getDefault().getSystemColor(SWT.COLOR_BLACK);
+    private static final Color INVALID_FILTER_REGEX_COLOR =
+            Display.getDefault().getSystemColor(SWT.COLOR_RED);
+
+    private LogCatReceiver mReceiver;
+    private IPreferenceStore mPrefStore;
+
+    private List<LogCatFilter> mLogCatFilters;
+    private Map<LogCatFilter, LogCatFilterData> mLogCatFilterData;
+    private int mCurrentSelectedFilterIndex;
+
+    private ToolItem mNewFilterToolItem;
+    private ToolItem mDeleteFilterToolItem;
+    private ToolItem mEditFilterToolItem;
+    private TableViewer mFiltersTableViewer;
+
+    private Combo mLiveFilterLevelCombo;
+    private Text mLiveFilterText;
+
+    private List<LogCatFilter> mCurrentFilters = Collections.emptyList();
+
+    private Table mTable;
+
+    private boolean mShouldScrollToLatestLog = true;
+    private ToolItem mScrollLockCheckBox;
+    private boolean mAutoScrollLock;
+
+    // Lock under which the vertical scroll bar listener should be added
+    private final Object mScrollBarSelectionListenerLock = new Object();
+    private SelectionListener mScrollBarSelectionListener;
+    private boolean mScrollBarListenerSet = false;
+
+    private String mLogFileExportFolder;
+
+    private Font mFont;
+    private int mWrapWidthInChars;
+
+    private Color mVerboseColor;
+    private Color mDebugColor;
+    private Color mInfoColor;
+    private Color mWarnColor;
+    private Color mErrorColor;
+    private Color mAssertColor;
+
+    private SashForm mSash;
+
+    // messages added since last refresh, synchronized on mLogBuffer
+    private List<LogCatMessage> mLogBuffer;
+
+    // # of messages deleted since last refresh, synchronized on mLogBuffer
+    private int mDeletedLogCount;
+
+    /**
+     * Construct a logcat panel.
+     * @param prefStore preference store where UI preferences will be saved
+     */
+    public LogCatPanel(IPreferenceStore prefStore) {
+        mPrefStore = prefStore;
+        mLogBuffer = new ArrayList<LogCatMessage>(LogCatMessageList.MAX_MESSAGES_DEFAULT);
+
+        initializeFilters();
+
+        setupDefaultPreferences();
+        initializePreferenceUpdateListeners();
+
+        mFont = getFontFromPrefStore();
+        loadMessageColorPreferences();
+        mAutoScrollLock = mPrefStore.getBoolean(AUTO_SCROLL_LOCK_PREFKEY);
+    }
+
+    private void loadMessageColorPreferences() {
+        if (mVerboseColor != null) {
+            disposeMessageColors();
+        }
+
+        mVerboseColor = getColorFromPrefStore(VERBOSE_COLOR_PREFKEY);
+        mDebugColor = getColorFromPrefStore(DEBUG_COLOR_PREFKEY);
+        mInfoColor = getColorFromPrefStore(INFO_COLOR_PREFKEY);
+        mWarnColor = getColorFromPrefStore(WARN_COLOR_PREFKEY);
+        mErrorColor = getColorFromPrefStore(ERROR_COLOR_PREFKEY);
+        mAssertColor = getColorFromPrefStore(ASSERT_COLOR_PREFKEY);
+    }
+
+    private void initializeFilters() {
+        mLogCatFilters = new ArrayList<LogCatFilter>();
+        mLogCatFilterData = new ConcurrentHashMap<LogCatFilter, LogCatFilterData>();
+
+        /* add default filter matching all messages */
+        String tag = "";
+        String text = "";
+        String pid = "";
+        String app = "";
+        LogCatFilter defaultFilter = new LogCatFilter("All messages (no filters)",
+                tag, text, pid, app, LogLevel.VERBOSE);
+
+        mLogCatFilters.add(defaultFilter);
+        mLogCatFilterData.put(defaultFilter, new LogCatFilterData(defaultFilter));
+
+        /* restore saved filters from prefStore */
+        List<LogCatFilter> savedFilters = getSavedFilters();
+        for (LogCatFilter f: savedFilters) {
+            mLogCatFilters.add(f);
+            mLogCatFilterData.put(f, new LogCatFilterData(f));
+        }
+    }
+
+    private void setupDefaultPreferences() {
+        PreferenceConverter.setDefault(mPrefStore, LogCatPanel.LOGCAT_VIEW_FONT_PREFKEY,
+                DEFAULT_LOGCAT_FONTDATA);
+        mPrefStore.setDefault(LogCatMessageList.MAX_MESSAGES_PREFKEY,
+                LogCatMessageList.MAX_MESSAGES_DEFAULT);
+        mPrefStore.setDefault(DISPLAY_FILTERS_COLUMN_PREFKEY, true);
+        mPrefStore.setDefault(AUTO_SCROLL_LOCK_PREFKEY, true);
+
+        /* Default Colors for different log levels. */
+        PreferenceConverter.setDefault(mPrefStore, LogCatPanel.VERBOSE_COLOR_PREFKEY,
+                new RGB(0, 0, 0));
+        PreferenceConverter.setDefault(mPrefStore, LogCatPanel.DEBUG_COLOR_PREFKEY,
+                new RGB(0, 0, 127));
+        PreferenceConverter.setDefault(mPrefStore, LogCatPanel.INFO_COLOR_PREFKEY,
+                new RGB(0, 127, 0));
+        PreferenceConverter.setDefault(mPrefStore, LogCatPanel.WARN_COLOR_PREFKEY,
+                new RGB(255, 127, 0));
+        PreferenceConverter.setDefault(mPrefStore, LogCatPanel.ERROR_COLOR_PREFKEY,
+                new RGB(255, 0, 0));
+        PreferenceConverter.setDefault(mPrefStore, LogCatPanel.ASSERT_COLOR_PREFKEY,
+                new RGB(255, 0, 0));
+    }
+
+    private void initializePreferenceUpdateListeners() {
+        mPrefStore.addPropertyChangeListener(new IPropertyChangeListener() {
+            @Override
+            public void propertyChange(PropertyChangeEvent event) {
+                String changedProperty = event.getProperty();
+                if (changedProperty.equals(LogCatPanel.LOGCAT_VIEW_FONT_PREFKEY)) {
+                    if (mFont != null) {
+                        mFont.dispose();
+                    }
+                    mFont = getFontFromPrefStore();
+                    recomputeWrapWidth();
+                    Display.getDefault().syncExec(new Runnable() {
+                        @Override
+                        public void run() {
+                            for (TableItem it: mTable.getItems()) {
+                                it.setFont(mFont);
+                            }
+                        }
+                    });
+                } else if (changedProperty.startsWith(MSG_COLOR_PREFKEY_PREFIX)) {
+                    loadMessageColorPreferences();
+                    Display.getDefault().syncExec(new Runnable() {
+                       @Override
+                       public void run() {
+                           Color c = mVerboseColor;
+                           for (TableItem it: mTable.getItems()) {
+                               Object data = it.getData();
+                               if (data instanceof LogCatMessage) {
+                                   c = getForegroundColor((LogCatMessage) data);
+                               }
+                               it.setForeground(c);
+                           }
+                       }
+                    });
+                } else if (changedProperty.equals(LogCatMessageList.MAX_MESSAGES_PREFKEY)) {
+                    mReceiver.resizeFifo(mPrefStore.getInt(
+                            LogCatMessageList.MAX_MESSAGES_PREFKEY));
+                    reloadLogBuffer();
+                } else if (changedProperty.equals(AUTO_SCROLL_LOCK_PREFKEY)) {
+                    mAutoScrollLock = mPrefStore.getBoolean(AUTO_SCROLL_LOCK_PREFKEY);
+                }
+            }
+        });
+    }
+
+    private void saveFilterPreferences() {
+        LogCatFilterSettingsSerializer serializer = new LogCatFilterSettingsSerializer();
+
+        /* save all filter settings except the first one which is the default */
+        String e = serializer.encodeToPreferenceString(
+                mLogCatFilters.subList(1, mLogCatFilters.size()), mLogCatFilterData);
+        mPrefStore.setValue(LOGCAT_FILTERS_LIST, e);
+    }
+
+    private List<LogCatFilter> getSavedFilters() {
+        LogCatFilterSettingsSerializer serializer = new LogCatFilterSettingsSerializer();
+        String e = mPrefStore.getString(LOGCAT_FILTERS_LIST);
+        return serializer.decodeFromPreferenceString(e);
+    }
+
+    @Override
+    public void deviceSelected() {
+        IDevice device = getCurrentDevice();
+        if (device == null) {
+            // If the device is not working properly, getCurrentDevice() could return null.
+            // In such a case, we don't launch logcat, nor switch the display.
+            return;
+        }
+
+        if (mReceiver != null) {
+            // Don't need to listen to new logcat messages from previous device anymore.
+            mReceiver.removeMessageReceivedEventListener(this);
+
+            // When switching between devices, existing filter match count should be reset.
+            for (LogCatFilter f : mLogCatFilters) {
+                LogCatFilterData fd = mLogCatFilterData.get(f);
+                fd.resetUnreadCount();
+            }
+        }
+
+        mReceiver = LogCatReceiverFactory.INSTANCE.newReceiver(device, mPrefStore);
+        mReceiver.addMessageReceivedEventListener(this);
+        reloadLogBuffer();
+
+        // Always scroll to last line whenever the selected device changes.
+        // Run this in a separate async thread to give the table some time to update after the
+        // setInput above.
+        Display.getDefault().asyncExec(new Runnable() {
+            @Override
+            public void run() {
+                scrollToLatestLog();
+            }
+        });
+    }
+
+    @Override
+    public void clientSelected() {
+    }
+
+    @Override
+    protected void postCreation() {
+    }
+
+    @Override
+    protected Control createControl(Composite parent) {
+        GridLayout layout = new GridLayout(1, false);
+        parent.setLayout(layout);
+
+        createViews(parent);
+        setupDefaults();
+
+        return null;
+    }
+
+    private void createViews(Composite parent) {
+        mSash = createSash(parent);
+
+        createListOfFilters(mSash);
+        createLogTableView(mSash);
+
+        boolean showFilters = mPrefStore.getBoolean(DISPLAY_FILTERS_COLUMN_PREFKEY);
+        updateFiltersColumn(showFilters);
+    }
+
+    private SashForm createSash(Composite parent) {
+        SashForm sash = new SashForm(parent, SWT.HORIZONTAL);
+        sash.setLayoutData(new GridData(GridData.FILL_BOTH));
+        return sash;
+    }
+
+    private void createListOfFilters(SashForm sash) {
+        Composite c = new Composite(sash, SWT.BORDER);
+        GridLayout layout = new GridLayout(2, false);
+        c.setLayout(layout);
+        c.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        createFiltersToolbar(c);
+        createFiltersTable(c);
+    }
+
+    private void createFiltersToolbar(Composite parent) {
+        Label l = new Label(parent, SWT.NONE);
+        l.setText("Saved Filters");
+        GridData gd = new GridData();
+        gd.horizontalAlignment = SWT.LEFT;
+        l.setLayoutData(gd);
+
+        ToolBar t = new ToolBar(parent, SWT.FLAT);
+        gd = new GridData();
+        gd.horizontalAlignment = SWT.RIGHT;
+        t.setLayoutData(gd);
+
+        /* new filter */
+        mNewFilterToolItem = new ToolItem(t, SWT.PUSH);
+        mNewFilterToolItem.setImage(
+                ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_ADD_FILTER, t.getDisplay()));
+        mNewFilterToolItem.setToolTipText("Add a new logcat filter");
+        mNewFilterToolItem.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                addNewFilter();
+            }
+        });
+
+        /* delete filter */
+        mDeleteFilterToolItem = new ToolItem(t, SWT.PUSH);
+        mDeleteFilterToolItem.setImage(
+                ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_DELETE_FILTER, t.getDisplay()));
+        mDeleteFilterToolItem.setToolTipText("Delete selected logcat filter");
+        mDeleteFilterToolItem.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                deleteSelectedFilter();
+            }
+        });
+
+        /* edit filter */
+        mEditFilterToolItem = new ToolItem(t, SWT.PUSH);
+        mEditFilterToolItem.setImage(
+                ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_EDIT_FILTER, t.getDisplay()));
+        mEditFilterToolItem.setToolTipText("Edit selected logcat filter");
+        mEditFilterToolItem.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                editSelectedFilter();
+            }
+        });
+    }
+
+    private void addNewFilter(String defaultTag, String defaultText, String defaultPid,
+            String defaultAppName, LogLevel defaultLevel) {
+        LogCatFilterSettingsDialog d = new LogCatFilterSettingsDialog(
+                Display.getCurrent().getActiveShell());
+        d.setDefaults("", defaultTag, defaultText, defaultPid, defaultAppName, defaultLevel);
+        if (d.open() != Window.OK) {
+            return;
+        }
+
+        LogCatFilter f = new LogCatFilter(d.getFilterName().trim(),
+                d.getTag().trim(),
+                d.getText().trim(),
+                d.getPid().trim(),
+                d.getAppName().trim(),
+                LogLevel.getByString(d.getLogLevel()));
+
+        mLogCatFilters.add(f);
+        mLogCatFilterData.put(f, new LogCatFilterData(f));
+        mFiltersTableViewer.refresh();
+
+        /* select the newly added entry */
+        int idx = mLogCatFilters.size() - 1;
+        mFiltersTableViewer.getTable().setSelection(idx);
+
+        filterSelectionChanged();
+        saveFilterPreferences();
+    }
+
+    private void addNewFilter() {
+        addNewFilter("", "", "", "", LogLevel.VERBOSE);
+    }
+
+    private void deleteSelectedFilter() {
+        int selectedIndex = mFiltersTableViewer.getTable().getSelectionIndex();
+        if (selectedIndex <= 0) {
+            /* return if no selected filter, or the default filter was selected (0th). */
+            return;
+        }
+
+        LogCatFilter f = mLogCatFilters.get(selectedIndex);
+        mLogCatFilters.remove(selectedIndex);
+        mLogCatFilterData.remove(f);
+
+        mFiltersTableViewer.refresh();
+        mFiltersTableViewer.getTable().setSelection(selectedIndex - 1);
+
+        filterSelectionChanged();
+        saveFilterPreferences();
+    }
+
+    private void editSelectedFilter() {
+        int selectedIndex = mFiltersTableViewer.getTable().getSelectionIndex();
+        if (selectedIndex < 0) {
+            return;
+        }
+
+        LogCatFilter curFilter = mLogCatFilters.get(selectedIndex);
+
+        LogCatFilterSettingsDialog dialog = new LogCatFilterSettingsDialog(
+                Display.getCurrent().getActiveShell());
+        dialog.setDefaults(curFilter.getName(), curFilter.getTag(), curFilter.getText(),
+                curFilter.getPid(), curFilter.getAppName(), curFilter.getLogLevel());
+        if (dialog.open() != Window.OK) {
+            return;
+        }
+
+        LogCatFilter f = new LogCatFilter(dialog.getFilterName(),
+                dialog.getTag(),
+                dialog.getText(),
+                dialog.getPid(),
+                dialog.getAppName(),
+                LogLevel.getByString(dialog.getLogLevel()));
+        mLogCatFilters.set(selectedIndex, f);
+        mFiltersTableViewer.refresh();
+
+        mFiltersTableViewer.getTable().setSelection(selectedIndex);
+        filterSelectionChanged();
+        saveFilterPreferences();
+    }
+
+    /**
+     * Select the transient filter for the specified application. If no such filter
+     * exists, then create one and then select that. This method should be called from
+     * the UI thread.
+     * @param appName application name to filter by
+     */
+    public void selectTransientAppFilter(String appName) {
+        assert mTable.getDisplay().getThread() == Thread.currentThread();
+
+        LogCatFilter f = findTransientAppFilter(appName);
+        if (f == null) {
+            f = createTransientAppFilter(appName);
+            mLogCatFilters.add(f);
+
+            LogCatFilterData fd = new LogCatFilterData(f);
+            fd.setTransient();
+            mLogCatFilterData.put(f, fd);
+        }
+
+        selectFilterAt(mLogCatFilters.indexOf(f));
+    }
+
+    private LogCatFilter findTransientAppFilter(String appName) {
+        for (LogCatFilter f : mLogCatFilters) {
+            LogCatFilterData fd = mLogCatFilterData.get(f);
+            if (fd != null && fd.isTransient() && f.getAppName().equals(appName)) {
+                return f;
+            }
+        }
+        return null;
+    }
+
+    private LogCatFilter createTransientAppFilter(String appName) {
+        LogCatFilter f = new LogCatFilter(appName + " (Session Filter)",
+                "",
+                "",
+                "",
+                appName,
+                LogLevel.VERBOSE);
+        return f;
+    }
+
+    private void selectFilterAt(final int index) {
+        mFiltersTableViewer.refresh();
+
+        if (index != mFiltersTableViewer.getTable().getSelectionIndex()) {
+            mFiltersTableViewer.getTable().setSelection(index);
+            filterSelectionChanged();
+        }
+    }
+
+    private void createFiltersTable(Composite parent) {
+        final Table table = new Table(parent, SWT.FULL_SELECTION);
+
+        GridData gd = new GridData(GridData.FILL_BOTH);
+        gd.horizontalSpan = 2;
+        table.setLayoutData(gd);
+
+        mFiltersTableViewer = new TableViewer(table);
+        mFiltersTableViewer.setContentProvider(new LogCatFilterContentProvider());
+        mFiltersTableViewer.setLabelProvider(new LogCatFilterLabelProvider(mLogCatFilterData));
+        mFiltersTableViewer.setInput(mLogCatFilters);
+
+        mFiltersTableViewer.getTable().addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent event) {
+                filterSelectionChanged();
+            }
+
+            @Override
+            public void widgetDefaultSelected(SelectionEvent arg0) {
+                editSelectedFilter();
+            }
+        });
+    }
+
+    private void createLogTableView(SashForm sash) {
+        Composite c = new Composite(sash, SWT.NONE);
+        c.setLayout(new GridLayout());
+        c.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        createLiveFilters(c);
+        createLogcatViewTable(c);
+    }
+
+    /** Create the search bar at the top of the logcat messages table. */
+    private void createLiveFilters(Composite parent) {
+        Composite c = new Composite(parent, SWT.NONE);
+        c.setLayout(new GridLayout(3, false));
+        c.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        mLiveFilterText = new Text(c, SWT.BORDER | SWT.SEARCH);
+        mLiveFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mLiveFilterText.setMessage(DEFAULT_SEARCH_MESSAGE);
+        mLiveFilterText.setToolTipText(DEFAULT_SEARCH_TOOLTIP);
+        mLiveFilterText.addModifyListener(new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent arg0) {
+                updateFilterTextColor();
+                updateAppliedFilters();
+            }
+        });
+
+        mLiveFilterLevelCombo = new Combo(c, SWT.READ_ONLY | SWT.DROP_DOWN);
+        mLiveFilterLevelCombo.setItems(
+                LogCatFilterSettingsDialog.getLogLevels().toArray(new String[0]));
+        mLiveFilterLevelCombo.select(0);
+        mLiveFilterLevelCombo.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                updateAppliedFilters();
+            }
+        });
+
+        ToolBar toolBar = new ToolBar(c, SWT.FLAT);
+
+        ToolItem saveToLog = new ToolItem(toolBar, SWT.PUSH);
+        saveToLog.setImage(ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_SAVE_LOG_TO_FILE,
+                toolBar.getDisplay()));
+        saveToLog.setToolTipText("Export Selected Items To Text File..");
+        saveToLog.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                saveLogToFile();
+            }
+        });
+
+        ToolItem clearLog = new ToolItem(toolBar, SWT.PUSH);
+        clearLog.setImage(
+                ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_CLEAR_LOG, toolBar.getDisplay()));
+        clearLog.setToolTipText("Clear Log");
+        clearLog.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                if (mReceiver != null) {
+                    mReceiver.clearMessages();
+                    refreshLogCatTable();
+                    resetUnreadCountForAllFilters();
+
+                    // the filters view is not cleared unless the filters are re-applied.
+                    updateAppliedFilters();
+                }
+            }
+        });
+
+        final ToolItem showFiltersColumn = new ToolItem(toolBar, SWT.CHECK);
+        showFiltersColumn.setImage(
+                ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_DISPLAY_FILTERS,
+                        toolBar.getDisplay()));
+        showFiltersColumn.setSelection(mPrefStore.getBoolean(DISPLAY_FILTERS_COLUMN_PREFKEY));
+        showFiltersColumn.setToolTipText("Display Saved Filters View");
+        showFiltersColumn.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent event) {
+                boolean showFilters = showFiltersColumn.getSelection();
+                mPrefStore.setValue(DISPLAY_FILTERS_COLUMN_PREFKEY, showFilters);
+                updateFiltersColumn(showFilters);
+            }
+        });
+
+        mScrollLockCheckBox = new ToolItem(toolBar, SWT.CHECK);
+        mScrollLockCheckBox.setImage(
+                ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_SCROLL_LOCK,
+                        toolBar.getDisplay()));
+        mScrollLockCheckBox.setSelection(true);
+        mScrollLockCheckBox.setToolTipText("Scroll Lock");
+        mScrollLockCheckBox.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent event) {
+                boolean scrollLock = mScrollLockCheckBox.getSelection();
+                setScrollToLatestLog(scrollLock);
+            }
+        });
+    }
+
+    /** Sets the foreground color of filter text based on whether the regex is valid. */
+    private void updateFilterTextColor() {
+        String text = mLiveFilterText.getText();
+        Color c;
+        try {
+            Pattern.compile(text.trim());
+            c = VALID_FILTER_REGEX_COLOR;
+        } catch (PatternSyntaxException e) {
+            c = INVALID_FILTER_REGEX_COLOR;
+        }
+        mLiveFilterText.setForeground(c);
+    }
+
+    private void updateFiltersColumn(boolean showFilters) {
+        if (showFilters) {
+            mSash.setWeights(WEIGHTS_SHOW_FILTERS);
+        } else {
+            mSash.setWeights(WEIGHTS_LOGCAT_ONLY);
+        }
+    }
+
+    /**
+     * Save logcat messages selected in the table to a file.
+     */
+    private void saveLogToFile() {
+        /* show dialog box and get target file name */
+        final String fName = getLogFileTargetLocation();
+        if (fName == null) {
+            return;
+        }
+
+        /* obtain list of selected messages */
+        final List<LogCatMessage> selectedMessages = getSelectedLogCatMessages();
+
+        /* save messages to file in a different (non UI) thread */
+        Thread t = new Thread(new Runnable() {
+            @Override
+            public void run() {
+                BufferedWriter w = null;
+                try {
+                    w = new BufferedWriter(new FileWriter(fName));
+                    for (LogCatMessage m : selectedMessages) {
+                        w.append(m.toString());
+                        w.newLine();
+                    }
+                } catch (final IOException e) {
+                    Display.getDefault().asyncExec(new Runnable() {
+                        @Override
+                        public void run() {
+                            MessageDialog.openError(Display.getCurrent().getActiveShell(),
+                                    "Unable to export selection to file.",
+                                    "Unexpected error while saving selected messages to file: "
+                                            + e.getMessage());
+                        }
+                    });
+                } finally {
+                    if (w != null) {
+                        try {
+                            w.close();
+                        } catch (IOException e) {
+                            // ignore
+                        }
+                    }
+                }
+            }
+        });
+        t.setName("Saving selected items to logfile..");
+        t.start();
+    }
+
+    /**
+     * Display a {@link FileDialog} to the user and obtain the location for the log file.
+     * @return path to target file, null if user canceled the dialog
+     */
+    private String getLogFileTargetLocation() {
+        FileDialog fd = new FileDialog(Display.getCurrent().getActiveShell(), SWT.SAVE);
+
+        fd.setText("Save Log..");
+        fd.setFileName("log.txt");
+
+        if (mLogFileExportFolder == null) {
+            mLogFileExportFolder = System.getProperty("user.home");
+        }
+        fd.setFilterPath(mLogFileExportFolder);
+
+        fd.setFilterNames(new String[] {
+                "Text Files (*.txt)"
+        });
+        fd.setFilterExtensions(new String[] {
+                "*.txt"
+        });
+
+        String fName = fd.open();
+        if (fName != null) {
+            mLogFileExportFolder = fd.getFilterPath();  /* save path to restore on future calls */
+        }
+
+        return fName;
+    }
+
+    private List<LogCatMessage> getSelectedLogCatMessages() {
+        int[] indices = mTable.getSelectionIndices();
+        Arrays.sort(indices); /* Table.getSelectionIndices() does not specify an order */
+
+        List<LogCatMessage> selectedMessages = new ArrayList<LogCatMessage>(indices.length);
+        for (int i : indices) {
+            Object data = mTable.getItem(i).getData();
+            if (data instanceof LogCatMessage) {
+                selectedMessages.add((LogCatMessage) data);
+            }
+        }
+
+        return selectedMessages;
+    }
+
+    private List<LogCatMessage> applyCurrentFilters(List<LogCatMessage> msgList) {
+        List<LogCatMessage> filteredItems = new ArrayList<LogCatMessage>(msgList.size());
+
+        for (LogCatMessage msg: msgList) {
+            if (isMessageAccepted(msg, mCurrentFilters)) {
+                filteredItems.add(msg);
+            }
+        }
+
+        return filteredItems;
+    }
+
+    private boolean isMessageAccepted(LogCatMessage msg, List<LogCatFilter> filters) {
+        for (LogCatFilter f : filters) {
+            if (!f.matches(msg)) {
+                // not accepted by this filter
+                return false;
+            }
+        }
+
+        // accepted by all filters
+        return true;
+    }
+
+    private void createLogcatViewTable(Composite parent) {
+        mTable = new Table(parent, SWT.FULL_SELECTION | SWT.MULTI);
+
+        mTable.setLayoutData(new GridData(GridData.FILL_BOTH));
+        mTable.getHorizontalBar().setVisible(true);
+
+        /** Columns to show in the table. */
+        String[] properties = {
+                "Level",
+                "Time",
+                "PID",
+                "TID",
+                "Application",
+                "Tag",
+                "Text",
+        };
+
+        /** The sampleText for each column is used to determine the default widths
+         * for each column. The contents do not matter, only their lengths are needed. */
+        String[] sampleText = {
+                "    ",
+                "    00-00 00:00:00.0000 ",
+                "    0000",
+                "    0000",
+                "    com.android.launcher",
+                "    SampleTagText",
+                "    Log Message field should be pretty long by default. As long as possible for correct display on Mac.",
+        };
+
+        for (int i = 0; i < properties.length; i++) {
+            TableHelper.createTableColumn(mTable,
+                    properties[i],                      /* Column title */
+                    SWT.LEFT,                           /* Column Style */
+                    sampleText[i],                      /* String to compute default col width */
+                    getColPreferenceKey(properties[i]), /* Preference Store key for this column */
+                    mPrefStore);
+        }
+
+        // don't zebra stripe the table: When the buffer is full, and scroll lock is on, having
+        // zebra striping means that the background could keep changing depending on the number
+        // of new messages added to the bottom of the log.
+        mTable.setLinesVisible(false);
+        mTable.setHeaderVisible(true);
+
+        // Set the row height to be sufficient enough to display the current font.
+        // This is not strictly necessary, except that on WinXP, the rows showed up clipped. So
+        // we explicitly set it to be sure.
+        mTable.addListener(SWT.MeasureItem, new Listener() {
+            @Override
+            public void handleEvent(Event event) {
+                event.height = event.gc.getFontMetrics().getHeight();
+            }
+        });
+
+        // Update the label provider whenever the text column's width changes
+        TableColumn textColumn = mTable.getColumn(properties.length - 1);
+        textColumn.addControlListener(new ControlAdapter() {
+            @Override
+            public void controlResized(ControlEvent event) {
+                recomputeWrapWidth();
+            }
+        });
+
+        addRightClickMenu(mTable);
+        initDoubleClickListener();
+        recomputeWrapWidth();
+
+        mTable.addDisposeListener(new DisposeListener() {
+            @Override
+            public void widgetDisposed(DisposeEvent arg0) {
+                dispose();
+            }
+        });
+
+        final ScrollBar vbar = mTable.getVerticalBar();
+        mScrollBarSelectionListener = new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                if (!mAutoScrollLock) {
+                    return;
+                }
+
+                // thumb + selection < max => bar is not at the bottom.
+                // We subtract an arbitrary amount (thumbSize/2) from this difference to allow
+                // for cases like half a line being displayed at the end from affecting this
+                // calculation. The thumbSize/2 number seems to work experimentally across
+                // Linux/Mac & Windows, but might possibly need tweaking.
+                int diff = vbar.getThumb() + vbar.getSelection() - vbar.getMaximum();
+                boolean isAtBottom = Math.abs(diff) < vbar.getThumb() / 2;
+
+                if (isAtBottom != mShouldScrollToLatestLog) {
+                    setScrollToLatestLog(isAtBottom);
+                    mScrollLockCheckBox.setSelection(isAtBottom);
+                }
+            }
+        };
+        startScrollBarMonitor(vbar);
+
+        // Explicitly set the values to use for the scroll bar. In particular, we want these values
+        // to have a high enough accuracy that even small movements of the scroll bar have an
+        // effect on the selection. The auto scroll lock detection assumes that the scroll bar is
+        // at the bottom iff selection + thumb == max.
+        final int MAX = 10000;
+        final int THUMB = 10;
+        vbar.setValues(MAX - THUMB, // selection
+                0,                  // min
+                MAX,                // max
+                THUMB,              // thumb
+                1,                  // increment
+                THUMB);             // page increment
+    }
+
+    private void startScrollBarMonitor(ScrollBar vbar) {
+        synchronized (mScrollBarSelectionListenerLock) {
+            if (!mScrollBarListenerSet) {
+                mScrollBarListenerSet = true;
+                vbar.addSelectionListener(mScrollBarSelectionListener);
+            }
+        }
+    }
+
+    private void stopScrollBarMonitor(ScrollBar vbar) {
+        synchronized (mScrollBarSelectionListenerLock) {
+            if (mScrollBarListenerSet) {
+                mScrollBarListenerSet = false;
+                vbar.removeSelectionListener(mScrollBarSelectionListener);
+            }
+        }
+    }
+
+    /** Setup menu to be displayed when right clicking a log message. */
+    private void addRightClickMenu(final Table table) {
+        // This action will pop up a create filter dialog pre-populated with current selection
+        final Action filterAction = new Action("Filter similar messages...") {
+            @Override
+            public void run() {
+                List<LogCatMessage> selectedMessages = getSelectedLogCatMessages();
+                if (selectedMessages.size() == 0) {
+                    addNewFilter();
+                } else {
+                    LogCatMessage m = selectedMessages.get(0);
+                    addNewFilter(m.getTag(), m.getMessage(), m.getPid(), m.getAppName(),
+                            m.getLogLevel());
+                }
+            }
+        };
+
+        final Action findAction = new Action("Find...") {
+            @Override
+            public void run() {
+                showFindDialog();
+            };
+        };
+
+        final MenuManager mgr = new MenuManager();
+        mgr.add(filterAction);
+        mgr.add(findAction);
+        final Menu menu = mgr.createContextMenu(table);
+
+        table.addListener(SWT.MenuDetect, new Listener() {
+            @Override
+            public void handleEvent(Event event) {
+                Point pt = table.getDisplay().map(null, table, new Point(event.x, event.y));
+                Rectangle clientArea = table.getClientArea();
+
+                // The click location is in the header if it is between
+                // clientArea.y and clientArea.y + header height
+                boolean header = pt.y > clientArea.y
+                                    && pt.y < (clientArea.y + table.getHeaderHeight());
+
+                // Show the menu only if it is not inside the header
+                table.setMenu(header ? null : menu);
+            }
+        });
+    }
+
+    public void recomputeWrapWidth() {
+        if (mTable == null || mTable.isDisposed()) {
+            return;
+        }
+
+        // get width of the last column (log message)
+        TableColumn tc = mTable.getColumn(mTable.getColumnCount() - 1);
+        int colWidth = tc.getWidth();
+
+        // get font width
+        GC gc = new GC(tc.getParent());
+        gc.setFont(mFont);
+        int avgCharWidth = gc.getFontMetrics().getAverageCharWidth();
+        gc.dispose();
+
+        int MIN_CHARS_PER_LINE = 50;    // show atleast these many chars per line
+        mWrapWidthInChars = Math.max(colWidth/avgCharWidth, MIN_CHARS_PER_LINE);
+
+        int OFFSET_AT_END_OF_LINE = 10; // leave some space at the end of the line
+        mWrapWidthInChars -= OFFSET_AT_END_OF_LINE;
+    }
+
+    private void setScrollToLatestLog(boolean scroll) {
+        mShouldScrollToLatestLog = scroll;
+        if (scroll) {
+            scrollToLatestLog();
+        }
+    }
+
+    private String getColPreferenceKey(String field) {
+        return LOGCAT_VIEW_COLSIZE_PREFKEY_PREFIX + field;
+    }
+
+    private Font getFontFromPrefStore() {
+        FontData fd = PreferenceConverter.getFontData(mPrefStore,
+                LogCatPanel.LOGCAT_VIEW_FONT_PREFKEY);
+        return new Font(Display.getDefault(), fd);
+    }
+
+    private Color getColorFromPrefStore(String key) {
+        RGB rgb = PreferenceConverter.getColor(mPrefStore, key);
+        return new Color(Display.getDefault(), rgb);
+    }
+
+    private void setupDefaults() {
+        int defaultFilterIndex = 0;
+        mFiltersTableViewer.getTable().setSelection(defaultFilterIndex);
+
+        filterSelectionChanged();
+    }
+
+    /**
+     * Perform all necessary updates whenever a filter is selected (by user or programmatically).
+     */
+    private void filterSelectionChanged() {
+        int idx = mFiltersTableViewer.getTable().getSelectionIndex();
+        if (idx == -1) {
+            /* One of the filters should always be selected.
+             * On Linux, there is no way to deselect an item.
+             * On Mac, clicking inside the list view, but not an any item will result
+             * in all items being deselected. In such a case, we simply reselect the
+             * first entry. */
+            idx = 0;
+            mFiltersTableViewer.getTable().setSelection(idx);
+        }
+
+        mCurrentSelectedFilterIndex = idx;
+
+        resetUnreadCountForAllFilters();
+        updateFiltersToolBar();
+        updateAppliedFilters();
+    }
+
+    private void resetUnreadCountForAllFilters() {
+        for (LogCatFilterData fd: mLogCatFilterData.values()) {
+            fd.resetUnreadCount();
+        }
+        refreshFiltersTable();
+    }
+
+    private void updateFiltersToolBar() {
+        /* The default filter at index 0 can neither be edited, nor removed. */
+        boolean en = mCurrentSelectedFilterIndex != DEFAULT_FILTER_INDEX;
+        mEditFilterToolItem.setEnabled(en);
+        mDeleteFilterToolItem.setEnabled(en);
+    }
+
+    private void updateAppliedFilters() {
+        mCurrentFilters = getFiltersToApply();
+        reloadLogBuffer();
+    }
+
+    private List<LogCatFilter> getFiltersToApply() {
+        /* list of filters to apply = saved filter + live filters */
+        List<LogCatFilter> filters = new ArrayList<LogCatFilter>();
+
+        if (mCurrentSelectedFilterIndex != DEFAULT_FILTER_INDEX) {
+            filters.add(getSelectedSavedFilter());
+        }
+
+        filters.addAll(getCurrentLiveFilters());
+        return filters;
+    }
+
+    private List<LogCatFilter> getCurrentLiveFilters() {
+        return LogCatFilter.fromString(
+                mLiveFilterText.getText(),                                  /* current query */
+                LogLevel.getByString(mLiveFilterLevelCombo.getText()));     /* current log level */
+    }
+
+    private LogCatFilter getSelectedSavedFilter() {
+        return mLogCatFilters.get(mCurrentSelectedFilterIndex);
+    }
+
+    @Override
+    public void setFocus() {
+    }
+
+    @Override
+    public void bufferChanged(List<LogCatMessage> addedMessages,
+            List<LogCatMessage> deletedMessages) {
+        updateUnreadCount(addedMessages);
+        refreshFiltersTable();
+
+        synchronized (mLogBuffer) {
+            addedMessages = applyCurrentFilters(addedMessages);
+            deletedMessages = applyCurrentFilters(deletedMessages);
+
+            mLogBuffer.addAll(addedMessages);
+            mDeletedLogCount += deletedMessages.size();
+        }
+
+        refreshLogCatTable();
+    }
+
+    private void reloadLogBuffer() {
+        mTable.removeAll();
+
+        synchronized (mLogBuffer) {
+            mLogBuffer.clear();
+            mDeletedLogCount = 0;
+        }
+
+        if (mReceiver == null || mReceiver.getMessages() == null) {
+            return;
+        }
+
+        List<LogCatMessage> addedMessages = mReceiver.getMessages().getAllMessages();
+        List<LogCatMessage> deletedMessages = Collections.emptyList();
+        bufferChanged(addedMessages, deletedMessages);
+    }
+
+    /**
+     * When new messages are received, and they match a saved filter, update
+     * the unread count associated with that filter.
+     * @param receivedMessages list of new messages received
+     */
+    private void updateUnreadCount(List<LogCatMessage> receivedMessages) {
+        for (int i = 0; i < mLogCatFilters.size(); i++) {
+            if (i == mCurrentSelectedFilterIndex) {
+                /* no need to update unread count for currently selected filter */
+                continue;
+            }
+            LogCatFilter f = mLogCatFilters.get(i);
+            LogCatFilterData fd = mLogCatFilterData.get(f);
+            fd.updateUnreadCount(receivedMessages);
+        }
+    }
+
+    private void refreshFiltersTable() {
+        Display.getDefault().asyncExec(new Runnable() {
+            @Override
+            public void run() {
+                if (mFiltersTableViewer.getTable().isDisposed()) {
+                    return;
+                }
+                mFiltersTableViewer.refresh();
+            }
+        });
+    }
+
+    /** Task currently submitted to {@link Display#asyncExec} to be run in UI thread. */
+    private LogCatTableRefresherTask mCurrentRefresher;
+
+    /**
+     * Refresh the logcat table asynchronously from the UI thread.
+     * This method adds a new async refresh only if there are no pending refreshes for the table.
+     * Doing so eliminates redundant refresh threads from being queued up to be run on the
+     * display thread.
+     */
+    private void refreshLogCatTable() {
+        synchronized (this) {
+            if (mCurrentRefresher == null) {
+                mCurrentRefresher = new LogCatTableRefresherTask();
+                Display.getDefault().asyncExec(mCurrentRefresher);
+            }
+        }
+    }
+
+    /**
+     * The {@link LogCatTableRefresherTask} takes care of refreshing the table with the
+     * new log messages that have been received. Since the log behaves like a circular buffer,
+     * the first step is to remove items from the top of the table (if necessary). This step
+     * is complicated by the fact that a single log message may span multiple rows if the message
+     * was wrapped. Once the deleted items are removed, the new messages are added to the bottom
+     * of the table. If scroll lock is enabled, the item that was original visible is made visible
+     * again, if not, the last item is made visible.
+     */
+    private class LogCatTableRefresherTask implements Runnable {
+        @Override
+        public void run() {
+            if (mTable.isDisposed()) {
+                return;
+            }
+            synchronized (LogCatPanel.this) {
+                mCurrentRefresher = null;
+            }
+
+            // Current topIndex so that it can be restored if scroll locked.
+            int topIndex = mTable.getTopIndex();
+
+            mTable.setRedraw(false);
+
+            // the scroll bar should only listen to user generated scroll events, not the
+            // scroll events that happen due to the addition of logs
+            stopScrollBarMonitor(mTable.getVerticalBar());
+
+            // Obtain the list of new messages, and the number of deleted messages.
+            List<LogCatMessage> newMessages;
+            int deletedMessageCount;
+            synchronized (mLogBuffer) {
+                newMessages = new ArrayList<LogCatMessage>(mLogBuffer);
+                mLogBuffer.clear();
+
+                deletedMessageCount = mDeletedLogCount;
+                mDeletedLogCount = 0;
+
+                mFindTarget.scrollBy(deletedMessageCount);
+            }
+
+            int originalItemCount = mTable.getItemCount();
+
+            // Remove entries from the start of the table if they were removed in the log buffer
+            // This is complicated by the fact that a single message may span multiple TableItems
+            // if it was word-wrapped.
+            deletedMessageCount -= removeFromTable(mTable, deletedMessageCount);
+
+            // Compute number of table items that were deleted from the table.
+            int deletedItemCount = originalItemCount - mTable.getItemCount();
+
+            // If there are more messages to delete (after deleting messages from the table),
+            // then delete them from the start of the newly added messages list
+            if (deletedMessageCount > 0) {
+                assert deletedMessageCount < newMessages.size();
+                for (int i = 0; i < deletedMessageCount; i++) {
+                    newMessages.remove(0);
+                }
+            }
+
+            // Add the remaining messages to the table.
+            for (LogCatMessage m: newMessages) {
+                List<String> wrappedMessageList = wrapMessage(m.getMessage(), mWrapWidthInChars);
+                Color c = getForegroundColor(m);
+                for (int i = 0; i < wrappedMessageList.size(); i++) {
+                    TableItem item = new TableItem(mTable, SWT.NONE);
+
+                    if (i == 0) {
+                        // Only set the message data in the first item. This allows code that
+                        // examines the table item data (such as copy selection) to distinguish
+                        // between real messages versus lines that are really just wrapped
+                        // content from the previous message.
+                        item.setData(m);
+
+                        item.setText(new String[] {
+                                Character.toString(m.getLogLevel().getPriorityLetter()),
+                                m.getTime(),
+                                m.getPid(),
+                                m.getTid(),
+                                m.getAppName(),
+                                m.getTag(),
+                                wrappedMessageList.get(i)
+                        });
+                    } else {
+                        item.setText(new String[] {
+                                "", "", "", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+                                "", "", "", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+                                wrappedMessageList.get(i)
+                        });
+                    }
+                    item.setForeground(c);
+                    item.setFont(mFont);
+                }
+            }
+
+            if (mShouldScrollToLatestLog) {
+                scrollToLatestLog();
+            } else {
+                // If scroll locked, show the same item that was original visible in the table.
+                int index = Math.max(topIndex - deletedItemCount, 0);
+                mTable.setTopIndex(index);
+            }
+
+            mTable.setRedraw(true);
+
+            // re-enable listening to scroll bar events, but do so in a separate thread to make
+            // sure that the current task (LogCatRefresherTask) has completed first
+            Display.getDefault().asyncExec(new Runnable() {
+                @Override
+                public void run() {
+                    if (!mTable.isDisposed()) {
+                        startScrollBarMonitor(mTable.getVerticalBar());
+                    }
+                }
+            });
+        }
+
+        /**
+         * Removes given number of messages from the table, starting at the top of the table.
+         * Note that the number of messages deleted is not equal to the number of rows
+         * deleted since a single message could span multiple rows. This method first calculates
+         * the number of rows that correspond to the number of messages to delete, and then
+         * removes all those rows.
+         * @param table table from which messages should be removed
+         * @param msgCount number of messages to be removed
+         * @return number of messages that were actually removed
+         */
+        private int removeFromTable(Table table, int msgCount) {
+            int deletedMessageCount = 0; // # of messages that have been deleted
+            int lastItemToDelete = 0;    // index of the last item that should be deleted
+
+            while (deletedMessageCount < msgCount && lastItemToDelete < table.getItemCount()) {
+                // only rows that begin a message have their item data set
+                TableItem item = table.getItem(lastItemToDelete);
+                if (item.getData() != null) {
+                    deletedMessageCount++;
+                }
+
+                lastItemToDelete++;
+            }
+
+            // If there are any table items left over at the end that are wrapped over from the
+            // previous message, mark them for deletion as well.
+            if (lastItemToDelete < table.getItemCount()
+                    && table.getItem(lastItemToDelete).getData() == null) {
+                lastItemToDelete++;
+            }
+
+            table.remove(0, lastItemToDelete - 1);
+
+            return deletedMessageCount;
+        }
+    }
+
+    /** Scroll to the last line. */
+    private void scrollToLatestLog() {
+        if (!mTable.isDisposed()) {
+            mTable.setTopIndex(mTable.getItemCount() - 1);
+        }
+    }
+
+    /**
+     * Splits the message into multiple lines if the message length exceeds given width.
+     * If the message was split, then a wrap character \u23ce is appended to the end of all
+     * lines but the last one.
+     */
+    private List<String> wrapMessage(String msg, int wrapWidth) {
+        if (msg.length() < wrapWidth) {
+            return Collections.singletonList(msg);
+        }
+
+        List<String> wrappedMessages = new ArrayList<String>();
+
+        int offset = 0;
+        int len = msg.length();
+
+        while (len > 0) {
+            int copylen = Math.min(wrapWidth, len);
+            String s = msg.substring(offset, offset + copylen);
+
+            offset += copylen;
+            len -= copylen;
+
+            if (len > 0) { // if there are more lines following, then append a wrap marker
+                s += " \u23ce"; //$NON-NLS-1$
+            }
+
+            wrappedMessages.add(s);
+        }
+
+        return wrappedMessages;
+    }
+
+    private Color getForegroundColor(LogCatMessage m) {
+        LogLevel l = m.getLogLevel();
+
+        if (l.equals(LogLevel.VERBOSE)) {
+            return mVerboseColor;
+        } else if (l.equals(LogLevel.INFO)) {
+            return mInfoColor;
+        } else if (l.equals(LogLevel.DEBUG)) {
+            return mDebugColor;
+        } else if (l.equals(LogLevel.ERROR)) {
+            return mErrorColor;
+        } else if (l.equals(LogLevel.WARN)) {
+            return mWarnColor;
+        } else if (l.equals(LogLevel.ASSERT)) {
+            return mAssertColor;
+        }
+
+        return mVerboseColor;
+    }
+
+    private List<ILogCatMessageSelectionListener> mMessageSelectionListeners;
+
+    private void initDoubleClickListener() {
+        mMessageSelectionListeners = new ArrayList<ILogCatMessageSelectionListener>(1);
+
+        mTable.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetDefaultSelected(SelectionEvent arg0) {
+                List<LogCatMessage> selectedMessages = getSelectedLogCatMessages();
+                if (selectedMessages.size() == 0) {
+                    return;
+                }
+
+                for (ILogCatMessageSelectionListener l : mMessageSelectionListeners) {
+                    l.messageDoubleClicked(selectedMessages.get(0));
+                }
+            }
+        });
+    }
+
+    public void addLogCatMessageSelectionListener(ILogCatMessageSelectionListener l) {
+        mMessageSelectionListeners.add(l);
+    }
+
+    private ITableFocusListener mTableFocusListener;
+
+    /**
+     * Specify the listener to be called when the logcat view gets focus. This interface is
+     * required by DDMS to hook up the menu items for Copy and Select All.
+     * @param listener listener to be notified when logcat view is in focus
+     */
+    public void setTableFocusListener(ITableFocusListener listener) {
+        mTableFocusListener = listener;
+
+        final IFocusedTableActivator activator = new IFocusedTableActivator() {
+            @Override
+            public void copy(Clipboard clipboard) {
+                copySelectionToClipboard(clipboard);
+            }
+
+            @Override
+            public void selectAll() {
+                mTable.selectAll();
+            }
+        };
+
+        mTable.addFocusListener(new FocusListener() {
+            @Override
+            public void focusGained(FocusEvent e) {
+                mTableFocusListener.focusGained(activator);
+            }
+
+            @Override
+            public void focusLost(FocusEvent e) {
+                mTableFocusListener.focusLost(activator);
+            }
+        });
+    }
+
+    /** Copy all selected messages to clipboard. */
+    public void copySelectionToClipboard(Clipboard clipboard) {
+        StringBuilder sb = new StringBuilder();
+
+        for (LogCatMessage m : getSelectedLogCatMessages()) {
+            sb.append(m.toString());
+            sb.append('\n');
+        }
+
+        if (sb.length() > 0) {
+            clipboard.setContents(
+                    new Object[] {sb.toString()},
+                    new Transfer[] {TextTransfer.getInstance()}
+                    );
+        }
+    }
+
+    /** Select all items in the logcat table. */
+    public void selectAll() {
+        mTable.selectAll();
+    }
+
+    private void dispose() {
+        if (mFont != null && !mFont.isDisposed()) {
+            mFont.dispose();
+        }
+
+        if (mVerboseColor != null && !mVerboseColor.isDisposed()) {
+            disposeMessageColors();
+        }
+    }
+
+    private void disposeMessageColors() {
+        mVerboseColor.dispose();
+        mDebugColor.dispose();
+        mInfoColor.dispose();
+        mWarnColor.dispose();
+        mErrorColor.dispose();
+        mAssertColor.dispose();
+    }
+
+    private class LogcatFindTarget extends AbstractBufferFindTarget {
+        @Override
+        public void selectAndReveal(int index) {
+            mTable.deselectAll();
+            mTable.select(index);
+            mTable.showSelection();
+        }
+
+        @Override
+        public int getItemCount() {
+            return mTable.getItemCount();
+        }
+
+        @Override
+        public String getItem(int index) {
+            Object data = mTable.getItem(index).getData();
+            if (data != null) {
+                return data.toString();
+            }
+
+            return null;
+        }
+
+        @Override
+        public int getStartingIndex() {
+            // start searches from current selection if present, otherwise from the tail end
+            // of the buffer
+            int s = mTable.getSelectionIndex();
+            if (s != -1) {
+                return s;
+            } else {
+                return getItemCount() - 1;
+            }
+        };
+    };
+
+    private FindDialog mFindDialog;
+    private LogcatFindTarget mFindTarget = new LogcatFindTarget();
+    public void showFindDialog() {
+        if (mFindDialog != null) {
+            // if the dialog is already displayed
+            return;
+        }
+
+        mFindDialog = new FindDialog(Display.getDefault().getActiveShell(), mFindTarget);
+        mFindDialog.open(); // blocks until find dialog is closed
+        mFindDialog = null;
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiver.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiver.java
new file mode 100644
index 0000000..a85cd03
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiver.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmlib.logcat.LogCatListener;
+import com.android.ddmlib.logcat.LogCatMessage;
+import com.android.ddmlib.logcat.LogCatReceiverTask;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A class to monitor a device for logcat messages. It stores the received
+ * log messages in a circular buffer.
+ */
+public final class LogCatReceiver implements LogCatListener {
+    private static LogCatMessage DEVICE_DISCONNECTED_MESSAGE =
+            new LogCatMessage(LogLevel.ERROR, "", "", "",
+                    "", "", "Device disconnected");
+
+    private LogCatMessageList mLogMessages;
+    private IDevice mCurrentDevice;
+    private LogCatReceiverTask mLogCatReceiverTask;
+    private Set<ILogCatBufferChangeListener> mLogCatMessageListeners;
+    private IPreferenceStore mPrefStore;
+
+    /**
+     * Construct a LogCat message receiver for provided device. This will launch a
+     * logcat command on the device, and monitor the output of that command in
+     * a separate thread. All logcat messages are then stored in a circular
+     * buffer, which can be retrieved using {@link LogCatReceiver#getMessages()}.
+     * @param device device to monitor for logcat messages
+     * @param prefStore
+     */
+    public LogCatReceiver(IDevice device, IPreferenceStore prefStore) {
+        mCurrentDevice = device;
+        mPrefStore = prefStore;
+
+        mLogCatMessageListeners = new HashSet<ILogCatBufferChangeListener>();
+        mLogMessages = new LogCatMessageList(getFifoSize());
+
+        startReceiverThread();
+    }
+
+    /**
+     * Stop receiving messages from currently active device.
+     */
+    public void stop() {
+        if (mLogCatReceiverTask != null) {
+            /* stop the current logcat command */
+            mLogCatReceiverTask.removeLogCatListener(this);
+            mLogCatReceiverTask.stop();
+            mLogCatReceiverTask = null;
+
+            // add a message to the log indicating that the device has been disconnected.
+            log(Collections.singletonList(DEVICE_DISCONNECTED_MESSAGE));
+        }
+
+        mCurrentDevice = null;
+    }
+
+    private int getFifoSize() {
+        int n = mPrefStore.getInt(LogCatMessageList.MAX_MESSAGES_PREFKEY);
+        return n == 0 ? LogCatMessageList.MAX_MESSAGES_DEFAULT : n;
+    }
+
+    private void startReceiverThread() {
+        if (mCurrentDevice == null) {
+            return;
+        }
+
+        mLogCatReceiverTask = new LogCatReceiverTask(mCurrentDevice);
+        mLogCatReceiverTask.addLogCatListener(this);
+
+        Thread t = new Thread(mLogCatReceiverTask);
+        t.setName("LogCat output receiver for " + mCurrentDevice.getSerialNumber());
+        t.start();
+    }
+
+    @Override
+    public void log(List<LogCatMessage> newMessages) {
+        List<LogCatMessage> deletedMessages;
+        synchronized (mLogMessages) {
+            deletedMessages = mLogMessages.ensureSpace(newMessages.size());
+            mLogMessages.appendMessages(newMessages);
+        }
+        sendLogChangedEvent(newMessages, deletedMessages);
+    }
+
+    /**
+     * Get the list of logcat messages received from currently active device.
+     * @return list of messages if currently listening, null otherwise
+     */
+    public LogCatMessageList getMessages() {
+        return mLogMessages;
+    }
+
+    /**
+     * Clear the list of messages received from the currently active device.
+     */
+    public void clearMessages() {
+        mLogMessages.clear();
+    }
+
+    /**
+     * Add to list of message event listeners.
+     * @param l listener to notified when messages are received from the device
+     */
+    public void addMessageReceivedEventListener(ILogCatBufferChangeListener l) {
+        mLogCatMessageListeners.add(l);
+    }
+
+    public void removeMessageReceivedEventListener(ILogCatBufferChangeListener l) {
+        mLogCatMessageListeners.remove(l);
+    }
+
+    private void sendLogChangedEvent(List<LogCatMessage> addedMessages,
+            List<LogCatMessage> deletedMessages) {
+        for (ILogCatBufferChangeListener l : mLogCatMessageListeners) {
+            l.bufferChanged(addedMessages, deletedMessages);
+        }
+    }
+
+    /**
+     * Resize the internal FIFO.
+     * @param size new size
+     */
+    public void resizeFifo(int size) {
+        mLogMessages.resize(size);
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiverFactory.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiverFactory.java
new file mode 100644
index 0000000..5b25e17
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiverFactory.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ddmuilib.logcat;
+
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
+import com.android.ddmlib.IDevice;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A factory for {@link LogCatReceiver} objects. Its primary objective is to cache
+ * constructed {@link LogCatReceiver}'s per device and hand them back when requested.
+ */
+public class LogCatReceiverFactory {
+    /** Singleton instance. */
+    public static final LogCatReceiverFactory INSTANCE = new LogCatReceiverFactory();
+
+    private Map<String, LogCatReceiver> mReceiverCache = new HashMap<String, LogCatReceiver>();
+
+    /** Private constructor: cannot instantiate. */
+    private LogCatReceiverFactory() {
+        AndroidDebugBridge.addDeviceChangeListener(new IDeviceChangeListener() {
+            @Override
+            public void deviceDisconnected(final IDevice device) {
+                // The deviceDisconnected() is called from DDMS code that holds
+                // multiple locks regarding list of clients, etc.
+                // It so happens that #newReceiver() below adds a clientChangeListener
+                // which requires those locks as well. So if we call
+                // #removeReceiverFor from a DDMS/Monitor thread, we could end up
+                // in a deadlock. As a result, we spawn a separate thread that
+                // doesn't hold any of the DDMS locks to remove the receiver.
+                Thread t = new Thread(new Runnable() {
+                        @Override
+                        public void run() {
+                            removeReceiverFor(device);                        }
+                    }, "Remove logcat receiver for " + device.getSerialNumber());
+                t.start();
+            }
+
+            @Override
+            public void deviceConnected(IDevice device) {
+            }
+
+            @Override
+            public void deviceChanged(IDevice device, int changeMask) {
+            }
+        });
+    }
+
+    /**
+     * Remove existing logcat receivers. This method should not be called from a DDMS thread
+     * context that might be holding locks. Doing so could result in a deadlock with the following
+     * two threads locked up: <ul>
+     * <li> {@link #removeReceiverFor(IDevice)} waiting to lock {@link LogCatReceiverFactory},
+     * while holding a DDMS monitor internal lock. </li>
+     * <li> {@link #newReceiver(IDevice, IPreferenceStore)} holding {@link LogCatReceiverFactory}
+     * while attempting to obtain a DDMS monitor lock. </li>
+     * </ul>
+     */
+    private synchronized void removeReceiverFor(IDevice device) {
+        LogCatReceiver r = mReceiverCache.get(device.getSerialNumber());
+        if (r != null) {
+            r.stop();
+            mReceiverCache.remove(device.getSerialNumber());
+        }
+    }
+
+    public synchronized LogCatReceiver newReceiver(IDevice device, IPreferenceStore prefs) {
+        LogCatReceiver r = mReceiverCache.get(device.getSerialNumber());
+        if (r != null) {
+            return r;
+        }
+
+        r = new LogCatReceiver(device, prefs);
+        mReceiverCache.put(device.getSerialNumber(), r);
+        return r;
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatStackTraceParser.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatStackTraceParser.java
new file mode 100644
index 0000000..3da9fd0
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatStackTraceParser.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ddmuilib.logcat;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Helper class that can determine if a string matches the exception
+ * stack trace pattern, and if so, can provide the java source file
+ * and line where the exception occured.
+ */
+public final class LogCatStackTraceParser {
+    /** Regex to match a stack trace line. E.g.:
+     *          at com.foo.Class.method(FileName.extension:10)
+     *  extension is typically java, but can be anything (java/groovy/scala/..).
+     */
+    private static final String EXCEPTION_LINE_REGEX =
+            "\\s*at\\ (.*)\\((.*)\\..*\\:(\\d+)\\)"; //$NON-NLS-1$
+
+    private static final Pattern EXCEPTION_LINE_PATTERN =
+            Pattern.compile(EXCEPTION_LINE_REGEX);
+
+    /**
+     * Identify if a input line matches the expected pattern
+     * for a stack trace from an exception.
+     */
+    public boolean isValidExceptionTrace(String line) {
+        return EXCEPTION_LINE_PATTERN.matcher(line).find();
+    }
+
+    /**
+     * Get fully qualified method name that threw the exception.
+     * @param line line from the stack trace, must have been validated with
+     * {@link LogCatStackTraceParser#isValidExceptionTrace(String)} before calling this method.
+     * @return fully qualified method name
+     */
+    public String getMethodName(String line) {
+        Matcher m = EXCEPTION_LINE_PATTERN.matcher(line);
+        m.find();
+        return m.group(1);
+    }
+
+    /**
+     * Get source file name where exception was generated. Input line must be first validated with
+     * {@link LogCatStackTraceParser#isValidExceptionTrace(String)}.
+     */
+    public String getFileName(String line) {
+        Matcher m = EXCEPTION_LINE_PATTERN.matcher(line);
+        m.find();
+        return m.group(2);
+    }
+
+    /**
+     * Get line number where exception was generated. Input line must be first validated with
+     * {@link LogCatStackTraceParser#isValidExceptionTrace(String)}.
+     */
+    public int getLineNumber(String line) {
+        Matcher m = EXCEPTION_LINE_PATTERN.matcher(line);
+        m.find();
+        try {
+            return Integer.parseInt(m.group(3));
+        } catch (NumberFormatException e) {
+            return 0;
+        }
+    }
+
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogColors.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogColors.java
new file mode 100644
index 0000000..9cff656
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogColors.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib.logcat;
+
+import org.eclipse.swt.graphics.Color;
+
+public class LogColors {
+    public Color infoColor;
+    public Color debugColor;
+    public Color errorColor;
+    public Color warningColor;
+    public Color verboseColor;
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogFilter.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogFilter.java
new file mode 100644
index 0000000..74a5e37
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogFilter.java
@@ -0,0 +1,556 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib.logcat;
+
+import com.android.ddmlib.Log;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmuilib.annotation.UiThread;
+import com.android.ddmuilib.logcat.LogPanel.LogMessage;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.widgets.ScrollBar;
+import org.eclipse.swt.widgets.TabItem;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableItem;
+
+import java.util.ArrayList;
+import java.util.regex.PatternSyntaxException;
+
+/** logcat output filter class */
+public class LogFilter {
+
+    public final static int MODE_PID = 0x01;
+    public final static int MODE_TAG = 0x02;
+    public final static int MODE_LEVEL = 0x04;
+
+    private String mName;
+
+    /**
+     * Filtering mode. Value can be a mix of MODE_PID, MODE_TAG, MODE_LEVEL
+     */
+    private int mMode = 0;
+
+    /**
+     * pid used for filtering. Only valid if mMode is MODE_PID.
+     */
+    private int mPid;
+
+    /** Single level log level as defined in Log.mLevelChar. Only valid
+     * if mMode is MODE_LEVEL */
+    private int mLogLevel;
+
+    /**
+     * log tag filtering. Only valid if mMode is MODE_TAG
+     */
+    private String mTag;
+
+    private Table mTable;
+    private TabItem mTabItem;
+    private boolean mIsCurrentTabItem = false;
+    private int mUnreadCount = 0;
+
+    /** Temp keyword filtering */
+    private String[] mTempKeywordFilters;
+
+    /** temp pid filtering */
+    private int mTempPid = -1;
+
+    /** temp tag filtering */
+    private String mTempTag;
+
+    /** temp log level filtering */
+    private int mTempLogLevel = -1;
+
+    private LogColors mColors;
+
+    private boolean mTempFilteringStatus = false;
+
+    private final ArrayList<LogMessage> mMessages = new ArrayList<LogMessage>();
+    private final ArrayList<LogMessage> mNewMessages = new ArrayList<LogMessage>();
+
+    private boolean mSupportsDelete = true;
+    private boolean mSupportsEdit = true;
+    private int mRemovedMessageCount = 0;
+
+    /**
+     * Creates a filter with a particular mode.
+     * @param name The name to be displayed in the UI
+     */
+    public LogFilter(String name) {
+        mName = name;
+    }
+
+    public LogFilter() {
+
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder(mName);
+
+        sb.append(':');
+        sb.append(mMode);
+        if ((mMode & MODE_PID) == MODE_PID) {
+            sb.append(':');
+            sb.append(mPid);
+        }
+
+        if ((mMode & MODE_LEVEL) == MODE_LEVEL) {
+            sb.append(':');
+            sb.append(mLogLevel);
+        }
+
+        if ((mMode & MODE_TAG) == MODE_TAG) {
+            sb.append(':');
+            sb.append(mTag);
+        }
+
+        return sb.toString();
+    }
+
+    public boolean loadFromString(String string) {
+        String[] segments = string.split(":"); //$NON-NLS-1$
+        int index = 0;
+
+        // get the name
+        mName = segments[index++];
+
+        // get the mode
+        mMode = Integer.parseInt(segments[index++]);
+
+        if ((mMode & MODE_PID) == MODE_PID) {
+            mPid = Integer.parseInt(segments[index++]);
+        }
+
+        if ((mMode & MODE_LEVEL) == MODE_LEVEL) {
+            mLogLevel = Integer.parseInt(segments[index++]);
+        }
+
+        if ((mMode & MODE_TAG) == MODE_TAG) {
+            mTag = segments[index++];
+        }
+
+        return true;
+    }
+
+
+    /** Sets the name of the filter. */
+    void setName(String name) {
+        mName = name;
+    }
+
+    /**
+     * Returns the UI display name.
+     */
+    public String getName() {
+        return mName;
+    }
+
+    /**
+     * Set the Table ui widget associated with this filter.
+     * @param tabItem The item in the TabFolder
+     * @param table The Table object
+     */
+    public void setWidgets(TabItem tabItem, Table table) {
+        mTable = table;
+        mTabItem = tabItem;
+    }
+
+    /**
+     * Returns true if the filter is ready for ui.
+     */
+    public boolean uiReady() {
+        return (mTable != null && mTabItem != null);
+    }
+
+    /**
+     * Returns the UI table object.
+     * @return
+     */
+    public Table getTable() {
+        return mTable;
+    }
+
+    public void dispose() {
+        mTable.dispose();
+        mTabItem.dispose();
+        mTable = null;
+        mTabItem = null;
+    }
+
+    /**
+     * Resets the filtering mode to be 0 (i.e. no filter).
+     */
+    public void resetFilteringMode() {
+        mMode = 0;
+    }
+
+    /**
+     * Returns the current filtering mode.
+     * @return A bitmask. Possible values are MODE_PID, MODE_TAG, MODE_LEVEL
+     */
+    public int getFilteringMode() {
+        return mMode;
+    }
+
+    /**
+     * Adds PID to the current filtering mode.
+     * @param pid
+     */
+    public void setPidMode(int pid) {
+        if (pid != -1) {
+            mMode |= MODE_PID;
+        } else {
+            mMode &= ~MODE_PID;
+        }
+        mPid = pid;
+    }
+
+    /** Returns the pid filter if valid, otherwise -1 */
+    public int getPidFilter() {
+        if ((mMode & MODE_PID) == MODE_PID)
+            return mPid;
+        return -1;
+    }
+
+    public void setTagMode(String tag) {
+        if (tag != null && tag.length() > 0) {
+            mMode |= MODE_TAG;
+        } else {
+            mMode &= ~MODE_TAG;
+        }
+        mTag = tag;
+    }
+
+    public String getTagFilter() {
+        if ((mMode & MODE_TAG) == MODE_TAG)
+            return mTag;
+        return null;
+    }
+
+    public void setLogLevel(int level) {
+        if (level == -1) {
+            mMode &= ~MODE_LEVEL;
+        } else {
+            mMode |= MODE_LEVEL;
+            mLogLevel = level;
+        }
+
+    }
+
+    public int getLogLevel() {
+        if ((mMode & MODE_LEVEL) == MODE_LEVEL) {
+            return mLogLevel;
+        }
+
+        return -1;
+    }
+
+
+    public boolean supportsDelete() {
+        return mSupportsDelete ;
+    }
+
+    public boolean supportsEdit() {
+        return mSupportsEdit;
+    }
+
+    /**
+     * Sets the selected state of the filter.
+     * @param selected selection state.
+     */
+    public void setSelectedState(boolean selected) {
+        if (selected) {
+            if (mTabItem != null) {
+                mTabItem.setText(mName);
+            }
+            mUnreadCount = 0;
+        }
+        mIsCurrentTabItem = selected;
+    }
+
+    /**
+     * Adds a new message and optionally removes an old message.
+     * <p/>The new message is filtered through {@link #accept(LogMessage)}.
+     * Calls to {@link #flush()} from a UI thread will display it (and other
+     * pending messages) to the associated {@link Table}.
+     * @param logMessage the MessageData object to filter
+     * @return true if the message was accepted.
+     */
+    public boolean addMessage(LogMessage newMessage, LogMessage oldMessage) {
+        synchronized (mMessages) {
+            if (oldMessage != null) {
+                int index = mMessages.indexOf(oldMessage);
+                if (index != -1) {
+                    // TODO check that index will always be -1 or 0, as only the oldest message is ever removed.
+                    mMessages.remove(index);
+                    mRemovedMessageCount++;
+                }
+
+                // now we look for it in mNewMessages. This can happen if the new message is added
+                // and then removed because too many messages are added between calls to #flush()
+                index = mNewMessages.indexOf(oldMessage);
+                if (index != -1) {
+                    // TODO check that index will always be -1 or 0, as only the oldest message is ever removed.
+                    mNewMessages.remove(index);
+                }
+            }
+
+            boolean filter = accept(newMessage);
+
+            if (filter) {
+                // at this point the message is accepted, we add it to the list
+                mMessages.add(newMessage);
+                mNewMessages.add(newMessage);
+            }
+
+            return filter;
+        }
+    }
+
+    /**
+     * Removes all the items in the filter and its {@link Table}.
+     */
+    public void clear() {
+        mRemovedMessageCount = 0;
+        mNewMessages.clear();
+        mMessages.clear();
+        mTable.removeAll();
+    }
+
+    /**
+     * Filters a message.
+     * @param logMessage the Message
+     * @return true if the message is accepted by the filter.
+     */
+    boolean accept(LogMessage logMessage) {
+        // do the regular filtering now
+        if ((mMode & MODE_PID) == MODE_PID && mPid != logMessage.data.pid) {
+            return false;
+        }
+
+        if ((mMode & MODE_TAG) == MODE_TAG && (
+                logMessage.data.tag == null ||
+                logMessage.data.tag.equals(mTag) == false)) {
+            return false;
+        }
+
+        int msgLogLevel = logMessage.data.logLevel.getPriority();
+
+        // test the temp log filtering first, as it replaces the old one
+        if (mTempLogLevel != -1) {
+            if (mTempLogLevel > msgLogLevel) {
+                return false;
+            }
+        } else if ((mMode & MODE_LEVEL) == MODE_LEVEL &&
+                mLogLevel > msgLogLevel) {
+            return false;
+        }
+
+        // do the temp filtering now.
+        if (mTempKeywordFilters != null) {
+            String msg = logMessage.msg;
+
+            for (String kw : mTempKeywordFilters) {
+                try {
+                    if (msg.contains(kw) == false && msg.matches(kw) == false) {
+                        return false;
+                    }
+                } catch (PatternSyntaxException e) {
+                    // if the string is not a valid regular expression,
+                    // this exception is thrown.
+                    return false;
+                }
+            }
+        }
+
+        if (mTempPid != -1 && mTempPid != logMessage.data.pid) {
+           return false;
+        }
+
+        if (mTempTag != null && mTempTag.length() > 0) {
+            if (mTempTag.equals(logMessage.data.tag) == false) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Takes all the accepted messages and display them.
+     * This must be called from a UI thread.
+     */
+    @UiThread
+    public void flush() {
+        // if scroll bar is at the bottom, we will scroll
+        ScrollBar bar = mTable.getVerticalBar();
+        boolean scroll = bar.getMaximum() == bar.getSelection() + bar.getThumb();
+
+        // if we are not going to scroll, get the current first item being shown.
+        int topIndex = mTable.getTopIndex();
+
+        // disable drawing
+        mTable.setRedraw(false);
+
+        int totalCount = mNewMessages.size();
+
+        try {
+            // remove the items of the old messages.
+            for (int i = 0 ; i < mRemovedMessageCount && mTable.getItemCount() > 0 ; i++) {
+                mTable.remove(0);
+            }
+            mRemovedMessageCount = 0;
+
+            if (mUnreadCount > mTable.getItemCount()) {
+                mUnreadCount = mTable.getItemCount();
+            }
+
+            // add the new items
+            for (int i = 0  ; i < totalCount ; i++) {
+                LogMessage msg = mNewMessages.get(i);
+                addTableItem(msg);
+            }
+        } catch (SWTException e) {
+            // log the error and keep going. Content of the logcat table maybe unexpected
+            // but at least ddms won't crash.
+            Log.e("LogFilter", e);
+        }
+
+        // redraw
+        mTable.setRedraw(true);
+
+        // scroll if needed, by showing the last item
+        if (scroll) {
+            totalCount = mTable.getItemCount();
+            if (totalCount > 0) {
+                mTable.showItem(mTable.getItem(totalCount-1));
+            }
+        } else if (mRemovedMessageCount > 0) {
+            // we need to make sure the topIndex is still visible.
+            // Because really old items are removed from the list, this could make it disappear
+            // if we don't change the scroll value at all.
+
+            topIndex -= mRemovedMessageCount;
+            if (topIndex < 0) {
+                // looks like it disappeared. Lets just show the first item
+                mTable.showItem(mTable.getItem(0));
+            } else {
+                mTable.showItem(mTable.getItem(topIndex));
+            }
+        }
+
+        // if this filter is not the current one, we update the tab text
+        // with the amount of unread message
+        if (mIsCurrentTabItem == false) {
+            mUnreadCount += mNewMessages.size();
+            totalCount = mTable.getItemCount();
+            if (mUnreadCount > 0) {
+                mTabItem.setText(mName + " (" //$NON-NLS-1$
+                        + (mUnreadCount > totalCount ? totalCount : mUnreadCount)
+                        + ")");  //$NON-NLS-1$
+            } else {
+                mTabItem.setText(mName);  //$NON-NLS-1$
+            }
+        }
+
+        mNewMessages.clear();
+    }
+
+    void setColors(LogColors colors) {
+        mColors = colors;
+    }
+
+    int getUnreadCount() {
+        return mUnreadCount;
+    }
+
+    void setUnreadCount(int unreadCount) {
+        mUnreadCount = unreadCount;
+    }
+
+    void setSupportsDelete(boolean support) {
+        mSupportsDelete = support;
+    }
+
+    void setSupportsEdit(boolean support) {
+        mSupportsEdit = support;
+    }
+
+    void setTempKeywordFiltering(String[] segments) {
+        mTempKeywordFilters = segments;
+        mTempFilteringStatus = true;
+    }
+
+    void setTempPidFiltering(int pid) {
+        mTempPid = pid;
+        mTempFilteringStatus = true;
+    }
+
+    void setTempTagFiltering(String tag) {
+        mTempTag = tag;
+        mTempFilteringStatus = true;
+    }
+
+    void resetTempFiltering() {
+        if (mTempPid != -1 || mTempTag != null || mTempKeywordFilters != null) {
+            mTempFilteringStatus = true;
+        }
+
+        mTempPid = -1;
+        mTempTag = null;
+        mTempKeywordFilters = null;
+    }
+
+    void resetTempFilteringStatus() {
+        mTempFilteringStatus = false;
+    }
+
+    boolean getTempFilterStatus() {
+        return mTempFilteringStatus;
+    }
+
+
+    /**
+     * Add a TableItem for the index-th item of the buffer
+     * @param filter The index of the table in which to insert the item.
+     */
+    private void addTableItem(LogMessage msg) {
+        TableItem item = new TableItem(mTable, SWT.NONE);
+        item.setText(0, msg.data.time);
+        item.setText(1, new String(new char[] { msg.data.logLevel.getPriorityLetter() }));
+        item.setText(2, msg.data.pidString);
+        item.setText(3, msg.data.tag);
+        item.setText(4, msg.msg);
+
+        // add the buffer index as data
+        item.setData(msg);
+
+        if (msg.data.logLevel == LogLevel.INFO) {
+            item.setForeground(mColors.infoColor);
+        } else if (msg.data.logLevel == LogLevel.DEBUG) {
+            item.setForeground(mColors.debugColor);
+        } else if (msg.data.logLevel == LogLevel.ERROR) {
+            item.setForeground(mColors.errorColor);
+        } else if (msg.data.logLevel == LogLevel.WARN) {
+            item.setForeground(mColors.warningColor);
+        } else {
+            item.setForeground(mColors.verboseColor);
+        }
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogPanel.java
new file mode 100644
index 0000000..a347155
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogPanel.java
@@ -0,0 +1,1626 @@
+/*
+ * Copyright (C) 2007 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.ddmuilib.logcat;
+
+import com.android.ddmlib.AdbCommandRejectedException;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmlib.MultiLineReceiver;
+import com.android.ddmlib.ShellCommandUnresponsiveException;
+import com.android.ddmlib.TimeoutException;
+import com.android.ddmuilib.DdmUiPreferences;
+import com.android.ddmuilib.ITableFocusListener;
+import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator;
+import com.android.ddmuilib.SelectionDependentPanel;
+import com.android.ddmuilib.TableHelper;
+import com.android.ddmuilib.actions.ICommonAction;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.ControlListener;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.TabFolder;
+import org.eclipse.swt.widgets.TabItem;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.TableItem;
+import org.eclipse.swt.widgets.Text;
+
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class LogPanel extends SelectionDependentPanel {
+
+    private static final int STRING_BUFFER_LENGTH = 10000;
+
+    /** no filtering. Only one tab with everything. */
+    public static final int FILTER_NONE = 0;
+    /** manual mode for filter. all filters are manually created. */
+    public static final int FILTER_MANUAL = 1;
+    /** automatic mode for filter (pid mode).
+     * All filters are automatically created. */
+    public static final int FILTER_AUTO_PID = 2;
+    /** automatic mode for filter (tag mode).
+     * All filters are automatically created. */
+    public static final int FILTER_AUTO_TAG = 3;
+    /** Manual filtering mode + new filter for debug app, if needed */
+    public static final int FILTER_DEBUG = 4;
+
+    public static final int COLUMN_MODE_MANUAL = 0;
+    public static final int COLUMN_MODE_AUTO = 1;
+
+    public static String PREFS_TIME;
+    public static String PREFS_LEVEL;
+    public static String PREFS_PID;
+    public static String PREFS_TAG;
+    public static String PREFS_MESSAGE;
+
+    /**
+     * This pattern is meant to parse the first line of a log message with the option
+     * 'logcat -v long'. The first line represents the date, tag, severity, etc.. while the
+     * following lines are the message (can be several line).<br>
+     * This first line looks something like<br>
+     * <code>"[ 00-00 00:00:00.000 <pid>:0x<???> <severity>/<tag>]"</code>
+     * <br>
+     * Note: severity is one of V, D, I, W, or EM<br>
+     * Note: the fraction of second value can have any number of digit.
+     * Note the tag should be trim as it may have spaces at the end.
+     */
+    private static Pattern sLogPattern = Pattern.compile(
+            "^\\[\\s(\\d\\d-\\d\\d\\s\\d\\d:\\d\\d:\\d\\d\\.\\d+)" + //$NON-NLS-1$
+            "\\s+(\\d*):(0x[0-9a-fA-F]+)\\s([VDIWE])/(.*)\\]$"); //$NON-NLS-1$
+
+    /**
+     * Interface for Storage Filter manager. Implementation of this interface
+     * provide a custom way to archive an reload filters.
+     */
+    public interface ILogFilterStorageManager {
+
+        public LogFilter[] getFilterFromStore();
+
+        public void saveFilters(LogFilter[] filters);
+
+        public boolean requiresDefaultFilter();
+    }
+
+    private Composite mParent;
+    private IPreferenceStore mStore;
+
+    /** top object in the view */
+    private TabFolder mFolders;
+
+    private LogColors mColors;
+
+    private ILogFilterStorageManager mFilterStorage;
+
+    private LogCatOuputReceiver mCurrentLogCat;
+
+    /**
+     * Circular buffer containing the logcat output. This is unfiltered.
+     * The valid content goes from <code>mBufferStart</code> to
+     * <code>mBufferEnd - 1</code>. Therefore its number of item is
+     * <code>mBufferEnd - mBufferStart</code>.
+     */
+    private LogMessage[] mBuffer = new LogMessage[STRING_BUFFER_LENGTH];
+
+    /** Represents the oldest message in the buffer */
+    private int mBufferStart = -1;
+
+    /**
+     * Represents the next usable item in the buffer to receive new message.
+     * This can be equal to mBufferStart, but when used mBufferStart will be
+     * incremented as well.
+     */
+    private int mBufferEnd = -1;
+
+    /** Filter list */
+    private LogFilter[] mFilters;
+
+    /** Default filter */
+    private LogFilter mDefaultFilter;
+
+    /** Current filter being displayed */
+    private LogFilter mCurrentFilter;
+
+    /** Filtering mode */
+    private int mFilterMode = FILTER_NONE;
+
+    /** Device currently running logcat */
+    private IDevice mCurrentLoggedDevice = null;
+
+    private ICommonAction mDeleteFilterAction;
+    private ICommonAction mEditFilterAction;
+
+    private ICommonAction[] mLogLevelActions;
+
+    /** message data, separated from content for multi line messages */
+    protected static class LogMessageInfo {
+        public LogLevel logLevel;
+        public int pid;
+        public String pidString;
+        public String tag;
+        public String time;
+    }
+
+    /** pointer to the latest LogMessageInfo. this is used for multi line
+     * log message, to reuse the info regarding level, pid, etc...
+     */
+    private LogMessageInfo mLastMessageInfo = null;
+
+    private boolean mPendingAsyncRefresh = false;
+
+    private String mDefaultLogSave;
+
+    private int mColumnMode = COLUMN_MODE_MANUAL;
+    private Font mDisplayFont;
+
+    private ITableFocusListener mGlobalListener;
+
+    private LogCatViewInterface mLogCatViewInterface = null;
+
+    /** message data, separated from content for multi line messages */
+    protected static class LogMessage {
+        public LogMessageInfo data;
+        public String msg;
+
+        @Override
+        public String toString() {
+            return data.time + ": " //$NON-NLS-1$
+                + data.logLevel + "/" //$NON-NLS-1$
+                + data.tag + "(" //$NON-NLS-1$
+                + data.pidString + "): " //$NON-NLS-1$
+                + msg;
+        }
+    }
+
+    /**
+     * objects able to receive the output of a remote shell command,
+     * specifically a logcat command in this case
+     */
+    private final class LogCatOuputReceiver extends MultiLineReceiver {
+
+        public boolean isCancelled = false;
+
+        public LogCatOuputReceiver() {
+            super();
+
+            setTrimLine(false);
+        }
+
+        @Override
+        public void processNewLines(String[] lines) {
+            if (isCancelled == false) {
+                processLogLines(lines);
+            }
+        }
+
+        @Override
+        public boolean isCancelled() {
+            return isCancelled;
+        }
+    }
+
+    /**
+     * Parser class for the output of a "ps" shell command executed on a device.
+     * This class looks for a specific pid to find the process name from it.
+     * Once found, the name is used to update a filter and a tab object
+     *
+     */
+    private class PsOutputReceiver extends MultiLineReceiver {
+
+        private LogFilter mFilter;
+
+        private TabItem mTabItem;
+
+        private int mPid;
+
+        /** set to true when we've found the pid we're looking for */
+        private boolean mDone = false;
+
+        PsOutputReceiver(int pid, LogFilter filter, TabItem tabItem) {
+            mPid = pid;
+            mFilter = filter;
+            mTabItem = tabItem;
+        }
+
+        @Override
+        public boolean isCancelled() {
+            return mDone;
+        }
+
+        @Override
+        public void processNewLines(String[] lines) {
+            for (String line : lines) {
+                if (line.startsWith("USER")) { //$NON-NLS-1$
+                    continue;
+                }
+                // get the pid.
+                int index = line.indexOf(' ');
+                if (index == -1) {
+                    continue;
+                }
+                // look for the next non blank char
+                index++;
+                while (line.charAt(index) == ' ') {
+                    index++;
+                }
+
+                // this is the start of the pid.
+                // look for the end.
+                int index2 = line.indexOf(' ', index);
+
+                // get the line
+                String pidStr = line.substring(index, index2);
+                int pid = Integer.parseInt(pidStr);
+                if (pid != mPid) {
+                    continue;
+                } else {
+                    // get the process name
+                    index = line.lastIndexOf(' ');
+                    final String name = line.substring(index + 1);
+
+                    mFilter.setName(name);
+
+                    // update the tab
+                    Display d = mFolders.getDisplay();
+                    d.asyncExec(new Runnable() {
+                       @Override
+                    public void run() {
+                           mTabItem.setText(name);
+                       }
+                    });
+
+                    // we're done with this ps.
+                    mDone = true;
+                    return;
+                }
+            }
+        }
+
+    }
+
+    /**
+     * Interface implemented by the LogCatView in Eclipse for particular action on double-click.
+     */
+    public interface LogCatViewInterface {
+        public void onDoubleClick();
+    }
+
+    /**
+     * Create the log view with some default parameters
+     * @param colors The display color object
+     * @param filterStorage the storage for user defined filters.
+     * @param mode The filtering mode
+     */
+    public LogPanel(LogColors colors,
+            ILogFilterStorageManager filterStorage, int mode) {
+        mColors = colors;
+        mFilterMode = mode;
+        mFilterStorage = filterStorage;
+        mStore = DdmUiPreferences.getStore();
+    }
+
+    public void setActions(ICommonAction deleteAction, ICommonAction editAction,
+            ICommonAction[] logLevelActions) {
+        mDeleteFilterAction = deleteAction;
+        mEditFilterAction = editAction;
+        mLogLevelActions = logLevelActions;
+    }
+
+    /**
+     * Sets the column mode. Must be called before creatUI
+     * @param mode the column mode. Valid values are COLUMN_MOD_MANUAL and
+     *  COLUMN_MODE_AUTO
+     */
+    public void setColumnMode(int mode) {
+        mColumnMode  = mode;
+    }
+
+    /**
+     * Sets the display font.
+     * @param font The display font.
+     */
+    public void setFont(Font font) {
+        mDisplayFont = font;
+
+        if (mFilters != null) {
+            for (LogFilter f : mFilters) {
+                Table table = f.getTable();
+                if (table != null) {
+                    table.setFont(font);
+                }
+            }
+        }
+
+        if (mDefaultFilter != null) {
+            Table table = mDefaultFilter.getTable();
+            if (table != null) {
+                table.setFont(font);
+            }
+        }
+    }
+
+    /**
+     * Sent when a new device is selected. The new device can be accessed
+     * with {@link #getCurrentDevice()}.
+     */
+    @Override
+    public void deviceSelected() {
+        startLogCat(getCurrentDevice());
+    }
+
+    /**
+     * Sent when a new client is selected. The new client can be accessed
+     * with {@link #getCurrentClient()}.
+     */
+    @Override
+    public void clientSelected() {
+        // pass
+    }
+
+
+    /**
+     * Creates a control capable of displaying some information.  This is
+     * called once, when the application is initializing, from the UI thread.
+     */
+    @Override
+    protected Control createControl(Composite parent) {
+        mParent = parent;
+
+        Composite top = new Composite(parent, SWT.NONE);
+        top.setLayoutData(new GridData(GridData.FILL_BOTH));
+        top.setLayout(new GridLayout(1, false));
+
+        // create the tab folder
+        mFolders = new TabFolder(top, SWT.NONE);
+        mFolders.setLayoutData(new GridData(GridData.FILL_BOTH));
+        mFolders.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                if (mCurrentFilter != null) {
+                    mCurrentFilter.setSelectedState(false);
+                }
+                mCurrentFilter = getCurrentFilter();
+                mCurrentFilter.setSelectedState(true);
+                updateColumns(mCurrentFilter.getTable());
+                if (mCurrentFilter.getTempFilterStatus()) {
+                    initFilter(mCurrentFilter);
+                }
+                selectionChanged(mCurrentFilter);
+            }
+        });
+
+
+        Composite bottom = new Composite(top, SWT.NONE);
+        bottom.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        bottom.setLayout(new GridLayout(3, false));
+
+        Label label = new Label(bottom, SWT.NONE);
+        label.setText("Filter:");
+
+        final Text filterText = new Text(bottom, SWT.SINGLE | SWT.BORDER);
+        filterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        filterText.addModifyListener(new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent e) {
+                updateFilteringWith(filterText.getText());
+            }
+        });
+
+        /*
+        Button addFilterBtn = new Button(bottom, SWT.NONE);
+        addFilterBtn.setImage(mImageLoader.loadImage("add.png", //$NON-NLS-1$
+                addFilterBtn.getDisplay()));
+        */
+
+        // get the filters
+        createFilters();
+
+        // for each filter, create a tab.
+        int index = 0;
+
+        if (mDefaultFilter != null) {
+            createTab(mDefaultFilter, index++, false);
+        }
+
+        if (mFilters != null) {
+            for (LogFilter f : mFilters) {
+                createTab(f, index++, false);
+            }
+        }
+
+        return top;
+    }
+
+    @Override
+    protected void postCreation() {
+        // pass
+    }
+
+    /**
+     * Sets the focus to the proper object.
+     */
+    @Override
+    public void setFocus() {
+        mFolders.setFocus();
+    }
+
+
+    /**
+     * Starts a new logcat and set mCurrentLogCat as the current receiver.
+     * @param device the device to connect logcat to.
+     */
+    public void startLogCat(final IDevice device) {
+        if (device == mCurrentLoggedDevice) {
+            return;
+        }
+
+        // if we have a logcat already running
+        if (mCurrentLoggedDevice != null) {
+            stopLogCat(false);
+            mCurrentLoggedDevice = null;
+        }
+
+        resetUI(false);
+
+        if (device != null) {
+            // create a new output receiver
+            mCurrentLogCat = new LogCatOuputReceiver();
+
+            // start the logcat in a different thread
+            new Thread("Logcat")  { //$NON-NLS-1$
+                @Override
+                public void run() {
+
+                    while (device.isOnline() == false &&
+                            mCurrentLogCat != null &&
+                            mCurrentLogCat.isCancelled == false) {
+                        try {
+                            sleep(2000);
+                        } catch (InterruptedException e) {
+                            return;
+                        }
+                    }
+
+                    if (mCurrentLogCat == null || mCurrentLogCat.isCancelled) {
+                        // logcat was stopped/cancelled before the device became ready.
+                        return;
+                    }
+
+                    try {
+                        mCurrentLoggedDevice = device;
+                        device.executeShellCommand("logcat -v long", mCurrentLogCat, 0 /*timeout*/); //$NON-NLS-1$
+                    } catch (Exception e) {
+                        Log.e("Logcat", e);
+                    } finally {
+                        // at this point the command is terminated.
+                        mCurrentLogCat = null;
+                        mCurrentLoggedDevice = null;
+                    }
+                }
+            }.start();
+        }
+    }
+
+    /** Stop the current logcat */
+    public void stopLogCat(boolean inUiThread) {
+        if (mCurrentLogCat != null) {
+            mCurrentLogCat.isCancelled = true;
+
+            // when the thread finishes, no one will reference that object
+            // and it'll be destroyed
+            mCurrentLogCat = null;
+
+            // reset the content buffer
+            for (int i = 0 ; i < STRING_BUFFER_LENGTH; i++) {
+                mBuffer[i] = null;
+            }
+
+            // because it's a circular buffer, it's hard to know if
+            // the array is empty with both start/end at 0 or if it's full
+            // with both start/end at 0 as well. So to mean empty, we use -1
+            mBufferStart = -1;
+            mBufferEnd = -1;
+
+            resetFilters();
+            resetUI(inUiThread);
+        }
+    }
+
+    /**
+     * Adds a new Filter. This methods displays the UI to create the filter
+     * and set up its parameters.<br>
+     * <b>MUST</b> be called from the ui thread.
+     *
+     */
+    public void addFilter() {
+        EditFilterDialog dlg = new EditFilterDialog(mFolders.getShell());
+        if (dlg.open()) {
+            synchronized (mBuffer) {
+                // get the new filter in the array
+                LogFilter filter = dlg.getFilter();
+                addFilterToArray(filter);
+
+                int index = mFilters.length - 1;
+                if (mDefaultFilter != null) {
+                    index++;
+                }
+
+                if (false) {
+
+                    for (LogFilter f : mFilters) {
+                        if (f.uiReady()) {
+                            f.dispose();
+                        }
+                    }
+                    if (mDefaultFilter != null && mDefaultFilter.uiReady()) {
+                        mDefaultFilter.dispose();
+                    }
+
+                    // for each filter, create a tab.
+                    int i = 0;
+                    if (mFilters != null) {
+                        for (LogFilter f : mFilters) {
+                            createTab(f, i++, true);
+                        }
+                    }
+                    if (mDefaultFilter != null) {
+                        createTab(mDefaultFilter, i++, true);
+                    }
+                } else {
+
+                    // create ui for the filter.
+                    createTab(filter, index, true);
+
+                    // reset the default as it shouldn't contain the content of
+                    // this new filter.
+                    if (mDefaultFilter != null) {
+                        initDefaultFilter();
+                    }
+                }
+
+                // select the new filter
+                if (mCurrentFilter != null) {
+                    mCurrentFilter.setSelectedState(false);
+                }
+                mFolders.setSelection(index);
+                filter.setSelectedState(true);
+                mCurrentFilter = filter;
+
+                selectionChanged(filter);
+
+                // finally we update the filtering mode if needed
+                if (mFilterMode == FILTER_NONE) {
+                    mFilterMode = FILTER_MANUAL;
+                }
+
+                mFilterStorage.saveFilters(mFilters);
+
+            }
+        }
+    }
+
+    /**
+     * Edits the current filter. The method displays the UI to edit the filter.
+     */
+    public void editFilter() {
+        if (mCurrentFilter != null && mCurrentFilter != mDefaultFilter) {
+            EditFilterDialog dlg = new EditFilterDialog(
+                    mFolders.getShell(), mCurrentFilter);
+            if (dlg.open()) {
+                synchronized (mBuffer) {
+                    // at this point the filter has been updated.
+                    // so we update its content
+                    initFilter(mCurrentFilter);
+
+                    // and the content of the "other" filter as well.
+                    if (mDefaultFilter != null) {
+                        initDefaultFilter();
+                    }
+
+                    mFilterStorage.saveFilters(mFilters);
+                }
+            }
+        }
+    }
+
+    /**
+     * Deletes the current filter.
+     */
+    public void deleteFilter() {
+        synchronized (mBuffer) {
+            if (mCurrentFilter != null && mCurrentFilter != mDefaultFilter) {
+                // remove the filter from the list
+                removeFilterFromArray(mCurrentFilter);
+                mCurrentFilter.dispose();
+
+                // select the new filter
+                mFolders.setSelection(0);
+                if (mFilters.length > 0) {
+                    mCurrentFilter = mFilters[0];
+                } else {
+                    mCurrentFilter = mDefaultFilter;
+                }
+
+                selectionChanged(mCurrentFilter);
+
+                // update the content of the "other" filter to include what was filtered out
+                // by the deleted filter.
+                if (mDefaultFilter != null) {
+                    initDefaultFilter();
+                }
+
+                mFilterStorage.saveFilters(mFilters);
+            }
+        }
+    }
+
+    /**
+     * saves the current selection in a text file.
+     * @return false if the saving failed.
+     */
+    public boolean save() {
+        synchronized (mBuffer) {
+            FileDialog dlg = new FileDialog(mParent.getShell(), SWT.SAVE);
+            String fileName;
+
+            dlg.setText("Save log...");
+            dlg.setFileName("log.txt");
+            String defaultPath = mDefaultLogSave;
+            if (defaultPath == null) {
+                defaultPath = System.getProperty("user.home"); //$NON-NLS-1$
+            }
+            dlg.setFilterPath(defaultPath);
+            dlg.setFilterNames(new String[] {
+                "Text Files (*.txt)"
+            });
+            dlg.setFilterExtensions(new String[] {
+                "*.txt"
+            });
+
+            fileName = dlg.open();
+            if (fileName != null) {
+                mDefaultLogSave = dlg.getFilterPath();
+
+                // get the current table and its selection
+                Table currentTable = mCurrentFilter.getTable();
+
+                int[] selection = currentTable.getSelectionIndices();
+
+                // we need to sort the items to be sure.
+                Arrays.sort(selection);
+
+                // loop on the selection and output the file.
+                FileWriter writer = null;
+                try {
+                    writer = new FileWriter(fileName);
+
+                    for (int i : selection) {
+                        TableItem item = currentTable.getItem(i);
+                        LogMessage msg = (LogMessage)item.getData();
+                        String line = msg.toString();
+                        writer.write(line);
+                        writer.write('\n');
+                    }
+                    writer.flush();
+
+                } catch (IOException e) {
+                    return false;
+                } finally {
+                    if (writer != null) {
+                        try {
+                            writer.close();
+                        } catch (IOException e) {
+                            // ignore
+                        }
+                    }
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Empty the current circular buffer.
+     */
+    public void clear() {
+        synchronized (mBuffer) {
+            for (int i = 0 ; i < STRING_BUFFER_LENGTH; i++) {
+                mBuffer[i] = null;
+            }
+
+            mBufferStart = -1;
+            mBufferEnd = -1;
+
+            // now we clear the existing filters
+            for (LogFilter filter : mFilters) {
+                filter.clear();
+            }
+
+            // and the default one
+            if (mDefaultFilter != null) {
+                mDefaultFilter.clear();
+            }
+        }
+    }
+
+    /**
+     * Copies the current selection of the current filter as multiline text.
+     *
+     * @param clipboard The clipboard to place the copied content.
+     */
+    public void copy(Clipboard clipboard) {
+        // get the current table and its selection
+        Table currentTable = mCurrentFilter.getTable();
+
+        copyTable(clipboard, currentTable);
+    }
+
+    /**
+     * Selects all lines.
+     */
+    public void selectAll() {
+        Table currentTable = mCurrentFilter.getTable();
+        currentTable.selectAll();
+    }
+
+    /**
+     * Sets a TableFocusListener which will be notified when one of the tables
+     * gets or loses focus.
+     *
+     * @param listener
+     */
+    public void setTableFocusListener(ITableFocusListener listener) {
+        // record the global listener, to make sure table created after
+        // this call will still be setup.
+        mGlobalListener = listener;
+
+        // now we setup the existing filters
+        for (LogFilter filter : mFilters) {
+            Table table = filter.getTable();
+
+            addTableToFocusListener(table);
+        }
+
+        // and the default one
+        if (mDefaultFilter != null) {
+            addTableToFocusListener(mDefaultFilter.getTable());
+        }
+    }
+
+    /**
+     * Sets up a Table object to notify the global Table Focus listener when it
+     * gets or loses the focus.
+     *
+     * @param table the Table object.
+     */
+    private void addTableToFocusListener(final Table table) {
+        // create the activator for this table
+        final IFocusedTableActivator activator = new IFocusedTableActivator() {
+            @Override
+            public void copy(Clipboard clipboard) {
+                copyTable(clipboard, table);
+            }
+
+            @Override
+            public void selectAll() {
+                table.selectAll();
+            }
+        };
+
+        // add the focus listener on the table to notify the global listener
+        table.addFocusListener(new FocusListener() {
+            @Override
+            public void focusGained(FocusEvent e) {
+                mGlobalListener.focusGained(activator);
+            }
+
+            @Override
+            public void focusLost(FocusEvent e) {
+                mGlobalListener.focusLost(activator);
+            }
+        });
+    }
+
+    /**
+     * Copies the current selection of a Table into the provided Clipboard, as
+     * multi-line text.
+     *
+     * @param clipboard The clipboard to place the copied content.
+     * @param table The table to copy from.
+     */
+    private static void copyTable(Clipboard clipboard, Table table) {
+        int[] selection = table.getSelectionIndices();
+
+        // we need to sort the items to be sure.
+        Arrays.sort(selection);
+
+        // all lines must be concatenated.
+        StringBuilder sb = new StringBuilder();
+
+        // loop on the selection and output the file.
+        for (int i : selection) {
+            TableItem item = table.getItem(i);
+            LogMessage msg = (LogMessage)item.getData();
+            String line = msg.toString();
+            sb.append(line);
+            sb.append('\n');
+        }
+
+        // now add that to the clipboard
+        clipboard.setContents(new Object[] {
+            sb.toString()
+        }, new Transfer[] {
+            TextTransfer.getInstance()
+        });
+    }
+
+    /**
+     * Sets the log level for the current filter, but does not save it.
+     * @param i
+     */
+    public void setCurrentFilterLogLevel(int i) {
+        LogFilter filter = getCurrentFilter();
+
+        filter.setLogLevel(i);
+
+        initFilter(filter);
+    }
+
+    /**
+     * Creates a new tab in the folderTab item. Must be called from the ui
+     *      thread.
+     * @param filter The filter associated with the tab.
+     * @param index the index of the tab. if -1, the tab will be added at the
+     *          end.
+     * @param fillTable If true the table is filled with the current content of
+     *          the buffer.
+     * @return The TabItem object that was created.
+     */
+    private TabItem createTab(LogFilter filter, int index, boolean fillTable) {
+        synchronized (mBuffer) {
+            TabItem item = null;
+            if (index != -1) {
+                item = new TabItem(mFolders, SWT.NONE, index);
+            } else {
+                item = new TabItem(mFolders, SWT.NONE);
+            }
+            item.setText(filter.getName());
+
+            // set the control (the parent is the TabFolder item, always)
+            Composite top = new Composite(mFolders, SWT.NONE);
+            item.setControl(top);
+
+            top.setLayout(new FillLayout());
+
+            // create the ui, first the table
+            final Table t = new Table(top, SWT.MULTI | SWT.FULL_SELECTION);
+            t.addSelectionListener(new SelectionAdapter() {
+                @Override
+                public void widgetDefaultSelected(SelectionEvent e) {
+                    if (mLogCatViewInterface != null) {
+                        mLogCatViewInterface.onDoubleClick();
+                    }
+                }
+            });
+
+            if (mDisplayFont != null) {
+                t.setFont(mDisplayFont);
+            }
+
+            // give the ui objects to the filters.
+            filter.setWidgets(item, t);
+
+            t.setHeaderVisible(true);
+            t.setLinesVisible(false);
+
+            if (mGlobalListener != null) {
+                addTableToFocusListener(t);
+            }
+
+            // create a controllistener that will handle the resizing of all the
+            // columns (except the last) and of the table itself.
+            ControlListener listener = null;
+            if (mColumnMode == COLUMN_MODE_AUTO) {
+                listener = new ControlListener() {
+                    @Override
+                    public void controlMoved(ControlEvent e) {
+                    }
+
+                    @Override
+                    public void controlResized(ControlEvent e) {
+                        Rectangle r = t.getClientArea();
+
+                        // get the size of all but the last column
+                        int total = t.getColumn(0).getWidth();
+                        total += t.getColumn(1).getWidth();
+                        total += t.getColumn(2).getWidth();
+                        total += t.getColumn(3).getWidth();
+
+                        if (r.width > total) {
+                            t.getColumn(4).setWidth(r.width-total);
+                        }
+                    }
+                };
+
+                t.addControlListener(listener);
+            }
+
+            // then its column
+            TableColumn col = TableHelper.createTableColumn(t, "Time", SWT.LEFT,
+                    "00-00 00:00:00", //$NON-NLS-1$
+                    PREFS_TIME, mStore);
+            if (mColumnMode == COLUMN_MODE_AUTO) {
+                col.addControlListener(listener);
+            }
+
+            col = TableHelper.createTableColumn(t, "", SWT.CENTER,
+                    "D", //$NON-NLS-1$
+                    PREFS_LEVEL, mStore);
+            if (mColumnMode == COLUMN_MODE_AUTO) {
+                col.addControlListener(listener);
+            }
+
+            col = TableHelper.createTableColumn(t, "pid", SWT.LEFT,
+                    "9999", //$NON-NLS-1$
+                    PREFS_PID, mStore);
+            if (mColumnMode == COLUMN_MODE_AUTO) {
+                col.addControlListener(listener);
+            }
+
+            col = TableHelper.createTableColumn(t, "tag", SWT.LEFT,
+                    "abcdefgh",  //$NON-NLS-1$
+                    PREFS_TAG, mStore);
+            if (mColumnMode == COLUMN_MODE_AUTO) {
+                col.addControlListener(listener);
+            }
+
+            col = TableHelper.createTableColumn(t, "Message", SWT.LEFT,
+                    "abcdefghijklmnopqrstuvwxyz0123456789",  //$NON-NLS-1$
+                    PREFS_MESSAGE, mStore);
+            if (mColumnMode == COLUMN_MODE_AUTO) {
+                // instead of listening on resize for the last column, we make
+                // it non resizable.
+                col.setResizable(false);
+            }
+
+            if (fillTable) {
+                initFilter(filter);
+            }
+            return item;
+        }
+    }
+
+    protected void updateColumns(Table table) {
+        if (table != null) {
+            int index = 0;
+            TableColumn col;
+
+            col = table.getColumn(index++);
+            col.setWidth(mStore.getInt(PREFS_TIME));
+
+            col = table.getColumn(index++);
+            col.setWidth(mStore.getInt(PREFS_LEVEL));
+
+            col = table.getColumn(index++);
+            col.setWidth(mStore.getInt(PREFS_PID));
+
+            col = table.getColumn(index++);
+            col.setWidth(mStore.getInt(PREFS_TAG));
+
+            col = table.getColumn(index++);
+            col.setWidth(mStore.getInt(PREFS_MESSAGE));
+        }
+    }
+
+    public void resetUI(boolean inUiThread) {
+        if (mFilterMode == FILTER_AUTO_PID || mFilterMode == FILTER_AUTO_TAG) {
+            if (inUiThread) {
+                mFolders.dispose();
+                mParent.pack(true);
+                createControl(mParent);
+            } else {
+                Display d = mFolders.getDisplay();
+
+                // run sync as we need to update right now.
+                d.syncExec(new Runnable() {
+                    @Override
+                    public void run() {
+                        mFolders.dispose();
+                        mParent.pack(true);
+                        createControl(mParent);
+                    }
+                });
+            }
+        } else  {
+            // the ui is static we just empty it.
+            if (mFolders.isDisposed() == false) {
+                if (inUiThread) {
+                    emptyTables();
+                } else {
+                    Display d = mFolders.getDisplay();
+
+                    // run sync as we need to update right now.
+                    d.syncExec(new Runnable() {
+                        @Override
+                        public void run() {
+                            if (mFolders.isDisposed() == false) {
+                                emptyTables();
+                            }
+                        }
+                    });
+                }
+            }
+        }
+    }
+
+    /**
+     * Process new Log lines coming from {@link LogCatOuputReceiver}.
+     * @param lines the new lines
+     */
+    protected void processLogLines(String[] lines) {
+        // WARNING: this will not work if the string contains more line than
+        // the buffer holds.
+
+        if (lines.length > STRING_BUFFER_LENGTH) {
+            Log.e("LogCat", "Receiving more lines than STRING_BUFFER_LENGTH");
+        }
+
+        // parse the lines and create LogMessage that are stored in a temporary list
+        final ArrayList<LogMessage> newMessages = new ArrayList<LogMessage>();
+
+        synchronized (mBuffer) {
+            for (String line : lines) {
+                // ignore empty lines.
+                if (line.length() > 0) {
+                    // check for header lines.
+                    Matcher matcher = sLogPattern.matcher(line);
+                    if (matcher.matches()) {
+                        // this is a header line, parse the header and keep it around.
+                        mLastMessageInfo = new LogMessageInfo();
+
+                        mLastMessageInfo.time = matcher.group(1);
+                        mLastMessageInfo.pidString = matcher.group(2);
+                        mLastMessageInfo.pid = Integer.valueOf(mLastMessageInfo.pidString);
+                        mLastMessageInfo.logLevel = LogLevel.getByLetterString(matcher.group(4));
+                        mLastMessageInfo.tag = matcher.group(5).trim();
+                    } else {
+                        // This is not a header line.
+                        // Create a new LogMessage and process it.
+                        LogMessage mc = new LogMessage();
+
+                        if (mLastMessageInfo == null) {
+                            // The first line of output wasn't preceded
+                            // by a header line; make something up so
+                            // that users of mc.data don't NPE.
+                            mLastMessageInfo = new LogMessageInfo();
+                            mLastMessageInfo.time = "??-?? ??:??:??.???"; //$NON-NLS1$
+                            mLastMessageInfo.pidString = "<unknown>"; //$NON-NLS1$
+                            mLastMessageInfo.pid = 0;
+                            mLastMessageInfo.logLevel = LogLevel.INFO;
+                            mLastMessageInfo.tag = "<unknown>"; //$NON-NLS1$
+                        }
+
+                        // If someone printed a log message with
+                        // embedded '\n' characters, there will
+                        // one header line followed by multiple text lines.
+                        // Use the last header that we saw.
+                        mc.data = mLastMessageInfo;
+
+                        // tabs seem to display as only 1 tab so we replace the leading tabs
+                        // by 4 spaces.
+                        mc.msg = line.replaceAll("\t", "    "); //$NON-NLS-1$ //$NON-NLS-2$
+
+                        // process the new LogMessage.
+                        processNewMessage(mc);
+
+                        // store the new LogMessage
+                        newMessages.add(mc);
+                    }
+                }
+            }
+
+            // if we don't have a pending Runnable that will do the refresh, we ask the Display
+            // to run one in the UI thread.
+            if (mPendingAsyncRefresh == false) {
+                mPendingAsyncRefresh = true;
+
+                try {
+                    Display display = mFolders.getDisplay();
+
+                    // run in sync because this will update the buffer start/end indices
+                    display.asyncExec(new Runnable() {
+                        @Override
+                        public void run() {
+                            asyncRefresh();
+                        }
+                    });
+                } catch (SWTException e) {
+                    // display is disposed, we're probably quitting. Let's stop.
+                    stopLogCat(false);
+                }
+            }
+        }
+    }
+
+    /**
+     * Refreshes the UI with new messages.
+     */
+    private void asyncRefresh() {
+        if (mFolders.isDisposed() == false) {
+            synchronized (mBuffer) {
+                try {
+                    // the circular buffer has been updated, let have the filter flush their
+                    // display with the new messages.
+                    if (mFilters != null) {
+                        for (LogFilter f : mFilters) {
+                            f.flush();
+                        }
+                    }
+
+                    if (mDefaultFilter != null) {
+                        mDefaultFilter.flush();
+                    }
+                } finally {
+                    // the pending refresh is done.
+                    mPendingAsyncRefresh = false;
+                }
+            }
+        } else {
+            stopLogCat(true);
+        }
+    }
+
+    /**
+     * Processes a new Message.
+     * <p/>This adds the new message to the buffer, and gives it to the existing filters.
+     * @param newMessage
+     */
+    private void processNewMessage(LogMessage newMessage) {
+        // if we are in auto filtering mode, make sure we have
+        // a filter for this
+        if (mFilterMode == FILTER_AUTO_PID ||
+                mFilterMode == FILTER_AUTO_TAG) {
+           checkFilter(newMessage.data);
+        }
+
+        // compute the index where the message goes.
+        // was the buffer empty?
+        int messageIndex = -1;
+        if (mBufferStart == -1) {
+            messageIndex = mBufferStart = 0;
+            mBufferEnd = 1;
+        } else {
+            messageIndex = mBufferEnd;
+
+            // increment the next usable slot index
+            mBufferEnd = (mBufferEnd + 1) % STRING_BUFFER_LENGTH;
+
+            // check we aren't overwriting start
+            if (mBufferEnd == mBufferStart) {
+                mBufferStart = (mBufferStart + 1) % STRING_BUFFER_LENGTH;
+            }
+        }
+
+        LogMessage oldMessage = null;
+
+        // record the message that was there before
+        if (mBuffer[messageIndex] != null) {
+            oldMessage = mBuffer[messageIndex];
+        }
+
+        // then add the new one
+        mBuffer[messageIndex] = newMessage;
+
+        // give the new message to every filters.
+        boolean filtered = false;
+        if (mFilters != null) {
+            for (LogFilter f : mFilters) {
+                filtered |= f.addMessage(newMessage, oldMessage);
+            }
+        }
+        if (filtered == false && mDefaultFilter != null) {
+            mDefaultFilter.addMessage(newMessage, oldMessage);
+        }
+    }
+
+    private void createFilters() {
+        if (mFilterMode == FILTER_DEBUG || mFilterMode == FILTER_MANUAL) {
+            // unarchive the filters.
+            mFilters = mFilterStorage.getFilterFromStore();
+
+            // set the colors
+            if (mFilters != null) {
+                for (LogFilter f : mFilters) {
+                    f.setColors(mColors);
+                }
+            }
+
+            if (mFilterStorage.requiresDefaultFilter()) {
+                mDefaultFilter = new LogFilter("Log");
+                mDefaultFilter.setColors(mColors);
+                mDefaultFilter.setSupportsDelete(false);
+                mDefaultFilter.setSupportsEdit(false);
+            }
+        } else if (mFilterMode == FILTER_NONE) {
+            // if the filtering mode is "none", we create a single filter that
+            // will receive all
+            mDefaultFilter = new LogFilter("Log");
+            mDefaultFilter.setColors(mColors);
+            mDefaultFilter.setSupportsDelete(false);
+            mDefaultFilter.setSupportsEdit(false);
+        }
+    }
+
+    /** Checks if there's an automatic filter for this md and if not
+     * adds the filter and the ui.
+     * This must be called from the UI!
+     * @param md
+     * @return true if the filter existed already
+     */
+    private boolean checkFilter(final LogMessageInfo md) {
+        if (true)
+            return true;
+        // look for a filter that matches the pid
+        if (mFilterMode == FILTER_AUTO_PID) {
+            for (LogFilter f : mFilters) {
+                if (f.getPidFilter() == md.pid) {
+                    return true;
+                }
+            }
+        } else if (mFilterMode == FILTER_AUTO_TAG) {
+            for (LogFilter f : mFilters) {
+                if (f.getTagFilter().equals(md.tag)) {
+                    return true;
+                }
+            }
+        }
+
+        // if we reach this point, no filter was found.
+        // create a filter with a temporary name of the pid
+        final LogFilter newFilter = new LogFilter(md.pidString);
+        String name = null;
+        if (mFilterMode == FILTER_AUTO_PID) {
+            newFilter.setPidMode(md.pid);
+
+            // ask the monitor thread if it knows the pid.
+            name = mCurrentLoggedDevice.getClientName(md.pid);
+        } else {
+            newFilter.setTagMode(md.tag);
+            name = md.tag;
+        }
+        addFilterToArray(newFilter);
+
+        final String fname = name;
+
+        // create the tabitem
+        final TabItem newTabItem = createTab(newFilter, -1, true);
+
+        // if the name is unknown
+        if (fname == null) {
+            // we need to find the process running under that pid.
+            // launch a thread do a ps on the device
+            new Thread("remote PS") { //$NON-NLS-1$
+                @Override
+                public void run() {
+                    // create the receiver
+                    PsOutputReceiver psor = new PsOutputReceiver(md.pid,
+                            newFilter, newTabItem);
+
+                    // execute ps
+                    try {
+                        mCurrentLoggedDevice.executeShellCommand("ps", psor); //$NON-NLS-1$
+                    } catch (IOException e) {
+                        // Ignore
+                    } catch (TimeoutException e) {
+                        // Ignore
+                    } catch (AdbCommandRejectedException e) {
+                        // Ignore
+                    } catch (ShellCommandUnresponsiveException e) {
+                        // Ignore
+                    }
+                }
+            }.start();
+        }
+
+        return false;
+    }
+
+    /**
+     * Adds a new filter to the current filter array, and set its colors
+     * @param newFilter The filter to add
+     */
+    private void addFilterToArray(LogFilter newFilter) {
+        // set the colors
+        newFilter.setColors(mColors);
+
+        // add it to the array.
+        if (mFilters != null && mFilters.length > 0) {
+            LogFilter[] newFilters = new LogFilter[mFilters.length+1];
+            System.arraycopy(mFilters, 0, newFilters, 0, mFilters.length);
+            newFilters[mFilters.length] = newFilter;
+            mFilters = newFilters;
+        } else {
+            mFilters = new LogFilter[1];
+            mFilters[0] = newFilter;
+        }
+    }
+
+    private void removeFilterFromArray(LogFilter oldFilter) {
+        // look for the index
+        int index = -1;
+        for (int i = 0 ; i < mFilters.length ; i++) {
+            if (mFilters[i] == oldFilter) {
+                index = i;
+                break;
+            }
+        }
+
+        if (index != -1) {
+            LogFilter[] newFilters = new LogFilter[mFilters.length-1];
+            System.arraycopy(mFilters, 0, newFilters, 0, index);
+            System.arraycopy(mFilters, index + 1, newFilters, index,
+                    newFilters.length-index);
+            mFilters = newFilters;
+        }
+    }
+
+    /**
+     * Initialize the filter with already existing buffer.
+     * @param filter
+     */
+    private void initFilter(LogFilter filter) {
+        // is it empty
+        if (filter.uiReady() == false) {
+            return;
+        }
+
+        if (filter == mDefaultFilter) {
+            initDefaultFilter();
+            return;
+        }
+
+        filter.clear();
+
+        if (mBufferStart != -1) {
+            int max = mBufferEnd;
+            if (mBufferEnd < mBufferStart) {
+                max += STRING_BUFFER_LENGTH;
+            }
+
+            for (int i = mBufferStart; i < max; i++) {
+                int realItemIndex = i % STRING_BUFFER_LENGTH;
+
+                filter.addMessage(mBuffer[realItemIndex], null /* old message */);
+            }
+        }
+
+        filter.flush();
+        filter.resetTempFilteringStatus();
+    }
+
+    /**
+     * Refill the default filter. Not to be called directly.
+     * @see initFilter()
+     */
+    private void initDefaultFilter() {
+        mDefaultFilter.clear();
+
+        if (mBufferStart != -1) {
+            int max = mBufferEnd;
+            if (mBufferEnd < mBufferStart) {
+                max += STRING_BUFFER_LENGTH;
+            }
+
+            for (int i = mBufferStart; i < max; i++) {
+                int realItemIndex = i % STRING_BUFFER_LENGTH;
+                LogMessage msg = mBuffer[realItemIndex];
+
+                // first we check that the other filters don't take this message
+                boolean filtered = false;
+                for (LogFilter f : mFilters) {
+                    filtered |= f.accept(msg);
+                }
+
+                if (filtered == false) {
+                    mDefaultFilter.addMessage(msg, null /* old message */);
+                }
+            }
+        }
+
+        mDefaultFilter.flush();
+        mDefaultFilter.resetTempFilteringStatus();
+    }
+
+    /**
+     * Reset the filters, to handle change in device in automatic filter mode
+     */
+    private void resetFilters() {
+        // if we are in automatic mode, then we need to rmove the current
+        // filter.
+        if (mFilterMode == FILTER_AUTO_PID || mFilterMode == FILTER_AUTO_TAG) {
+            mFilters = null;
+
+            // recreate the filters.
+            createFilters();
+        }
+    }
+
+
+    private LogFilter getCurrentFilter() {
+        int index = mFolders.getSelectionIndex();
+
+        // if mFilters is null or index is invalid, we return the default
+        // filter. It doesn't matter if that one is null as well, since we
+        // would return null anyway.
+        if (index == 0 || mFilters == null) {
+            return mDefaultFilter;
+        }
+
+        return mFilters[index-1];
+    }
+
+
+    private void emptyTables() {
+        for (LogFilter f : mFilters) {
+            f.getTable().removeAll();
+        }
+
+        if (mDefaultFilter != null) {
+            mDefaultFilter.getTable().removeAll();
+        }
+    }
+
+    protected void updateFilteringWith(String text) {
+        synchronized (mBuffer) {
+            // reset the temp filtering for all the filters
+            for (LogFilter f : mFilters) {
+                f.resetTempFiltering();
+            }
+            if (mDefaultFilter != null) {
+                mDefaultFilter.resetTempFiltering();
+            }
+
+            // now we need to figure out the new temp filtering
+            // split each word
+            String[] segments = text.split(" "); //$NON-NLS-1$
+
+            ArrayList<String> keywords = new ArrayList<String>(segments.length);
+
+            // loop and look for temp id/tag
+            int tempPid = -1;
+            String tempTag = null;
+            for (int i = 0 ; i < segments.length; i++) {
+                String s = segments[i];
+                if (tempPid == -1 && s.startsWith("pid:")) { //$NON-NLS-1$
+                    // get the pid
+                    String[] seg = s.split(":"); //$NON-NLS-1$
+                    if (seg.length == 2) {
+                        if (seg[1].matches("^[0-9]*$")) { //$NON-NLS-1$
+                            tempPid = Integer.valueOf(seg[1]);
+                        }
+                    }
+                } else if (tempTag == null && s.startsWith("tag:")) { //$NON-NLS-1$
+                    String seg[] = segments[i].split(":"); //$NON-NLS-1$
+                    if (seg.length == 2) {
+                        tempTag = seg[1];
+                    }
+                } else {
+                    keywords.add(s);
+                }
+            }
+
+            // set the temp filtering in the filters
+            if (tempPid != -1 || tempTag != null || keywords.size() > 0) {
+                String[] keywordsArray = keywords.toArray(
+                        new String[keywords.size()]);
+
+                for (LogFilter f : mFilters) {
+                    if (tempPid != -1) {
+                        f.setTempPidFiltering(tempPid);
+                    }
+                    if (tempTag != null) {
+                        f.setTempTagFiltering(tempTag);
+                    }
+                    f.setTempKeywordFiltering(keywordsArray);
+                }
+
+                if (mDefaultFilter != null) {
+                    if (tempPid != -1) {
+                        mDefaultFilter.setTempPidFiltering(tempPid);
+                    }
+                    if (tempTag != null) {
+                        mDefaultFilter.setTempTagFiltering(tempTag);
+                    }
+                    mDefaultFilter.setTempKeywordFiltering(keywordsArray);
+
+                }
+            }
+
+            initFilter(mCurrentFilter);
+        }
+    }
+
+    /**
+     * Called when the current filter selection changes.
+     * @param selectedFilter
+     */
+    private void selectionChanged(LogFilter selectedFilter) {
+        if (mLogLevelActions != null) {
+            // get the log level
+            int level = selectedFilter.getLogLevel();
+            for (int i = 0 ; i < mLogLevelActions.length; i++) {
+                ICommonAction a = mLogLevelActions[i];
+                if (i == level - 2) {
+                    a.setChecked(true);
+                } else {
+                    a.setChecked(false);
+                }
+            }
+        }
+
+        if (mDeleteFilterAction != null) {
+            mDeleteFilterAction.setEnabled(selectedFilter.supportsDelete());
+        }
+        if (mEditFilterAction != null) {
+            mEditFilterAction.setEnabled(selectedFilter.supportsEdit());
+        }
+    }
+
+    public String getSelectedErrorLineMessage() {
+        Table table = mCurrentFilter.getTable();
+        int[] selection = table.getSelectionIndices();
+
+        if (selection.length == 1) {
+            TableItem item = table.getItem(selection[0]);
+            LogMessage msg = (LogMessage)item.getData();
+            if (msg.data.logLevel == LogLevel.ERROR || msg.data.logLevel == LogLevel.WARN)
+                return msg.msg;
+        }
+        return null;
+    }
+
+    public void setLogCatViewInterface(LogCatViewInterface i) {
+        mLogCatViewInterface = i;
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/net/NetworkPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/net/NetworkPanel.java
new file mode 100644
index 0000000..15b8b56
--- /dev/null
+++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/net/NetworkPanel.java
@@ -0,0 +1,1125 @@
+/*
+ * Copyright (C) 2012 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.ddmuilib.net;
+
+import com.android.ddmlib.AdbCommandRejectedException;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.MultiLineReceiver;
+import com.android.ddmlib.ShellCommandUnresponsiveException;
+import com.android.ddmlib.TimeoutException;
+import com.android.ddmuilib.DdmUiPreferences;
+import com.android.ddmuilib.TableHelper;
+import com.android.ddmuilib.TablePanel;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.dialogs.ErrorDialog;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Table;
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.axis.AxisLocation;
+import org.jfree.chart.axis.NumberAxis;
+import org.jfree.chart.axis.ValueAxis;
+import org.jfree.chart.plot.DatasetRenderingOrder;
+import org.jfree.chart.plot.ValueMarker;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.StackedXYAreaRenderer2;
+import org.jfree.chart.renderer.xy.XYAreaRenderer;
+import org.jfree.data.DefaultKeyedValues2D;
+import org.jfree.data.time.Millisecond;
+import org.jfree.data.time.TimePeriod;
+import org.jfree.data.time.TimeSeries;
+import org.jfree.data.time.TimeSeriesCollection;
+import org.jfree.data.xy.AbstractIntervalXYDataset;
+import org.jfree.data.xy.TableXYDataset;
+import org.jfree.experimental.chart.swt.ChartComposite;
+import org.jfree.ui.RectangleAnchor;
+import org.jfree.ui.TextAnchor;
+
+import java.io.IOException;
+import java.text.DecimalFormat;
+import java.text.FieldPosition;
+import java.text.NumberFormat;
+import java.text.ParsePosition;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Formatter;
+import java.util.Iterator;
+
+/**
+ * Displays live network statistics for currently selected {@link Client}.
+ */
+public class NetworkPanel extends TablePanel {
+
+    // TODO: enable view of packets and bytes/packet
+    // TODO: add sash to resize chart and table
+    // TODO: let user edit tags to be meaningful
+
+    /** Amount of historical data to display. */
+    private static final long HISTORY_MILLIS = 30 * 1000;
+
+    private final static String PREFS_NETWORK_COL_TITLE = "networkPanel.title";
+    private final static String PREFS_NETWORK_COL_RX_BYTES = "networkPanel.rxBytes";
+    private final static String PREFS_NETWORK_COL_RX_PACKETS = "networkPanel.rxPackets";
+    private final static String PREFS_NETWORK_COL_TX_BYTES = "networkPanel.txBytes";
+    private final static String PREFS_NETWORK_COL_TX_PACKETS = "networkPanel.txPackets";
+
+    /** Path to network statistics on remote device. */
+    private static final String PROC_XT_QTAGUID = "/proc/net/xt_qtaguid/stats";
+
+    private static final java.awt.Color TOTAL_COLOR = java.awt.Color.GRAY;
+
+    /** Colors used for tag series data. */
+    private static final java.awt.Color[] SERIES_COLORS = new java.awt.Color[] {
+        java.awt.Color.decode("0x2bc4c1"), // teal
+        java.awt.Color.decode("0xD50F25"), // red
+        java.awt.Color.decode("0x3369E8"), // blue
+        java.awt.Color.decode("0xEEB211"), // orange
+        java.awt.Color.decode("0x00bd2e"), // green
+        java.awt.Color.decode("0xae26ae"), // purple
+    };
+
+    private Display mDisplay;
+
+    private Composite mPanel;
+
+    /** Header panel with configuration options. */
+    private Composite mHeader;
+
+    private Label mSpeedLabel;
+    private Combo mSpeedCombo;
+
+    /** Current sleep between each sample, from {@link #mSpeedCombo}. */
+    private long mSpeedMillis;
+
+    private Button mRunningButton;
+    private Button mResetButton;
+
+    /** Chart of recent network activity. */
+    private JFreeChart mChart;
+    private ChartComposite mChartComposite;
+
+    private ValueAxis mDomainAxis;
+
+    /** Data for total traffic (tag 0x0).  */
+    private TimeSeriesCollection mTotalCollection;
+    private TimeSeries mRxTotalSeries;
+    private TimeSeries mTxTotalSeries;
+
+    /** Data for detailed tagged traffic. */
+    private LiveTimeTableXYDataset mRxDetailDataset;
+    private LiveTimeTableXYDataset mTxDetailDataset;
+
+    private XYAreaRenderer mTotalRenderer;
+    private StackedXYAreaRenderer2 mRenderer;
+
+    /** Table showing summary of network activity. */
+    private Table mTable;
+    private TableViewer mTableViewer;
+
+    /** UID of currently selected {@link Client}. */
+    private int mActiveUid = -1;
+
+    /** List of traffic flows being actively tracked. */
+    private ArrayList<TrackedItem> mTrackedItems = new ArrayList<TrackedItem>();
+
+    private SampleThread mSampleThread;
+
+    private class SampleThread extends Thread {
+        private volatile boolean mFinish;
+
+        public void finish() {
+            mFinish = true;
+            interrupt();
+        }
+
+        @Override
+        public void run() {
+            while (!mFinish && !mDisplay.isDisposed()) {
+                performSample();
+
+                try {
+                    Thread.sleep(mSpeedMillis);
+                } catch (InterruptedException e) {
+                    // ignored
+                }
+            }
+        }
+    }
+
+    /** Last snapshot taken by {@link #performSample()}. */
+    private NetworkSnapshot mLastSnapshot;
+
+    @Override
+    protected Control createControl(Composite parent) {
+        mDisplay = parent.getDisplay();
+
+        mPanel = new Composite(parent, SWT.NONE);
+
+        final FormLayout formLayout = new FormLayout();
+        mPanel.setLayout(formLayout);
+
+        createHeader();
+        createChart();
+        createTable();
+
+        return mPanel;
+    }
+
+    /**
+     * Create header panel with configuration options.
+     */
+    private void createHeader() {
+
+        mHeader = new Composite(mPanel, SWT.NONE);
+        final RowLayout layout = new RowLayout();
+        layout.center = true;
+        mHeader.setLayout(layout);
+
+        mSpeedLabel = new Label(mHeader, SWT.NONE);
+        mSpeedLabel.setText("Speed:");
+        mSpeedCombo = new Combo(mHeader, SWT.PUSH);
+        mSpeedCombo.add("Fast (100ms)");
+        mSpeedCombo.add("Medium (250ms)");
+        mSpeedCombo.add("Slow (500ms)");
+        mSpeedCombo.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                updateSpeed();
+            }
+        });
+
+        mSpeedCombo.select(1);
+        updateSpeed();
+
+        mRunningButton = new Button(mHeader, SWT.PUSH);
+        mRunningButton.setText("Start");
+        mRunningButton.setEnabled(false);
+        mRunningButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                final boolean alreadyRunning = mSampleThread != null;
+                updateRunning(!alreadyRunning);
+            }
+        });
+
+        mResetButton = new Button(mHeader, SWT.PUSH);
+        mResetButton.setText("Reset");
+        mResetButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                clearTrackedItems();
+            }
+        });
+
+        final FormData data = new FormData();
+        data.top = new FormAttachment(0);
+        data.left = new FormAttachment(0);
+        data.right = new FormAttachment(100);
+        mHeader.setLayoutData(data);
+    }
+
+    /**
+     * Create chart of recent network activity.
+     */
+    private void createChart() {
+
+        mChart = ChartFactory.createTimeSeriesChart(null, null, null, null, false, false, false);
+
+        // create backing datasets and series
+        mRxTotalSeries = new TimeSeries("RX total");
+        mTxTotalSeries = new TimeSeries("TX total");
+
+        mRxTotalSeries.setMaximumItemAge(HISTORY_MILLIS);
+        mTxTotalSeries.setMaximumItemAge(HISTORY_MILLIS);
+
+        mTotalCollection = new TimeSeriesCollection();
+        mTotalCollection.addSeries(mRxTotalSeries);
+        mTotalCollection.addSeries(mTxTotalSeries);
+
+        mRxDetailDataset = new LiveTimeTableXYDataset();
+        mTxDetailDataset = new LiveTimeTableXYDataset();
+
+        mTotalRenderer = new XYAreaRenderer(XYAreaRenderer.AREA);
+        mRenderer = new StackedXYAreaRenderer2();
+
+        final XYPlot xyPlot = mChart.getXYPlot();
+
+        xyPlot.setDatasetRenderingOrder(DatasetRenderingOrder.FORWARD);
+
+        xyPlot.setDataset(0, mTotalCollection);
+        xyPlot.setDataset(1, mRxDetailDataset);
+        xyPlot.setDataset(2, mTxDetailDataset);
+        xyPlot.setRenderer(0, mTotalRenderer);
+        xyPlot.setRenderer(1, mRenderer);
+        xyPlot.setRenderer(2, mRenderer);
+
+        // we control domain axis manually when taking samples
+        mDomainAxis = xyPlot.getDomainAxis();
+        mDomainAxis.setAutoRange(false);
+
+        final NumberAxis axis = new NumberAxis();
+        axis.setNumberFormatOverride(new BytesFormat(true));
+        axis.setAutoRangeMinimumSize(50);
+        xyPlot.setRangeAxis(axis);
+        xyPlot.setRangeAxisLocation(AxisLocation.BOTTOM_OR_RIGHT);
+
+        // draw thick line to separate RX versus TX traffic
+        xyPlot.addRangeMarker(
+                new ValueMarker(0, java.awt.Color.BLACK, new java.awt.BasicStroke(2)));
+
+        // label to indicate that positive axis is RX traffic
+        final ValueMarker rxMarker = new ValueMarker(0);
+        rxMarker.setStroke(new java.awt.BasicStroke(0));
+        rxMarker.setLabel("RX");
+        rxMarker.setLabelFont(rxMarker.getLabelFont().deriveFont(30f));
+        rxMarker.setLabelPaint(java.awt.Color.LIGHT_GRAY);
+        rxMarker.setLabelAnchor(RectangleAnchor.TOP_RIGHT);
+        rxMarker.setLabelTextAnchor(TextAnchor.BOTTOM_RIGHT);
+        xyPlot.addRangeMarker(rxMarker);
+
+        // label to indicate that negative axis is TX traffic
+        final ValueMarker txMarker = new ValueMarker(0);
+        txMarker.setStroke(new java.awt.BasicStroke(0));
+        txMarker.setLabel("TX");
+        txMarker.setLabelFont(txMarker.getLabelFont().deriveFont(30f));
+        txMarker.setLabelPaint(java.awt.Color.LIGHT_GRAY);
+        txMarker.setLabelAnchor(RectangleAnchor.BOTTOM_RIGHT);
+        txMarker.setLabelTextAnchor(TextAnchor.TOP_RIGHT);
+        xyPlot.addRangeMarker(txMarker);
+
+        mChartComposite = new ChartComposite(mPanel, SWT.BORDER, mChart,
+                ChartComposite.DEFAULT_WIDTH, ChartComposite.DEFAULT_HEIGHT,
+                ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH,
+                ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT, 4096, 4096, true, true, true, true,
+                false, true);
+
+        final FormData data = new FormData();
+        data.top = new FormAttachment(mHeader);
+        data.left = new FormAttachment(0);
+        data.bottom = new FormAttachment(70);
+        data.right = new FormAttachment(100);
+        mChartComposite.setLayoutData(data);
+    }
+
+    /**
+     * Create table showing summary of network activity.
+     */
+    private void createTable() {
+        mTable = new Table(mPanel, SWT.BORDER | SWT.MULTI | SWT.FULL_SELECTION);
+
+        final FormData data = new FormData();
+        data.top = new FormAttachment(mChartComposite);
+        data.left = new FormAttachment(mChartComposite, 0, SWT.CENTER);
+        data.bottom = new FormAttachment(100);
+        mTable.setLayoutData(data);
+
+        mTable.setHeaderVisible(true);
+        mTable.setLinesVisible(true);
+
+        final IPreferenceStore store = DdmUiPreferences.getStore();
+
+        TableHelper.createTableColumn(mTable, "", SWT.CENTER, buildSampleText(2), null, null);
+        TableHelper.createTableColumn(
+                mTable, "Tag", SWT.LEFT, buildSampleText(32), PREFS_NETWORK_COL_TITLE, store);
+        TableHelper.createTableColumn(mTable, "RX bytes", SWT.RIGHT, buildSampleText(12),
+                PREFS_NETWORK_COL_RX_BYTES, store);
+        TableHelper.createTableColumn(mTable, "RX packets", SWT.RIGHT, buildSampleText(12),
+                PREFS_NETWORK_COL_RX_PACKETS, store);
+        TableHelper.createTableColumn(mTable, "TX bytes", SWT.RIGHT, buildSampleText(12),
+                PREFS_NETWORK_COL_TX_BYTES, store);
+        TableHelper.createTableColumn(mTable, "TX packets", SWT.RIGHT, buildSampleText(12),
+                PREFS_NETWORK_COL_TX_PACKETS, store);
+
+        mTableViewer = new TableViewer(mTable);
+        mTableViewer.setContentProvider(new ContentProvider());
+        mTableViewer.setLabelProvider(new LabelProvider());
+    }
+
+    /**
+     * Update {@link #mSpeedMillis} to match {@link #mSpeedCombo} selection.
+     */
+    private void updateSpeed() {
+        switch (mSpeedCombo.getSelectionIndex()) {
+            case 0:
+                mSpeedMillis = 100;
+                break;
+            case 1:
+                mSpeedMillis = 250;
+                break;
+            case 2:
+                mSpeedMillis = 500;
+                break;
+        }
+    }
+
+    /**
+     * Update if {@link SampleThread} should be actively running. Will create
+     * new thread or finish existing thread to match requested state.
+     */
+    private void updateRunning(boolean shouldRun) {
+        final boolean alreadyRunning = mSampleThread != null;
+        if (alreadyRunning && !shouldRun) {
+            mSampleThread.finish();
+            mSampleThread = null;
+
+            mRunningButton.setText("Start");
+            mHeader.pack();
+        } else if (!alreadyRunning && shouldRun) {
+            mSampleThread = new SampleThread();
+            mSampleThread.start();
+
+            mRunningButton.setText("Stop");
+            mHeader.pack();
+        }
+    }
+
+    @Override
+    public void setFocus() {
+        mPanel.setFocus();
+    }
+
+    private static java.awt.Color nextSeriesColor(int index) {
+        return SERIES_COLORS[index % SERIES_COLORS.length];
+    }
+
+    /**
+     * Find a {@link TrackedItem} that matches the requested UID and tag, or
+     * create one if none exists.
+     */
+    public TrackedItem findOrCreateTrackedItem(int uid, int tag) {
+        // try searching for existing item
+        for (TrackedItem item : mTrackedItems) {
+            if (item.uid == uid && item.tag == tag) {
+                return item;
+            }
+        }
+
+        // nothing found; create new item
+        final TrackedItem item = new TrackedItem(uid, tag);
+        if (item.isTotal()) {
+            item.color = TOTAL_COLOR;
+            item.label = "Total";
+        } else {
+            final int size = mTrackedItems.size();
+            item.color = nextSeriesColor(size);
+            Formatter formatter = new Formatter();
+            item.label = "0x" + formatter.format("%08x", tag);
+            formatter.close();
+        }
+
+        // create color chip to display as legend in table
+        item.colorImage = new Image(mDisplay, 20, 20);
+        final GC gc = new GC(item.colorImage);
+        gc.setBackground(new org.eclipse.swt.graphics.Color(mDisplay, item.color
+                .getRed(), item.color.getGreen(), item.color.getBlue()));
+        gc.fillRectangle(item.colorImage.getBounds());
+        gc.dispose();
+
+        mTrackedItems.add(item);
+        return item;
+    }
+
+    /**
+     * Clear all {@link TrackedItem} and chart history.
+     */
+    public void clearTrackedItems() {
+        mRxTotalSeries.clear();
+        mTxTotalSeries.clear();
+
+        mRxDetailDataset.clear();
+        mTxDetailDataset.clear();
+
+        mTrackedItems.clear();
+        mTableViewer.setInput(mTrackedItems);
+    }
+
+    /**
+     * Update the {@link #mRenderer} colors to match {@link TrackedItem#color}.
+     */
+    private void updateSeriesPaint() {
+        for (TrackedItem item : mTrackedItems) {
+            final int seriesIndex = mRxDetailDataset.getColumnIndex(item.label);
+            if (seriesIndex >= 0) {
+                mRenderer.setSeriesPaint(seriesIndex, item.color);
+                mRenderer.setSeriesFillPaint(seriesIndex, item.color);
+            }
+        }
+
+        // series data is always the same color
+        final int count = mTotalCollection.getSeriesCount();
+        for (int i = 0; i < count; i++) {
+            mTotalRenderer.setSeriesPaint(i, TOTAL_COLOR);
+            mTotalRenderer.setSeriesFillPaint(i, TOTAL_COLOR);
+        }
+    }
+
+    /**
+     * Traffic flow being actively tracked, uniquely defined by UID and tag. Can
+     * record {@link NetworkSnapshot} deltas into {@link TimeSeries} for
+     * charting, and into summary statistics for {@link Table} display.
+     */
+    private class TrackedItem {
+        public final int uid;
+        public final int tag;
+
+        public java.awt.Color color;
+        public Image colorImage;
+
+        public String label;
+        public long rxBytes;
+        public long rxPackets;
+        public long txBytes;
+        public long txPackets;
+
+        public TrackedItem(int uid, int tag) {
+            this.uid = uid;
+            this.tag = tag;
+        }
+
+        public boolean isTotal() {
+            return tag == 0x0;
+        }
+
+        /**
+         * Record the given {@link NetworkSnapshot} delta, updating
+         * {@link TimeSeries} and summary statistics.
+         *
+         * @param time Timestamp when delta was observed.
+         * @param deltaMillis Time duration covered by delta, in milliseconds.
+         */
+        public void recordDelta(Millisecond time, long deltaMillis, NetworkSnapshot.Entry delta) {
+            final long rxBytesPerSecond = (delta.rxBytes * 1000) / deltaMillis;
+            final long txBytesPerSecond = (delta.txBytes * 1000) / deltaMillis;
+
+            // record values under correct series
+            if (isTotal()) {
+                mRxTotalSeries.addOrUpdate(time, rxBytesPerSecond);
+                mTxTotalSeries.addOrUpdate(time, -txBytesPerSecond);
+            } else {
+                mRxDetailDataset.addValue(rxBytesPerSecond, time, label);
+                mTxDetailDataset.addValue(-txBytesPerSecond, time, label);
+            }
+
+            rxBytes += delta.rxBytes;
+            rxPackets += delta.rxPackets;
+            txBytes += delta.txBytes;
+            txPackets += delta.txPackets;
+        }
+    }
+
+    @Override
+    public void deviceSelected() {
+        // treat as client selection to update enabled states
+        clientSelected();
+    }
+
+    @Override
+    public void clientSelected() {
+        mActiveUid = -1;
+
+        final Client client = getCurrentClient();
+        if (client != null) {
+            final int pid = client.getClientData().getPid();
+            try {
+                // map PID to UID from device
+                final UidParser uidParser = new UidParser();
+                getCurrentDevice().executeShellCommand("cat /proc/" + pid + "/status", uidParser);
+                mActiveUid = uidParser.uid;
+            } catch (TimeoutException e) {
+                e.printStackTrace();
+            } catch (AdbCommandRejectedException e) {
+                e.printStackTrace();
+            } catch (ShellCommandUnresponsiveException e) {
+                e.printStackTrace();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+
+        clearTrackedItems();
+        updateRunning(false);
+
+        final boolean validUid = mActiveUid != -1;
+        mRunningButton.setEnabled(validUid);
+    }
+
+    @Override
+    public void clientChanged(Client client, int changeMask) {
+        // ignored
+    }
+
+    /**
+     * Take a snapshot from {@link #getCurrentDevice()}, recording any delta
+     * network traffic to {@link TrackedItem}.
+     */
+    public void performSample() {
+        final IDevice device = getCurrentDevice();
+        if (device == null) return;
+
+        try {
+            final NetworkSnapshotParser parser = new NetworkSnapshotParser();
+            device.executeShellCommand("cat " + PROC_XT_QTAGUID, parser);
+
+            if (parser.isError()) {
+                mDisplay.asyncExec(new Runnable() {
+                    @Override
+                    public void run() {
+                        updateRunning(false);
+
+                        final String title = "Problem reading stats";
+                        final String message = "Problem reading xt_qtaguid network "
+                                + "statistics from selected device.";
+                        Status status = new Status(IStatus.ERROR, "NetworkPanel", 0, message, null);
+                        ErrorDialog.openError(mPanel.getShell(), title, title, status);
+                    }
+                });
+
+                return;
+            }
+
+            final NetworkSnapshot snapshot = parser.getParsedSnapshot();
+
+            // use first snapshot as baseline
+            if (mLastSnapshot == null) {
+                mLastSnapshot = snapshot;
+                return;
+            }
+
+            final NetworkSnapshot delta = NetworkSnapshot.subtract(snapshot, mLastSnapshot);
+            mLastSnapshot = snapshot;
+
+            // perform delta updates over on UI thread
+            if (!mDisplay.isDisposed()) {
+                mDisplay.syncExec(new UpdateDeltaRunnable(delta, snapshot.timestamp));
+            }
+
+        } catch (TimeoutException e) {
+            e.printStackTrace();
+        } catch (AdbCommandRejectedException e) {
+            e.printStackTrace();
+        } catch (ShellCommandUnresponsiveException e) {
+            e.printStackTrace();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * Task that updates UI with given {@link NetworkSnapshot} delta.
+     */
+    private class UpdateDeltaRunnable implements Runnable {
+        private final NetworkSnapshot mDelta;
+        private final long mEndTime;
+
+        public UpdateDeltaRunnable(NetworkSnapshot delta, long endTime) {
+            mDelta = delta;
+            mEndTime = endTime;
+        }
+
+        @Override
+        public void run() {
+            if (mDisplay.isDisposed()) return;
+
+            final Millisecond time = new Millisecond(new Date(mEndTime));
+            for (NetworkSnapshot.Entry entry : mDelta) {
+                if (mActiveUid != entry.uid) continue;
+
+                final TrackedItem item = findOrCreateTrackedItem(entry.uid, entry.tag);
+                item.recordDelta(time, mDelta.timestamp, entry);
+            }
+
+            // remove any historical detail data
+            final long beforeMillis = mEndTime - HISTORY_MILLIS;
+            mRxDetailDataset.removeBefore(beforeMillis);
+            mTxDetailDataset.removeBefore(beforeMillis);
+
+            // trigger refresh from bulk changes above
+            mRxDetailDataset.fireDatasetChanged();
+            mTxDetailDataset.fireDatasetChanged();
+
+            // update axis to show latest 30 second time period
+            mDomainAxis.setRange(mEndTime - HISTORY_MILLIS, mEndTime);
+
+            updateSeriesPaint();
+
+            // kick table viewer to update
+            mTableViewer.setInput(mTrackedItems);
+        }
+    }
+
+    /**
+     * Parser that extracts UID from remote {@code /proc/pid/status} file.
+     */
+    private static class UidParser extends MultiLineReceiver {
+        public int uid = -1;
+
+        @Override
+        public boolean isCancelled() {
+            return false;
+        }
+
+        @Override
+        public void processNewLines(String[] lines) {
+            for (String line : lines) {
+                if (line.startsWith("Uid:")) {
+                    // we care about the "real" UID
+                    final String[] cols = line.split("\t");
+                    uid = Integer.parseInt(cols[1]);
+                }
+            }
+        }
+    }
+
+    /**
+     * Parser that populates {@link NetworkSnapshot} based on contents of remote
+     * {@link NetworkPanel#PROC_XT_QTAGUID} file.
+     */
+    private static class NetworkSnapshotParser extends MultiLineReceiver {
+        private NetworkSnapshot mSnapshot;
+
+        public NetworkSnapshotParser() {
+            mSnapshot = new NetworkSnapshot(System.currentTimeMillis());
+        }
+
+        public boolean isError() {
+            return mSnapshot == null;
+        }
+
+        public NetworkSnapshot getParsedSnapshot() {
+            return mSnapshot;
+        }
+
+        @Override
+        public boolean isCancelled() {
+            return false;
+        }
+
+        @Override
+        public void processNewLines(String[] lines) {
+            for (String line : lines) {
+                if (line.endsWith("No such file or directory")) {
+                    mSnapshot = null;
+                    return;
+                }
+
+                // ignore header line
+                if (line.startsWith("idx")) {
+                    continue;
+                }
+
+                final String[] cols = line.split(" ");
+                if (cols.length < 9) continue;
+
+                // iface and set are currently ignored, which groups those
+                // entries together.
+                final NetworkSnapshot.Entry entry = new NetworkSnapshot.Entry();
+
+                entry.iface = null; //cols[1];
+                entry.uid = Integer.parseInt(cols[3]);
+                entry.set = -1; //Integer.parseInt(cols[4]);
+                entry.tag = kernelToTag(cols[2]);
+                entry.rxBytes = Long.parseLong(cols[5]);
+                entry.rxPackets = Long.parseLong(cols[6]);
+                entry.txBytes = Long.parseLong(cols[7]);
+                entry.txPackets = Long.parseLong(cols[8]);
+
+                mSnapshot.combine(entry);
+            }
+        }
+
+        /**
+         * Convert {@code /proc/} tag format to {@link Integer}. Assumes incoming
+         * format like {@code 0x7fffffff00000000}.
+         * Matches code in android.server.NetworkManagementSocketTagger
+         */
+        public static int kernelToTag(String string) {
+            int length = string.length();
+            if (length > 10) {
+                return Long.decode(string.substring(0, length - 8)).intValue();
+            } else {
+                return 0;
+            }
+        }
+    }
+
+    /**
+     * Parsed snapshot of {@link NetworkPanel#PROC_XT_QTAGUID} at specific time.
+     */
+    private static class NetworkSnapshot implements Iterable<NetworkSnapshot.Entry> {
+        private ArrayList<Entry> mStats = new ArrayList<Entry>();
+
+        public final long timestamp;
+
+        /** Single parsed statistics row. */
+        public static class Entry {
+            public String iface;
+            public int uid;
+            public int set;
+            public int tag;
+            public long rxBytes;
+            public long rxPackets;
+            public long txBytes;
+            public long txPackets;
+
+            public boolean isEmpty() {
+                return rxBytes == 0 && rxPackets == 0 && txBytes == 0 && txPackets == 0;
+            }
+        }
+
+        public NetworkSnapshot(long timestamp) {
+            this.timestamp = timestamp;
+        }
+
+        public void clear() {
+            mStats.clear();
+        }
+
+        /**
+         * Combine the given {@link Entry} with any existing {@link Entry}, or
+         * insert if none exists.
+         */
+        public void combine(Entry entry) {
+            final Entry existing = findEntry(entry.iface, entry.uid, entry.set, entry.tag);
+            if (existing != null) {
+                existing.rxBytes += entry.rxBytes;
+                existing.rxPackets += entry.rxPackets;
+                existing.txBytes += entry.txBytes;
+                existing.txPackets += entry.txPackets;
+            } else {
+                mStats.add(entry);
+            }
+        }
+
+        @Override
+        public Iterator<Entry> iterator() {
+            return mStats.iterator();
+        }
+
+        public Entry findEntry(String iface, int uid, int set, int tag) {
+            for (Entry entry : mStats) {
+                if (entry.uid == uid && entry.set == set && entry.tag == tag
+                        && equal(entry.iface, iface)) {
+                    return entry;
+                }
+            }
+            return null;
+        }
+
+        /**
+         * Subtract the two given {@link NetworkSnapshot} objects, returning the
+         * delta between them.
+         */
+        public static NetworkSnapshot subtract(NetworkSnapshot left, NetworkSnapshot right) {
+            final NetworkSnapshot result = new NetworkSnapshot(left.timestamp - right.timestamp);
+
+            // for each row on left, subtract value from right side
+            for (Entry leftEntry : left) {
+                final Entry rightEntry = right.findEntry(
+                        leftEntry.iface, leftEntry.uid, leftEntry.set, leftEntry.tag);
+                if (rightEntry == null) continue;
+
+                final Entry resultEntry = new Entry();
+                resultEntry.iface = leftEntry.iface;
+                resultEntry.uid = leftEntry.uid;
+                resultEntry.set = leftEntry.set;
+                resultEntry.tag = leftEntry.tag;
+                resultEntry.rxBytes = leftEntry.rxBytes - rightEntry.rxBytes;
+                resultEntry.rxPackets = leftEntry.rxPackets - rightEntry.rxPackets;
+                resultEntry.txBytes = leftEntry.txBytes - rightEntry.txBytes;
+                resultEntry.txPackets = leftEntry.txPackets - rightEntry.txPackets;
+
+                result.combine(resultEntry);
+            }
+
+            return result;
+        }
+    }
+
+    /**
+     * Provider of {@link #mTrackedItems}.
+     */
+    private class ContentProvider implements IStructuredContentProvider {
+        @Override
+        public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+            // pass
+        }
+
+        @Override
+        public void dispose() {
+            // pass
+        }
+
+        @Override
+        public Object[] getElements(Object inputElement) {
+            return mTrackedItems.toArray();
+        }
+    }
+
+    /**
+     * Provider of labels for {@Link TrackedItem} values.
+     */
+    private static class LabelProvider implements ITableLabelProvider {
+        private final DecimalFormat mFormat = new DecimalFormat("#,###");
+
+        @Override
+        public Image getColumnImage(Object element, int columnIndex) {
+            if (element instanceof TrackedItem) {
+                final TrackedItem item = (TrackedItem) element;
+                switch (columnIndex) {
+                    case 0:
+                        return item.colorImage;
+                }
+            }
+            return null;
+        }
+
+        @Override
+        public String getColumnText(Object element, int columnIndex) {
+            if (element instanceof TrackedItem) {
+                final TrackedItem item = (TrackedItem) element;
+                switch (columnIndex) {
+                    case 0:
+                        return null;
+                    case 1:
+                        return item.label;
+                    case 2:
+                        return mFormat.format(item.rxBytes);
+                    case 3:
+                        return mFormat.format(item.rxPackets);
+                    case 4:
+                        return mFormat.format(item.txBytes);
+                    case 5:
+                        return mFormat.format(item.txPackets);
+                }
+            }
+            return null;
+        }
+
+        @Override
+        public void addListener(ILabelProviderListener listener) {
+            // pass
+        }
+
+        @Override
+        public void dispose() {
+            // pass
+        }
+
+        @Override
+        public boolean isLabelProperty(Object element, String property) {
+            // pass
+            return false;
+        }
+
+        @Override
+        public void removeListener(ILabelProviderListener listener) {
+            // pass
+        }
+    }
+
+    /**
+     * Format that displays simplified byte units for when given values are
+     * large enough.
+     */
+    private static class BytesFormat extends NumberFormat {
+        private final String[] mUnits;
+        private final DecimalFormat mFormat = new DecimalFormat("#.#");
+
+        public BytesFormat(boolean perSecond) {
+            if (perSecond) {
+                mUnits = new String[] { "B/s", "KB/s", "MB/s" };
+            } else {
+                mUnits = new String[] { "B", "KB", "MB" };
+            }
+        }
+
+        @Override
+        public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) {
+            double value = Math.abs(number);
+
+            int i = 0;
+            while (value > 1024 && i < mUnits.length - 1) {
+                value /= 1024;
+                i++;
+            }
+
+            toAppendTo.append(mFormat.format(value));
+            toAppendTo.append(mUnits[i]);
+
+            return toAppendTo;
+        }
+
+        @Override
+        public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) {
+            return format((long) number, toAppendTo, pos);
+        }
+
+        @Override
+        public Number parse(String source, ParsePosition parsePosition) {
+            return null;
+        }
+    }
+
+    public static boolean equal(Object a, Object b) {
+        return a == b || (a != null && a.equals(b));
+    }
+
+    /**
+     * Build stub string of requested length, usually for measurement.
+     */
+    private static String buildSampleText(int length) {
+        final StringBuilder builder = new StringBuilder(length);
+        for (int i = 0; i < length; i++) {
+            builder.append("X");
+        }
+        return builder.toString();
+    }
+
+    /**
+     * Dataset that contains live measurements. Exposes
+     * {@link #removeBefore(long)} to efficiently remove old data, and enables
+     * batched {@link #fireDatasetChanged()} events.
+     */
+    public static class LiveTimeTableXYDataset extends AbstractIntervalXYDataset implements
+            TableXYDataset {
+        private DefaultKeyedValues2D mValues = new DefaultKeyedValues2D(true);
+
+        /**
+         * Caller is responsible for triggering {@link #fireDatasetChanged()}.
+         */
+        public void addValue(Number value, TimePeriod rowKey, String columnKey) {
+            mValues.addValue(value, rowKey, columnKey);
+        }
+
+        /**
+         * Caller is responsible for triggering {@link #fireDatasetChanged()}.
+         */
+        public void removeBefore(long beforeMillis) {
+            while(mValues.getRowCount() > 0) {
+                final TimePeriod period = (TimePeriod) mValues.getRowKey(0);
+                if (period.getEnd().getTime() < beforeMillis) {
+                    mValues.removeRow(0);
+                } else {
+                    break;
+                }
+            }
+        }
+
+        public int getColumnIndex(String key) {
+            return mValues.getColumnIndex(key);
+        }
+
+        public void clear() {
+            mValues.clear();
+            fireDatasetChanged();
+        }
+
+        @Override
+        public void fireDatasetChanged() {
+            super.fireDatasetChanged();
+        }
+
+        @Override
+        public int getItemCount() {
+            return mValues.getRowCount();
+        }
+
+        @Override
+        public int getItemCount(int series) {
+            return mValues.getRowCount();
+        }
+
+        @Override
+        public int getSeriesCount() {
+            return mValues.getColumnCount();
+        }
+
+        @Override
+        public Comparable getSeriesKey(int series) {
+            return mValues.getColumnKey(series);
+        }
+
+        @Override
+        public double getXValue(int series, int item) {
+            final TimePeriod period = (TimePeriod) mValues.getRowKey(item);
+            return period.getStart().getTime();
+        }
+
+        @Override
+        public double getStartXValue(int series, int item) {
+            return getXValue(series, item);
+        }
+
+        @Override
+        public double getEndXValue(int series, int item) {
+            return getXValue(series, item);
+        }
+
+        @Override
+        public Number getX(int series, int item) {
+            return getXValue(series, item);
+        }
+
+        @Override
+        public Number getStartX(int series, int item) {
+            return getXValue(series, item);
+        }
+
+        @Override
+        public Number getEndX(int series, int item) {
+            return getXValue(series, item);
+        }
+
+        @Override
+        public Number getY(int series, int item) {
+            return mValues.getValue(item, series);
+        }
+
+        @Override
+        public Number getStartY(int series, int item) {
+            return getY(series, item);
+        }
+
+        @Override
+        public Number getEndY(int series, int item) {
+            return getY(series, item);
+        }
+    }
+}
diff --git a/ddms/ddmuilib/src/main/java/images/add.png b/ddms/ddmuilib/src/main/java/images/add.png
new file mode 100644
index 0000000..eefc2ca
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/add.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/android.png b/ddms/ddmuilib/src/main/java/images/android.png
new file mode 100644
index 0000000..3779d4d
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/android.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/backward.png b/ddms/ddmuilib/src/main/java/images/backward.png
new file mode 100644
index 0000000..90a9713
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/backward.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/capture.png b/ddms/ddmuilib/src/main/java/images/capture.png
new file mode 100644
index 0000000..da5c10b
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/capture.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/clear.png b/ddms/ddmuilib/src/main/java/images/clear.png
new file mode 100644
index 0000000..0009cf6
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/clear.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/d.png b/ddms/ddmuilib/src/main/java/images/d.png
new file mode 100644
index 0000000..d45506e
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/d.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/debug-attach.png b/ddms/ddmuilib/src/main/java/images/debug-attach.png
new file mode 100644
index 0000000..9b8a11c
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/debug-attach.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/debug-error.png b/ddms/ddmuilib/src/main/java/images/debug-error.png
new file mode 100644
index 0000000..f22da1f
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/debug-error.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/debug-wait.png b/ddms/ddmuilib/src/main/java/images/debug-wait.png
new file mode 100644
index 0000000..322be63
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/debug-wait.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/delete.png b/ddms/ddmuilib/src/main/java/images/delete.png
new file mode 100644
index 0000000..db5fab8
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/delete.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/device.png b/ddms/ddmuilib/src/main/java/images/device.png
new file mode 100644
index 0000000..7dbbbb6
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/device.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/diff.png b/ddms/ddmuilib/src/main/java/images/diff.png
new file mode 100644
index 0000000..bdd9e5c
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/diff.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/displayfilters.png b/ddms/ddmuilib/src/main/java/images/displayfilters.png
new file mode 100644
index 0000000..d110c2c
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/displayfilters.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/down.png b/ddms/ddmuilib/src/main/java/images/down.png
new file mode 100644
index 0000000..f9426cb
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/down.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/e.png b/ddms/ddmuilib/src/main/java/images/e.png
new file mode 100644
index 0000000..dee7c97
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/e.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/edit.png b/ddms/ddmuilib/src/main/java/images/edit.png
new file mode 100644
index 0000000..b8f65bc
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/edit.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/empty.png b/ddms/ddmuilib/src/main/java/images/empty.png
new file mode 100644
index 0000000..f021542
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/empty.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/emulator.png b/ddms/ddmuilib/src/main/java/images/emulator.png
new file mode 100644
index 0000000..a718042
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/emulator.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/file.png b/ddms/ddmuilib/src/main/java/images/file.png
new file mode 100644
index 0000000..043a814
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/file.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/folder.png b/ddms/ddmuilib/src/main/java/images/folder.png
new file mode 100644
index 0000000..7e29b1a
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/folder.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/forward.png b/ddms/ddmuilib/src/main/java/images/forward.png
new file mode 100644
index 0000000..a97a605
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/forward.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/gc.png b/ddms/ddmuilib/src/main/java/images/gc.png
new file mode 100644
index 0000000..5194806
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/gc.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/groupby.png b/ddms/ddmuilib/src/main/java/images/groupby.png
new file mode 100644
index 0000000..250b982
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/groupby.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/halt.png b/ddms/ddmuilib/src/main/java/images/halt.png
new file mode 100644
index 0000000..10e3720
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/halt.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/heap.png b/ddms/ddmuilib/src/main/java/images/heap.png
new file mode 100644
index 0000000..e3aa3f0
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/heap.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/hprof.png b/ddms/ddmuilib/src/main/java/images/hprof.png
new file mode 100644
index 0000000..123d062
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/hprof.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/i.png b/ddms/ddmuilib/src/main/java/images/i.png
new file mode 100644
index 0000000..98385c5
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/i.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/importBug.png b/ddms/ddmuilib/src/main/java/images/importBug.png
new file mode 100644
index 0000000..f5da179
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/importBug.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/load.png b/ddms/ddmuilib/src/main/java/images/load.png
new file mode 100644
index 0000000..9e7bf6e
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/load.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/pause.png b/ddms/ddmuilib/src/main/java/images/pause.png
new file mode 100644
index 0000000..19d286d
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/pause.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/play.png b/ddms/ddmuilib/src/main/java/images/play.png
new file mode 100644
index 0000000..d54f013
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/play.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/pull.png b/ddms/ddmuilib/src/main/java/images/pull.png
new file mode 100644
index 0000000..f48f1b1
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/pull.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/push.png b/ddms/ddmuilib/src/main/java/images/push.png
new file mode 100644
index 0000000..6222864
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/push.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/save.png b/ddms/ddmuilib/src/main/java/images/save.png
new file mode 100644
index 0000000..040ebda
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/save.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/scroll_lock.png b/ddms/ddmuilib/src/main/java/images/scroll_lock.png
new file mode 100644
index 0000000..5d26689
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/scroll_lock.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/sort_down.png b/ddms/ddmuilib/src/main/java/images/sort_down.png
new file mode 100644
index 0000000..2d4ccc1
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/sort_down.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/sort_up.png b/ddms/ddmuilib/src/main/java/images/sort_up.png
new file mode 100644
index 0000000..3a0bc3c
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/sort_up.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/thread.png b/ddms/ddmuilib/src/main/java/images/thread.png
new file mode 100644
index 0000000..ac839e8
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/thread.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/tracing_start.png b/ddms/ddmuilib/src/main/java/images/tracing_start.png
new file mode 100644
index 0000000..88771cc
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/tracing_start.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/tracing_stop.png b/ddms/ddmuilib/src/main/java/images/tracing_stop.png
new file mode 100644
index 0000000..71bd215
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/tracing_stop.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/up.png b/ddms/ddmuilib/src/main/java/images/up.png
new file mode 100644
index 0000000..92edf5a
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/up.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/v.png b/ddms/ddmuilib/src/main/java/images/v.png
new file mode 100644
index 0000000..8044051
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/v.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/w.png b/ddms/ddmuilib/src/main/java/images/w.png
new file mode 100644
index 0000000..129d0f9
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/w.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/warning.png b/ddms/ddmuilib/src/main/java/images/warning.png
new file mode 100644
index 0000000..ca3b6ed
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/warning.png differ
diff --git a/ddms/ddmuilib/src/main/java/images/zygote.png b/ddms/ddmuilib/src/main/java/images/zygote.png
new file mode 100644
index 0000000..5cbb1d2
Binary files /dev/null and b/ddms/ddmuilib/src/main/java/images/zygote.png differ
diff --git a/hierarchyviewer2/MODULE_LICENSE_APACHE2 b/hierarchyviewer2/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
diff --git a/hierarchyviewer2/app/.classpath b/hierarchyviewer2/app/.classpath
new file mode 100644
index 0000000..67d8beb
--- /dev/null
+++ b/hierarchyviewer2/app/.classpath
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src/main/java"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+	<classpathentry combineaccessrules="false" exported="true" kind="src" path="/hierarchyviewer2lib"/>
+	<classpathentry combineaccessrules="false" exported="true" kind="src" path="/ddmlib"/>
+	<classpathentry combineaccessrules="false" exported="true" kind="src" path="/ddmuilib"/>
+	<classpathentry combineaccessrules="false" exported="true" kind="src" path="/sdklib"/>
+	<classpathentry combineaccessrules="false" exported="true" kind="src" path="/common"/>
+	<classpathentry combineaccessrules="false" exported="true" kind="src" path="/swtmenubar"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/hierarchyviewer2/app/.project b/hierarchyviewer2/app/.project
new file mode 100644
index 0000000..ab2e61e
--- /dev/null
+++ b/hierarchyviewer2/app/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>hierarchyviewer2</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>
diff --git a/hierarchyviewer2/app/.settings/README.txt b/hierarchyviewer2/app/.settings/README.txt
new file mode 100644
index 0000000..9120b20
--- /dev/null
+++ b/hierarchyviewer2/app/.settings/README.txt
@@ -0,0 +1,2 @@
+Copy this in eclipse project as a .settings folder at the root.
+This ensure proper compilation compliance and warning/error levels.
\ No newline at end of file
diff --git a/hierarchyviewer2/app/.settings/org.eclipse.jdt.core.prefs b/hierarchyviewer2/app/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..9dbff07
--- /dev/null
+++ b/hierarchyviewer2/app/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,98 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
+org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault
+org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled
+org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
+org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
+org.eclipse.jdt.core.compiler.problem.deadCode=warning
+org.eclipse.jdt.core.compiler.problem.deprecation=warning
+org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
+org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
+org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
+org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning
+org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled
+org.eclipse.jdt.core.compiler.problem.fieldHiding=warning
+org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
+org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=error
+org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
+org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled
+org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
+org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning
+org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning
+org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
+org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
+org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
+org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
+org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
+org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
+org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
+org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
+org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
+org.eclipse.jdt.core.compiler.problem.nullReference=error
+org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning
+org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore
+org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
+org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning
+org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
+org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
+org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
+org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled
+org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
+org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
+org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
+org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled
+org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning
+org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error
+org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning
+org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.unusedImport=warning
+org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
+org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
+org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning
+org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled
+org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
+org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
+org.eclipse.jdt.core.compiler.source=1.6
diff --git a/hierarchyviewer2/app/NOTICE b/hierarchyviewer2/app/NOTICE
new file mode 100644
index 0000000..c5b1efa
--- /dev/null
+++ b/hierarchyviewer2/app/NOTICE
@@ -0,0 +1,190 @@
+
+   Copyright (c) 2005-2008, 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.
+
+   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.
+
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
diff --git a/hierarchyviewer2/app/README b/hierarchyviewer2/app/README
new file mode 100755
index 0000000..c00ef99
--- /dev/null
+++ b/hierarchyviewer2/app/README
@@ -0,0 +1,69 @@
+Using the Eclipse project HierarchyViewer
+-----------------------------------------
+
+HierarchyViewer requires some external libraries to compile.
+If you build HierarchyViewer using the makefile, you have nothing
+to configure. However if you want to develop on HierarchyViewer
+using Eclipse, you need to perform the following configuration.
+
+
+-------
+1- Projects required in Eclipse
+-------
+
+To run HierarchyViewer from Eclipse, you need to import the following 5 projects:
+
+  - sdk/hierarchyviewer2/app
+  - sdk/hierarchyviewer2/libs/hierarchyviewerlib/
+  - sdk/ddms/libs/ddmlib
+  - sdk/ddms/libs/ddmuilib
+  - sdk/sdkmanager/libs/sdklib
+
+
+-------
+2- HierarchyViewer requires some SWT JARs to compile.
+-------
+
+SWT is available in the tree under prebuild/<platform>/swt
+
+Because the build path cannot contain relative path that are not inside
+the project directory, the .classpath file references a user library
+called ANDROID_SWT.
+
+In order to compile the project:
+- Open Preferences > Java > Build Path > User Libraries
+
+- Create a new user library named ANDROID_SWT
+- Add the following 4 JAR files:
+
+  - prebuilt/<platform>/swt/swt.jar
+  - prebuilt/common/eclipse/org.eclipse.core.commands_3.*.jar
+  - prebuilt/common/eclipse/org.eclipse.equinox.common_3.*.jar
+  - prebuilt/common/eclipse/org.eclipse.jface_3.*.jar
+
+
+-------
+3- HierarchyViewer also requires the compiled SwtMenuBar library.
+-------
+
+Build the swtmenubar library:
+$ cd $TOP (top of Android tree)
+$ . build/envsetup.sh && lunch sdk-eng
+$ sdk/eclipse/scripts/create_sdkman_symlinks.sh
+
+Define a classpath variable in Eclipse:
+- Open Preferences > Java > Build Path > Classpath Variables
+- Create a new classpath variable named ANDROID_OUT_FRAMEWORK
+- Set its folder value to <Android tree>/out/host/<platform>/framework
+- Create a new classpath variable named ANDROID_SRC
+- Set its folder value to <Android tree>
+
+You might need to clean the ddms project (Project > Clean...) after
+you add the new classpath variable, otherwise previous errors might not
+go away automatically.
+
+The ANDROID_SRC part should be optional. It allows you to have access to
+the SwtMenuBar generic parts from the Java editor.
+
+--
+EOF
diff --git a/hierarchyviewer2/app/etc/hierarchyviewer b/hierarchyviewer2/app/etc/hierarchyviewer
new file mode 100755
index 0000000..a0cc5f9
--- /dev/null
+++ b/hierarchyviewer2/app/etc/hierarchyviewer
@@ -0,0 +1,114 @@
+#!/bin/sh
+# Copyright 2008, 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.
+
+# Set up prog to be the path of this script, including following symlinks,
+# and set up progdir to be the fully-qualified pathname of its directory.
+
+prog="$0"
+while [ -h "${prog}" ]; do
+    newProg=`/bin/ls -ld "${prog}"`
+    newProg=`expr "${newProg}" : ".* -> \(.*\)$"`
+    if expr "x${newProg}" : 'x/' >/dev/null; then
+        prog="${newProg}"
+    else
+        progdir=`dirname "${prog}"`
+        prog="${progdir}/${newProg}"
+    fi
+done
+oldwd=`pwd`
+progdir=`dirname "${prog}"`
+cd "${progdir}"
+progdir=`pwd`
+prog="${progdir}"/`basename "${prog}"`
+cd "${oldwd}"
+
+jarfile=hierarchyviewer2.jar
+frameworkdir="$progdir"
+libdir="$progdir"
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+    frameworkdir=`dirname "$progdir"`/tools/lib
+    libdir=`dirname "$progdir"`/tools/lib
+fi
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+    frameworkdir=`dirname "$progdir"`/framework
+    libdir=`dirname "$progdir"`/lib
+fi
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+    echo `basename "$prog"`": can't find $jarfile"
+    exit 1
+fi
+
+
+# Check args.
+if [ debug = "$1" ]; then
+    # add this in for debugging
+    java_debug=-agentlib:jdwp=transport=dt_socket,server=y,address=8050,suspend=y
+    shift 1
+else
+    java_debug=
+fi
+
+javaCmd="java"
+
+# Mac OS X needs an additional arg, or you get an "illegal thread" complaint.
+if [ `uname` = "Darwin" ]; then
+    os_opts="-XstartOnFirstThread"
+else
+    os_opts=
+fi
+
+if [ `uname` = "Linux" ]; then
+    export GDK_NATIVE_WINDOWS=true
+fi
+
+jarpath="$frameworkdir/$jarfile:$frameworkdir/swtmenubar.jar"
+
+# Figure out the path to the swt.jar for the current architecture.
+# if ANDROID_SWT is defined, then just use this.
+# else, if running in the Android source tree, then look for the correct swt folder in prebuilt
+# else, look for the correct swt folder in the SDK under tools/lib/
+swtpath=""
+if [ -n "$ANDROID_SWT" ]; then
+    swtpath="$ANDROID_SWT"
+else
+    vmarch=`${javaCmd} -jar "${frameworkdir}"/archquery.jar`
+    if [ -n "$ANDROID_BUILD_TOP" ]; then
+        osname=`uname -s | tr A-Z a-z`
+        swtpath="${ANDROID_BUILD_TOP}/prebuilts/tools/${osname}-${vmarch}/swt"
+    else
+        swtpath="${frameworkdir}/${vmarch}"
+    fi
+fi
+
+if [ ! -d "$swtpath" ]; then
+    echo "SWT folder '${swtpath}' does not exist."
+    echo "Please export ANDROID_SWT to point to the folder containing swt.jar for your platform."
+    exit 1
+fi
+
+if [ -x $progdir/monitor ]; then
+    echo "The standalone version of hieararchyviewer is deprecated."
+    echo "Please use Android Device Monitor (tools/monitor) instead."
+fi
+# need to use "java.ext.dirs" because "-jar" causes classpath to be ignored
+# might need more memory, e.g. -Xmx128M
+exec "$javaCmd" \
+    -Xmx512M $os_opts $java_debug \
+    -Dcom.android.hierarchyviewer.bindir="$progdir" \
+    -classpath "$jarpath:$swtpath/swt.jar" \
+    com.android.hierarchyviewer.HierarchyViewerApplication "$@"
diff --git a/hierarchyviewer2/app/etc/hierarchyviewer.bat b/hierarchyviewer2/app/etc/hierarchyviewer.bat
new file mode 100755
index 0000000..432294d
--- /dev/null
+++ b/hierarchyviewer2/app/etc/hierarchyviewer.bat
@@ -0,0 +1,75 @@
+ at echo off
+rem Copyright (C) 2008 The Android Open Source Project
+rem
+rem Licensed under the Apache License, Version 2.0 (the "License");
+rem you may not use this file except in compliance with the License.
+rem You may obtain a copy of the License at
+rem
+rem      http://www.apache.org/licenses/LICENSE-2.0
+rem
+rem Unless required by applicable law or agreed to in writing, software
+rem distributed under the License is distributed on an "AS IS" BASIS,
+rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+rem See the License for the specific language governing permissions and
+rem limitations under the License.
+
+rem don't modify the caller's environment
+setlocal
+
+rem Set up prog to be the path of this script, including following symlinks,
+rem and set up progdir to be the fully-qualified pathname of its directory.
+set prog=%~f0
+
+rem Change current directory and drive to where the script is, to avoid
+rem issues with directories containing whitespaces.
+cd /d %~dp0
+
+rem Get the CWD as a full path with short names only (without spaces)
+for %%i in ("%cd%") do set prog_dir=%%~fsi
+
+rem Check we have a valid Java.exe in the path.
+set java_exe=
+call lib\find_java.bat
+if not defined java_exe goto :EOF
+
+set jarfile=hierarchyviewer2.jar
+set frameworkdir=
+set libdir=
+
+if exist %frameworkdir%%jarfile% goto JarFileOk
+    set frameworkdir=lib\
+
+if exist %frameworkdir%%jarfile% goto JarFileOk
+    set frameworkdir=..\framework\
+
+:JarFileOk
+
+if debug NEQ "%1" goto NoDebug
+    set java_debug=-agentlib:jdwp=transport=dt_socket,server=y,address=8050,suspend=y
+    shift 1
+:NoDebug
+
+set jarpath=%frameworkdir%%jarfile%;%frameworkdir%hierarchyviewerlib.jar;%frameworkdir%swtmenubar.jar
+
+if not defined ANDROID_SWT goto QueryArch
+    set swt_path=%ANDROID_SWT%
+    goto SwtDone
+
+:QueryArch
+
+    for /f %%a in ('%java_exe% -jar %frameworkdir%archquery.jar') do set swt_path=%frameworkdir%%%a
+
+:SwtDone
+
+if exist %swt_path% goto SetPath
+    echo SWT folder '%swt_path%' does not exist.
+    echo Please set ANDROID_SWT to point to the folder containing swt.jar for your platform.
+    exit /B
+
+:SetPath
+
+echo The standalone version of hieararchyviewer is deprecated.
+echo Please use Android Device Monitor (tools/monitor.bat) instead.
+call %java_exe% %java_debug% -Xmx512m -Dcom.android.hierarchyviewer.bindir=%prog_dir% -classpath "%jarpath%;%swt_path%\swt.jar" com.android.hierarchyviewer.HierarchyViewerApplication %*
+
+
diff --git a/hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/AboutDialog.java b/hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/AboutDialog.java
new file mode 100644
index 0000000..9968788
--- /dev/null
+++ b/hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/AboutDialog.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewer;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CLabel;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+public class AboutDialog extends Dialog {
+    private Image mAboutImage;
+
+    private Image mSmallImage;
+
+    public AboutDialog(Shell shell) {
+        super(shell);
+        ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class);
+        mSmallImage = imageLoader.loadImage("sdk-hierarchyviewer-16.png", Display.getDefault()); //$NON-NLS-1$
+        mAboutImage = imageLoader.loadImage("sdk-hierarchyviewer-128.png", Display.getDefault()); //$NON-NLS-1$
+    }
+
+    @Override
+    protected void createButtonsForButtonBar(Composite parent) {
+        createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true);
+    }
+
+    @Override
+    protected Control createDialogArea(Composite parent) {
+        Composite control = new Composite(parent, SWT.NONE);
+        control.setLayout(new GridLayout(2, true));
+        Composite imageControl = new Composite(control, SWT.BORDER);
+        imageControl.setLayout(new FillLayout());
+        imageControl.setLayoutData(new GridData(GridData.FILL_VERTICAL));
+        Label imageLabel = new Label(imageControl, SWT.CENTER);
+        imageLabel.setImage(mAboutImage);
+
+        CLabel textLabel = new CLabel(control, SWT.NONE);
+        // TODO: update with new year date (search this to find other occurrences to update)
+        textLabel.setText("Hierarchy Viewer\nCopyright 2012, The Android Open Source Project\nAll Rights Reserved.");
+        textLabel.setLayoutData(new GridData(GridData.BEGINNING, GridData.CENTER, true, true));
+        getShell().setText("About...");
+        getShell().setImage(mSmallImage);
+        return control;
+
+    }
+}
diff --git a/hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/HierarchyViewerApplication.java b/hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/HierarchyViewerApplication.java
new file mode 100644
index 0000000..8983f67
--- /dev/null
+++ b/hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/HierarchyViewerApplication.java
@@ -0,0 +1,942 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewer;
+
+import com.android.ddmlib.Log;
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewer.actions.AboutAction;
+import com.android.hierarchyviewer.actions.LoadAllViewsAction;
+import com.android.hierarchyviewer.actions.QuitAction;
+import com.android.hierarchyviewer.actions.ShowOverlayAction;
+import com.android.hierarchyviewer.util.ActionButton;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+import com.android.hierarchyviewerlib.actions.CapturePSDAction;
+import com.android.hierarchyviewerlib.actions.DisplayViewAction;
+import com.android.hierarchyviewerlib.actions.DumpDisplayListAction;
+import com.android.hierarchyviewerlib.actions.InspectScreenshotAction;
+import com.android.hierarchyviewerlib.actions.InvalidateAction;
+import com.android.hierarchyviewerlib.actions.LoadOverlayAction;
+import com.android.hierarchyviewerlib.actions.LoadViewHierarchyAction;
+import com.android.hierarchyviewerlib.actions.PixelPerfectAutoRefreshAction;
+import com.android.hierarchyviewerlib.actions.ProfileNodesAction;
+import com.android.hierarchyviewerlib.actions.RefreshPixelPerfectAction;
+import com.android.hierarchyviewerlib.actions.RefreshPixelPerfectTreeAction;
+import com.android.hierarchyviewerlib.actions.RefreshViewAction;
+import com.android.hierarchyviewerlib.actions.RefreshWindowsAction;
+import com.android.hierarchyviewerlib.actions.RequestLayoutAction;
+import com.android.hierarchyviewerlib.actions.SavePixelPerfectAction;
+import com.android.hierarchyviewerlib.actions.SaveTreeViewAction;
+import com.android.hierarchyviewerlib.device.IHvDevice;
+import com.android.hierarchyviewerlib.models.DeviceSelectionModel;
+import com.android.hierarchyviewerlib.models.PixelPerfectModel;
+import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener;
+import com.android.hierarchyviewerlib.models.TreeViewModel;
+import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener;
+import com.android.hierarchyviewerlib.ui.DeviceSelector;
+import com.android.hierarchyviewerlib.ui.InvokeMethodPrompt;
+import com.android.hierarchyviewerlib.ui.LayoutViewer;
+import com.android.hierarchyviewerlib.ui.PixelPerfect;
+import com.android.hierarchyviewerlib.ui.PixelPerfectControls;
+import com.android.hierarchyviewerlib.ui.PixelPerfectLoupe;
+import com.android.hierarchyviewerlib.ui.PixelPerfectPixelPanel;
+import com.android.hierarchyviewerlib.ui.PixelPerfectTree;
+import com.android.hierarchyviewerlib.ui.PropertyViewer;
+import com.android.hierarchyviewerlib.ui.TreeView;
+import com.android.hierarchyviewerlib.ui.TreeViewControls;
+import com.android.hierarchyviewerlib.ui.TreeViewOverview;
+import com.android.menubar.IMenuBarEnhancer;
+import com.android.menubar.IMenuBarEnhancer.MenuBarMode;
+import com.android.menubar.MenuBarEnhancer;
+
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.window.ApplicationWindow;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.custom.StackLayout;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.ProgressBar;
+import org.eclipse.swt.widgets.Shell;
+
+public class HierarchyViewerApplication extends ApplicationWindow {
+
+    private static final String APP_NAME = "Hierarchy Viewer";
+    private static final int INITIAL_WIDTH = 1280;
+    private static final int INITIAL_HEIGHT = 800;
+
+    private static HierarchyViewerApplication sMainWindow;
+
+    // Images for moving between the 3 main windows.
+    private Image mDeviceViewImage;
+    private Image mPixelPerfectImage;
+    private Image mTreeViewImage;
+    private Image mDeviceViewSelectedImage;
+    private Image mPixelPerfectSelectedImage;
+    private Image mTreeViewSelectedImage;
+
+    // And their buttons
+    private Button mTreeViewButton;
+    private Button mPixelPerfectButton;
+    private Button mDeviceViewButton;
+
+    private Label mProgressLabel;
+    private ProgressBar mProgressBar;
+    private String mProgressString;
+
+    private Composite mDeviceSelectorPanel;
+    private Composite mTreeViewPanel;
+    private Composite mPixelPerfectPanel;
+    private StackLayout mMainWindowStackLayout;
+    private DeviceSelector mDeviceSelector;
+    private Composite mStatusBar;
+    private TreeView mTreeView;
+    private Composite mMainWindow;
+    private Image mOnBlackImage;
+    private Image mOnWhiteImage;
+    private Button mOnBlackWhiteButton;
+    private Button mShowExtras;
+    private LayoutViewer mLayoutViewer;
+    private PixelPerfectLoupe mPixelPerfectLoupe;
+    private Composite mTreeViewControls;
+    private InvokeMethodPrompt mInvokeMethodPrompt;
+
+    private ActionButton dumpDisplayList;
+
+    private HierarchyViewerDirector mDirector;
+
+    /*
+     * If a thread bails with an uncaught exception, bring the whole
+     * thing down.
+     */
+    private static class UncaughtHandler implements Thread.UncaughtExceptionHandler {
+        @Override
+        public void uncaughtException(Thread t, Throwable e) {
+            Log.e("HierarchyViewer", "shutting down due to uncaught exception");
+            Log.e("HierarchyViewer", e);
+            System.exit(1);
+        }
+    }
+
+    public static final HierarchyViewerApplication getMainWindow() {
+        return sMainWindow;
+    }
+
+    public HierarchyViewerApplication() {
+        super(null /*shell*/);
+
+        sMainWindow = this;
+
+        addMenuBar();
+    }
+
+    @Override
+    protected void configureShell(Shell shell) {
+        super.configureShell(shell);
+        shell.setText(APP_NAME);
+        ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class);
+        Image image = imageLoader.loadImage("sdk-hierarchyviewer-128.png", Display.getDefault()); //$NON-NLS-1$
+        shell.setImage(image);
+    }
+
+    @Override
+    public MenuManager createMenuManager() {
+        return new MenuManager();
+    }
+
+    public void run() {
+        setBlockOnOpen(true);
+
+        try {
+            open();
+        } catch (SWTException e) {
+         // Ignore "widget disposed" errors after we closed.
+            if (!getShell().isDisposed()) {
+                throw e;
+            }
+        }
+
+        TreeViewModel.getModel().removeTreeChangeListener(mTreeChangeListener);
+        PixelPerfectModel.getModel().removeImageChangeListener(mImageChangeListener);
+
+        ImageLoader.dispose();
+        mDirector.stopListenForDevices();
+        mDirector.stopDebugBridge();
+        mDirector.terminate();
+    }
+
+    @Override
+    protected void initializeBounds() {
+        Rectangle monitorArea = Display.getDefault().getPrimaryMonitor().getBounds();
+        getShell().setSize(Math.min(monitorArea.width, INITIAL_WIDTH),
+                Math.min(monitorArea.height, INITIAL_HEIGHT));
+        getShell().setLocation(monitorArea.x + (monitorArea.width - INITIAL_WIDTH) / 2,
+                monitorArea.y + (monitorArea.height - INITIAL_HEIGHT) / 2);
+    }
+
+    private void loadResources() {
+        ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class);
+        mTreeViewImage = imageLoader.loadImage("tree-view.png", Display.getDefault()); //$NON-NLS-1$
+        mTreeViewSelectedImage =
+                imageLoader.loadImage("tree-view-selected.png", Display.getDefault()); //$NON-NLS-1$
+        mPixelPerfectImage = imageLoader.loadImage("pixel-perfect-view.png", Display.getDefault()); //$NON-NLS-1$
+        mPixelPerfectSelectedImage =
+                imageLoader.loadImage("pixel-perfect-view-selected.png", Display.getDefault()); //$NON-NLS-1$
+        mDeviceViewImage = imageLoader.loadImage("device-view.png", Display.getDefault()); //$NON-NLS-1$
+        mDeviceViewSelectedImage =
+                imageLoader.loadImage("device-view-selected.png", Display.getDefault()); //$NON-NLS-1$
+        mOnBlackImage = imageLoader.loadImage("on-black.png", Display.getDefault()); //$NON-NLS-1$
+        mOnWhiteImage = imageLoader.loadImage("on-white.png", Display.getDefault()); //$NON-NLS-1$
+    }
+
+    @Override
+    protected Control createContents(Composite parent) {
+        // create this only once the window is opened to please SWT on Mac
+        mDirector = HierarchyViewerApplicationDirector.createDirector();
+        mDirector.initDebugBridge();
+        mDirector.startListenForDevices();
+        mDirector.populateDeviceSelectionModel();
+
+        TreeViewModel.getModel().addTreeChangeListener(mTreeChangeListener);
+        PixelPerfectModel.getModel().addImageChangeListener(mImageChangeListener);
+
+        loadResources();
+
+        Composite control = new Composite(parent, SWT.NONE);
+        GridLayout mainLayout = new GridLayout();
+        mainLayout.marginHeight = mainLayout.marginWidth = 0;
+        mainLayout.verticalSpacing = mainLayout.horizontalSpacing = 0;
+        control.setLayout(mainLayout);
+        mMainWindow = new Composite(control, SWT.NONE);
+        mMainWindow.setLayoutData(new GridData(GridData.FILL_BOTH));
+        mMainWindowStackLayout = new StackLayout();
+        mMainWindow.setLayout(mMainWindowStackLayout);
+
+        buildDeviceSelectorPanel(mMainWindow);
+        buildTreeViewPanel(mMainWindow);
+        buildPixelPerfectPanel(mMainWindow);
+
+        buildStatusBar(control);
+
+        showDeviceSelector();
+
+        return control;
+    }
+
+
+    private void buildStatusBar(Composite parent) {
+        mStatusBar = new Composite(parent, SWT.NONE);
+        mStatusBar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        FormLayout statusBarLayout = new FormLayout();
+        statusBarLayout.marginHeight = statusBarLayout.marginWidth = 2;
+
+        mStatusBar.setLayout(statusBarLayout);
+
+        mDeviceViewButton = new Button(mStatusBar, SWT.TOGGLE);
+        mDeviceViewButton.setImage(mDeviceViewImage);
+        mDeviceViewButton.setToolTipText("Switch to the window selection view");
+        mDeviceViewButton.addSelectionListener(deviceViewButtonSelectionListener);
+        FormData deviceViewButtonFormData = new FormData();
+        deviceViewButtonFormData.left = new FormAttachment();
+        mDeviceViewButton.setLayoutData(deviceViewButtonFormData);
+
+        mTreeViewButton = new Button(mStatusBar, SWT.TOGGLE);
+        mTreeViewButton.setImage(mTreeViewImage);
+        mTreeViewButton.setEnabled(false);
+        mTreeViewButton.setToolTipText("Switch to the tree view");
+        mTreeViewButton.addSelectionListener(treeViewButtonSelectionListener);
+        FormData treeViewButtonFormData = new FormData();
+        treeViewButtonFormData.left = new FormAttachment(mDeviceViewButton, 2);
+        mTreeViewButton.setLayoutData(treeViewButtonFormData);
+
+        mPixelPerfectButton = new Button(mStatusBar, SWT.TOGGLE);
+        mPixelPerfectButton.setImage(mPixelPerfectImage);
+        mPixelPerfectButton.setEnabled(false);
+        mPixelPerfectButton.setToolTipText("Switch to the pixel perfect view");
+        mPixelPerfectButton.addSelectionListener(pixelPerfectButtonSelectionListener);
+        FormData pixelPerfectButtonFormData = new FormData();
+        pixelPerfectButtonFormData.left = new FormAttachment(mTreeViewButton, 2);
+        mPixelPerfectButton.setLayoutData(pixelPerfectButtonFormData);
+
+        // Tree View control panel...
+        mTreeViewControls = new TreeViewControls(mStatusBar);
+        FormData treeViewControlsFormData = new FormData();
+        treeViewControlsFormData.left = new FormAttachment(mPixelPerfectButton, 2);
+        treeViewControlsFormData.top = new FormAttachment(mTreeViewButton, 0, SWT.CENTER);
+        treeViewControlsFormData.width = 552;
+        mTreeViewControls.setLayoutData(treeViewControlsFormData);
+
+        // Progress stuff
+        mProgressLabel = new Label(mStatusBar, SWT.RIGHT);
+
+        mProgressBar = new ProgressBar(mStatusBar, SWT.HORIZONTAL | SWT.INDETERMINATE | SWT.SMOOTH);
+        FormData progressBarFormData = new FormData();
+        progressBarFormData.right = new FormAttachment(100, 0);
+        progressBarFormData.top = new FormAttachment(mTreeViewButton, 0, SWT.CENTER);
+        mProgressBar.setLayoutData(progressBarFormData);
+
+        FormData progressLabelFormData = new FormData();
+        progressLabelFormData.right = new FormAttachment(mProgressBar, -2);
+        progressLabelFormData.top = new FormAttachment(mTreeViewButton, 0, SWT.CENTER);
+        mProgressLabel.setLayoutData(progressLabelFormData);
+
+        if (mProgressString == null) {
+            mProgressLabel.setVisible(false);
+            mProgressBar.setVisible(false);
+        } else {
+            mProgressLabel.setText(mProgressString);
+        }
+    }
+
+    private void buildDeviceSelectorPanel(Composite parent) {
+        mDeviceSelectorPanel = new Composite(parent, SWT.NONE);
+        GridLayout gridLayout = new GridLayout();
+        gridLayout.marginWidth = gridLayout.marginHeight = 0;
+        gridLayout.horizontalSpacing = gridLayout.verticalSpacing = 0;
+        mDeviceSelectorPanel.setLayout(gridLayout);
+
+        Composite buttonPanel = new Composite(mDeviceSelectorPanel, SWT.NONE);
+        buttonPanel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        GridLayout buttonLayout = new GridLayout();
+        buttonLayout.marginWidth = buttonLayout.marginHeight = 0;
+        buttonLayout.horizontalSpacing = buttonLayout.verticalSpacing = 0;
+        buttonPanel.setLayout(buttonLayout);
+
+        Composite innerButtonPanel = new Composite(buttonPanel, SWT.NONE);
+        innerButtonPanel.setLayoutData(new GridData(GridData.FILL_VERTICAL));
+        GridLayout innerButtonPanelLayout = new GridLayout(3, true);
+        innerButtonPanelLayout.marginWidth = innerButtonPanelLayout.marginHeight = 2;
+        innerButtonPanelLayout.horizontalSpacing = innerButtonPanelLayout.verticalSpacing = 2;
+        innerButtonPanel.setLayout(innerButtonPanelLayout);
+
+        ActionButton refreshWindows =
+                new ActionButton(innerButtonPanel, RefreshWindowsAction.getAction());
+        refreshWindows.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        ActionButton loadViewHierarchyButton =
+                new ActionButton(innerButtonPanel, LoadViewHierarchyAction.getAction());
+        loadViewHierarchyButton.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        ActionButton inspectScreenshotButton =
+                new ActionButton(innerButtonPanel, InspectScreenshotAction.getAction());
+        inspectScreenshotButton.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        Composite deviceSelectorContainer = new Composite(mDeviceSelectorPanel, SWT.BORDER);
+        deviceSelectorContainer.setLayoutData(new GridData(GridData.FILL_BOTH));
+        deviceSelectorContainer.setLayout(new FillLayout());
+        mDeviceSelector = new DeviceSelector(deviceSelectorContainer, true, true);
+    }
+
+    public void buildTreeViewPanel(Composite parent) {
+        mTreeViewPanel = new Composite(parent, SWT.NONE);
+        GridLayout gridLayout = new GridLayout();
+        gridLayout.marginWidth = gridLayout.marginHeight = 0;
+        gridLayout.horizontalSpacing = gridLayout.verticalSpacing = 0;
+        mTreeViewPanel.setLayout(gridLayout);
+
+        Composite buttonPanel = new Composite(mTreeViewPanel, SWT.NONE);
+        buttonPanel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        GridLayout buttonLayout = new GridLayout();
+        buttonLayout.marginWidth = buttonLayout.marginHeight = 0;
+        buttonLayout.horizontalSpacing = buttonLayout.verticalSpacing = 0;
+        buttonPanel.setLayout(buttonLayout);
+
+        Composite innerButtonPanel = new Composite(buttonPanel, SWT.NONE);
+        innerButtonPanel.setLayoutData(new GridData(GridData.FILL_VERTICAL));
+        GridLayout innerButtonPanelLayout = new GridLayout(8, true);
+        innerButtonPanelLayout.marginWidth = innerButtonPanelLayout.marginHeight = 2;
+        innerButtonPanelLayout.horizontalSpacing = innerButtonPanelLayout.verticalSpacing = 2;
+        innerButtonPanel.setLayout(innerButtonPanelLayout);
+
+        ActionButton saveTreeView =
+                new ActionButton(innerButtonPanel, SaveTreeViewAction.getAction(getShell()));
+        saveTreeView.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        ActionButton capturePSD =
+                new ActionButton(innerButtonPanel, CapturePSDAction.getAction(getShell()));
+        capturePSD.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        ActionButton refreshViewAction =
+                new ActionButton(innerButtonPanel, RefreshViewAction.getAction());
+        refreshViewAction.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        ActionButton displayView =
+                new ActionButton(innerButtonPanel, DisplayViewAction.getAction(getShell()));
+        displayView.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        ActionButton invalidate = new ActionButton(innerButtonPanel, InvalidateAction.getAction());
+        invalidate.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        ActionButton requestLayout =
+                new ActionButton(innerButtonPanel, RequestLayoutAction.getAction());
+        requestLayout.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        dumpDisplayList =
+                new ActionButton(innerButtonPanel, DumpDisplayListAction.getAction());
+        dumpDisplayList.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        ActionButton profileNodes =
+                new ActionButton(innerButtonPanel, ProfileNodesAction.getAction());
+        profileNodes.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        SashForm mainSash = new SashForm(mTreeViewPanel, SWT.HORIZONTAL | SWT.SMOOTH);
+        mainSash.setLayoutData(new GridData(GridData.FILL_BOTH));
+        Composite treeViewContainer = new Composite(mainSash, SWT.BORDER);
+        treeViewContainer.setLayout(new FillLayout());
+        mTreeView = new TreeView(treeViewContainer);
+
+        SashForm sideSash = new SashForm(mainSash, SWT.VERTICAL | SWT.SMOOTH);
+
+        mainSash.SASH_WIDTH = 4;
+        mainSash.setWeights(new int[] {
+                7, 3
+        });
+
+        Composite treeViewOverviewContainer = new Composite(sideSash, SWT.BORDER);
+        treeViewOverviewContainer.setLayout(new FillLayout());
+        new TreeViewOverview(treeViewOverviewContainer);
+
+        Composite propertyViewerContainer = new Composite(sideSash, SWT.BORDER);
+        propertyViewerContainer.setLayout(new GridLayout());
+
+        PropertyViewer pv = new PropertyViewer(propertyViewerContainer);
+        pv.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        mInvokeMethodPrompt = new InvokeMethodPrompt(propertyViewerContainer);
+        mInvokeMethodPrompt.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        Composite layoutViewerContainer = new Composite(sideSash, SWT.NONE);
+        GridLayout layoutViewerLayout = new GridLayout();
+        layoutViewerLayout.marginWidth = layoutViewerLayout.marginHeight = 0;
+        layoutViewerLayout.horizontalSpacing = layoutViewerLayout.verticalSpacing = 1;
+        layoutViewerContainer.setLayout(layoutViewerLayout);
+
+        Composite fullButtonBar = new Composite(layoutViewerContainer, SWT.NONE);
+        fullButtonBar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        GridLayout fullButtonBarLayout = new GridLayout(2, false);
+        fullButtonBarLayout.marginWidth = fullButtonBarLayout.marginHeight = 0;
+        fullButtonBarLayout.marginRight = 2;
+        fullButtonBarLayout.horizontalSpacing = fullButtonBarLayout.verticalSpacing = 0;
+        fullButtonBar.setLayout(fullButtonBarLayout);
+
+        Composite buttonBar = new Composite(fullButtonBar, SWT.NONE);
+        buttonBar.setLayoutData(new GridData(GridData.FILL_VERTICAL));
+        RowLayout rowLayout = new RowLayout(SWT.HORIZONTAL);
+        rowLayout.marginLeft =
+                rowLayout.marginRight = rowLayout.marginTop = rowLayout.marginBottom = 0;
+        rowLayout.pack = true;
+        rowLayout.center = true;
+        buttonBar.setLayout(rowLayout);
+
+        mOnBlackWhiteButton = new Button(buttonBar, SWT.PUSH);
+        mOnBlackWhiteButton.setImage(mOnWhiteImage);
+        mOnBlackWhiteButton.addSelectionListener(onBlackWhiteSelectionListener);
+        mOnBlackWhiteButton.setToolTipText("Change layout viewer background color");
+
+        mShowExtras = new Button(buttonBar, SWT.CHECK);
+        mShowExtras.setText("Show Extras");
+        mShowExtras.addSelectionListener(showExtrasSelectionListener);
+        mShowExtras.setToolTipText("Show images");
+
+        ActionButton loadAllViewsButton =
+                new ActionButton(fullButtonBar, LoadAllViewsAction.getAction());
+        loadAllViewsButton.setLayoutData(new GridData(GridData.END, GridData.CENTER, true, true));
+        loadAllViewsButton.addSelectionListener(loadAllViewsSelectionListener);
+
+        Composite layoutViewerMainContainer = new Composite(layoutViewerContainer, SWT.BORDER);
+        layoutViewerMainContainer.setLayoutData(new GridData(GridData.FILL_BOTH));
+        layoutViewerMainContainer.setLayout(new FillLayout());
+        mLayoutViewer = new LayoutViewer(layoutViewerMainContainer);
+
+        sideSash.SASH_WIDTH = 4;
+        sideSash.setWeights(new int[] {
+                238, 332, 416
+        });
+
+    }
+
+    private void buildPixelPerfectPanel(Composite parent) {
+        mPixelPerfectPanel = new Composite(parent, SWT.NONE);
+        GridLayout gridLayout = new GridLayout();
+        gridLayout.marginWidth = gridLayout.marginHeight = 0;
+        gridLayout.horizontalSpacing = gridLayout.verticalSpacing = 0;
+        mPixelPerfectPanel.setLayout(gridLayout);
+
+        Composite buttonPanel = new Composite(mPixelPerfectPanel, SWT.NONE);
+        buttonPanel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        GridLayout buttonLayout = new GridLayout();
+        buttonLayout.marginWidth = buttonLayout.marginHeight = 0;
+        buttonLayout.horizontalSpacing = buttonLayout.verticalSpacing = 0;
+        buttonPanel.setLayout(buttonLayout);
+
+        Composite innerButtonPanel = new Composite(buttonPanel, SWT.NONE);
+        innerButtonPanel.setLayoutData(new GridData(GridData.FILL_VERTICAL));
+        GridLayout innerButtonPanelLayout = new GridLayout(6, true);
+        innerButtonPanelLayout.marginWidth = innerButtonPanelLayout.marginHeight = 2;
+        innerButtonPanelLayout.horizontalSpacing = innerButtonPanelLayout.verticalSpacing = 2;
+        innerButtonPanel.setLayout(innerButtonPanelLayout);
+
+        ActionButton saveTreeView =
+                new ActionButton(innerButtonPanel, SavePixelPerfectAction.getAction(getShell()));
+        saveTreeView.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        ActionButton refreshPixelPerfect =
+                new ActionButton(innerButtonPanel, RefreshPixelPerfectAction.getAction());
+        refreshPixelPerfect.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        ActionButton refreshPixelPerfectTree =
+                new ActionButton(innerButtonPanel, RefreshPixelPerfectTreeAction.getAction());
+        refreshPixelPerfectTree.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        ActionButton loadOverlay =
+                new ActionButton(innerButtonPanel, LoadOverlayAction.getAction(getShell()));
+        loadOverlay.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        ActionButton showInLoupe =
+                new ActionButton(innerButtonPanel, ShowOverlayAction.getAction());
+        showInLoupe.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        ActionButton autoRefresh =
+                new ActionButton(innerButtonPanel, PixelPerfectAutoRefreshAction.getAction());
+        autoRefresh.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        SashForm mainSash = new SashForm(mPixelPerfectPanel, SWT.HORIZONTAL | SWT.SMOOTH);
+        mainSash.setLayoutData(new GridData(GridData.FILL_BOTH));
+        mainSash.SASH_WIDTH = 4;
+
+        Composite pixelPerfectTreeContainer = new Composite(mainSash, SWT.BORDER);
+        pixelPerfectTreeContainer.setLayout(new FillLayout());
+        new PixelPerfectTree(pixelPerfectTreeContainer);
+
+        Composite pixelPerfectLoupeContainer = new Composite(mainSash, SWT.NONE);
+        GridLayout loupeLayout = new GridLayout();
+        loupeLayout.marginWidth = loupeLayout.marginHeight = 0;
+        loupeLayout.horizontalSpacing = loupeLayout.verticalSpacing = 0;
+        pixelPerfectLoupeContainer.setLayout(loupeLayout);
+
+        Composite pixelPerfectLoupeBorder = new Composite(pixelPerfectLoupeContainer, SWT.BORDER);
+        pixelPerfectLoupeBorder.setLayoutData(new GridData(GridData.FILL_BOTH));
+        GridLayout pixelPerfectLoupeBorderGridLayout = new GridLayout();
+        pixelPerfectLoupeBorderGridLayout.marginWidth =
+                pixelPerfectLoupeBorderGridLayout.marginHeight = 0;
+        pixelPerfectLoupeBorderGridLayout.horizontalSpacing =
+                pixelPerfectLoupeBorderGridLayout.verticalSpacing = 0;
+        pixelPerfectLoupeBorder.setLayout(pixelPerfectLoupeBorderGridLayout);
+
+        mPixelPerfectLoupe = new PixelPerfectLoupe(pixelPerfectLoupeBorder);
+        mPixelPerfectLoupe.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        PixelPerfectPixelPanel pixelPerfectPixelPanel =
+                new PixelPerfectPixelPanel(pixelPerfectLoupeBorder);
+        pixelPerfectPixelPanel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        PixelPerfectControls pixelPerfectControls =
+                new PixelPerfectControls(pixelPerfectLoupeContainer);
+        pixelPerfectControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+
+        Composite pixelPerfectContainer = new Composite(mainSash, SWT.BORDER);
+        pixelPerfectContainer.setLayout(new FillLayout());
+        new PixelPerfect(pixelPerfectContainer);
+
+        mainSash.setWeights(new int[] {
+                272, 376, 346
+        });
+
+    }
+
+    public void showOverlayInLoupe(boolean value) {
+        mPixelPerfectLoupe.setShowOverlay(value);
+    }
+
+    // Shows the progress notification...
+    public void startTask(final String taskName) {
+        mProgressString = taskName;
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                if (mProgressLabel != null && mProgressBar != null) {
+                    mProgressLabel.setText(taskName);
+                    mProgressLabel.setVisible(true);
+                    mProgressBar.setVisible(true);
+                    mStatusBar.layout();
+                }
+            }
+        });
+    }
+
+    // And hides it!
+    public void endTask() {
+        mProgressString = null;
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                if (mProgressLabel != null && mProgressBar != null) {
+                    mProgressLabel.setVisible(false);
+                    mProgressBar.setVisible(false);
+                }
+            }
+        });
+    }
+
+    public void showDeviceSelector() {
+        // Show the menus.
+        MenuManager mm = getMenuBarManager();
+        mm.removeAll();
+
+        MenuManager file = new MenuManager("&File");
+        IMenuBarEnhancer enhancer = MenuBarEnhancer.setupMenuManager(
+                APP_NAME,
+                getShell().getDisplay(),
+                file,
+                AboutAction.getAction(getShell()),
+                null /*preferencesAction*/,
+                QuitAction.getAction());
+        if (enhancer.getMenuBarMode() == MenuBarMode.GENERIC) {
+            mm.add(file);
+        }
+
+        MenuManager device = new MenuManager("&Devices");
+        mm.add(device);
+
+        device.add(RefreshWindowsAction.getAction());
+        device.add(LoadViewHierarchyAction.getAction());
+        device.add(InspectScreenshotAction.getAction());
+
+        mm.updateAll(true);
+
+        mDeviceViewButton.setSelection(true);
+        mDeviceViewButton.setImage(mDeviceViewSelectedImage);
+
+        mTreeViewButton.setSelection(false);
+        mTreeViewButton.setImage(mTreeViewImage);
+
+        mPixelPerfectButton.setSelection(false);
+        mPixelPerfectButton.setImage(mPixelPerfectImage);
+
+        mMainWindowStackLayout.topControl = mDeviceSelectorPanel;
+
+        mMainWindow.layout();
+
+        mDeviceSelector.setFocus();
+
+        mTreeViewControls.setVisible(false);
+    }
+
+    public void showTreeView() {
+        // Show the menus.
+        MenuManager mm = getMenuBarManager();
+        mm.removeAll();
+
+        MenuManager file = new MenuManager("&File");
+        IMenuBarEnhancer enhancer = MenuBarEnhancer.setupMenuManager(
+                APP_NAME,
+                getShell().getDisplay(),
+                file,
+                AboutAction.getAction(getShell()),
+                null /*preferencesAction*/,
+                QuitAction.getAction());
+        if (enhancer.getMenuBarMode() == MenuBarMode.GENERIC) {
+            mm.add(file);
+        }
+
+        MenuManager treeViewMenu = new MenuManager("&Tree View");
+        mm.add(treeViewMenu);
+
+        treeViewMenu.add(SaveTreeViewAction.getAction(getShell()));
+        treeViewMenu.add(CapturePSDAction.getAction(getShell()));
+        treeViewMenu.add(new Separator());
+        treeViewMenu.add(RefreshViewAction.getAction());
+        treeViewMenu.add(DisplayViewAction.getAction(getShell()));
+
+        IHvDevice hvDevice = DeviceSelectionModel.getModel().getSelectedDevice();
+        if (hvDevice.supportsDisplayListDump()) {
+            treeViewMenu.add(DumpDisplayListAction.getAction());
+            dumpDisplayList.setVisible(true);
+        } else {
+            dumpDisplayList.setVisible(false);
+        }
+        treeViewMenu.add(new Separator());
+        treeViewMenu.add(InvalidateAction.getAction());
+        treeViewMenu.add(RequestLayoutAction.getAction());
+
+        mm.updateAll(true);
+
+        mDeviceViewButton.setSelection(false);
+        mDeviceViewButton.setImage(mDeviceViewImage);
+
+        mTreeViewButton.setSelection(true);
+        mTreeViewButton.setImage(mTreeViewSelectedImage);
+
+        mInvokeMethodPrompt.setEnabled(hvDevice.isViewUpdateEnabled());
+
+        mPixelPerfectButton.setSelection(false);
+        mPixelPerfectButton.setImage(mPixelPerfectImage);
+
+        mMainWindowStackLayout.topControl = mTreeViewPanel;
+
+        mMainWindow.layout();
+
+        mTreeView.setFocus();
+
+        mTreeViewControls.setVisible(true);
+    }
+
+    public void showPixelPerfect() {
+        // Show the menus.
+        MenuManager mm = getMenuBarManager();
+        mm.removeAll();
+
+        MenuManager file = new MenuManager("&File");
+        IMenuBarEnhancer enhancer = MenuBarEnhancer.setupMenuManager(
+                APP_NAME,
+                getShell().getDisplay(),
+                file,
+                AboutAction.getAction(getShell()),
+                null /*preferencesAction*/,
+                QuitAction.getAction());
+        if (enhancer.getMenuBarMode() == MenuBarMode.GENERIC) {
+            mm.add(file);
+        }
+
+        MenuManager pixelPerfect = new MenuManager("&Pixel Perfect");
+        pixelPerfect.add(SavePixelPerfectAction.getAction(getShell()));
+        pixelPerfect.add(RefreshPixelPerfectAction.getAction());
+        pixelPerfect.add(RefreshPixelPerfectTreeAction.getAction());
+        pixelPerfect.add(PixelPerfectAutoRefreshAction.getAction());
+        pixelPerfect.add(new Separator());
+        pixelPerfect.add(LoadOverlayAction.getAction(getShell()));
+        pixelPerfect.add(ShowOverlayAction.getAction());
+
+        mm.add(pixelPerfect);
+
+        mm.updateAll(true);
+
+        mDeviceViewButton.setSelection(false);
+        mDeviceViewButton.setImage(mDeviceViewImage);
+
+        mTreeViewButton.setSelection(false);
+        mTreeViewButton.setImage(mTreeViewImage);
+
+        mPixelPerfectButton.setSelection(true);
+        mPixelPerfectButton.setImage(mPixelPerfectSelectedImage);
+
+        mMainWindowStackLayout.topControl = mPixelPerfectPanel;
+
+        mMainWindow.layout();
+
+        mPixelPerfectLoupe.setFocus();
+
+        mTreeViewControls.setVisible(false);
+    }
+
+    private SelectionListener deviceViewButtonSelectionListener = new SelectionListener() {
+        @Override
+        public void widgetDefaultSelected(SelectionEvent e) {
+            // pass
+        }
+
+        @Override
+        public void widgetSelected(SelectionEvent e) {
+            mDeviceViewButton.setSelection(true);
+            showDeviceSelector();
+        }
+    };
+
+    private SelectionListener treeViewButtonSelectionListener = new SelectionListener() {
+        @Override
+        public void widgetDefaultSelected(SelectionEvent e) {
+            // pass
+        }
+
+        @Override
+        public void widgetSelected(SelectionEvent e) {
+            mTreeViewButton.setSelection(true);
+            showTreeView();
+        }
+    };
+
+    private SelectionListener pixelPerfectButtonSelectionListener = new SelectionListener() {
+        @Override
+        public void widgetDefaultSelected(SelectionEvent e) {
+            // pass
+        }
+
+        @Override
+        public void widgetSelected(SelectionEvent e) {
+            mPixelPerfectButton.setSelection(true);
+            showPixelPerfect();
+        }
+    };
+
+    private SelectionListener onBlackWhiteSelectionListener = new SelectionListener() {
+        @Override
+        public void widgetDefaultSelected(SelectionEvent e) {
+            // pass
+        }
+
+        @Override
+        public void widgetSelected(SelectionEvent e) {
+            if (mLayoutViewer.getOnBlack()) {
+                mLayoutViewer.setOnBlack(false);
+                mOnBlackWhiteButton.setImage(mOnBlackImage);
+            } else {
+                mLayoutViewer.setOnBlack(true);
+                mOnBlackWhiteButton.setImage(mOnWhiteImage);
+            }
+        }
+    };
+
+    private SelectionListener showExtrasSelectionListener = new SelectionListener() {
+        @Override
+        public void widgetDefaultSelected(SelectionEvent e) {
+            // pass
+        }
+
+        @Override
+        public void widgetSelected(SelectionEvent e) {
+            mLayoutViewer.setShowExtras(mShowExtras.getSelection());
+        }
+    };
+
+    private SelectionListener loadAllViewsSelectionListener = new SelectionListener() {
+        @Override
+        public void widgetDefaultSelected(SelectionEvent e) {
+            // pass
+        }
+
+        @Override
+        public void widgetSelected(SelectionEvent e) {
+            mShowExtras.setSelection(true);
+            showExtrasSelectionListener.widgetSelected(null);
+        }
+    };
+
+    private ITreeChangeListener mTreeChangeListener = new ITreeChangeListener() {
+        @Override
+        public void selectionChanged() {
+            // pass
+        }
+
+        @Override
+        public void treeChanged() {
+            Display.getDefault().syncExec(new Runnable() {
+                @Override
+                public void run() {
+                    if (TreeViewModel.getModel().getTree() == null) {
+                        showDeviceSelector();
+                        mTreeViewButton.setEnabled(false);
+                    } else {
+                        showTreeView();
+                        mTreeViewButton.setEnabled(true);
+                    }
+                }
+            });
+        }
+
+        @Override
+        public void viewportChanged() {
+            // pass
+        }
+
+        @Override
+        public void zoomChanged() {
+            // pass
+        }
+    };
+
+    private IImageChangeListener mImageChangeListener = new IImageChangeListener() {
+
+        @Override
+        public void crosshairMoved() {
+            // pass
+        }
+
+        @Override
+        public void treeChanged() {
+            // pass
+        }
+
+        @Override
+        public void imageChanged() {
+            // pass
+        }
+
+        @Override
+        public void imageLoaded() {
+            Display.getDefault().syncExec(new Runnable() {
+                @Override
+                public void run() {
+                    if (PixelPerfectModel.getModel().getImage() == null) {
+                        mPixelPerfectButton.setEnabled(false);
+                        showDeviceSelector();
+                    } else {
+                        mPixelPerfectButton.setEnabled(true);
+                        showPixelPerfect();
+                    }
+                }
+            });
+        }
+
+        @Override
+        public void overlayChanged() {
+            // pass
+        }
+
+        @Override
+        public void overlayTransparencyChanged() {
+            // pass
+        }
+
+        @Override
+        public void selectionChanged() {
+            // pass
+        }
+
+        @Override
+        public void zoomChanged() {
+            // pass
+        }
+
+    };
+
+    public static void main(String[] args) {
+        Thread.setDefaultUncaughtExceptionHandler(new UncaughtHandler());
+
+        Display.setAppName("HierarchyViewer");
+        new HierarchyViewerApplication().run();
+    }
+}
diff --git a/hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/HierarchyViewerApplicationDirector.java b/hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/HierarchyViewerApplicationDirector.java
new file mode 100644
index 0000000..b09274b
--- /dev/null
+++ b/hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/HierarchyViewerApplicationDirector.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewer;
+
+import com.android.SdkConstants;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+
+import java.io.File;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * This is the application version of the director.
+ */
+public class HierarchyViewerApplicationDirector extends HierarchyViewerDirector {
+
+    private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
+
+    public static HierarchyViewerDirector createDirector() {
+        return sDirector = new HierarchyViewerApplicationDirector();
+    }
+
+    @Override
+    public void terminate() {
+        super.terminate();
+        mExecutor.shutdown();
+    }
+
+    /*
+     * Gets the location of adb. The script that runs the hierarchy viewer
+     * defines com.android.hierarchyviewer.bindir.
+     */
+    @Override
+    public String getAdbLocation() {
+        String hvParentLocation = System.getProperty("com.android.hierarchyviewer.bindir"); //$NON-NLS-1$
+
+        // in the new SDK, adb is in the platform-tools, but when run from the command line
+        // in the Android source tree, then adb is in $ANDROID_HOST_OUT/bin/adb
+        if (hvParentLocation != null && hvParentLocation.length() != 0) {
+            // check if there's a platform-tools folder
+            File platformTools = new File(new File(hvParentLocation).getParent(),
+                    SdkConstants.FD_PLATFORM_TOOLS);
+            if (platformTools.isDirectory()) {
+                return platformTools.getAbsolutePath() + File.separator + SdkConstants.FN_ADB;
+            }
+
+            String androidOut = System.getenv("ANDROID_HOST_OUT");
+            if (androidOut != null) {
+                return androidOut + File.separator + "bin" + File.separator + SdkConstants.FN_ADB;
+            }
+        }
+
+        return SdkConstants.FN_ADB;
+    }
+
+    /*
+     * In the application, we handle background tasks using a single thread,
+     * just to get rid of possible race conditions that can occur. We update the
+     * progress bar to show that we are doing something in the background.
+     */
+    @Override
+    public void executeInBackground(final String taskName, final Runnable task) {
+        mExecutor.execute(new Runnable() {
+            @Override
+            public void run() {
+                HierarchyViewerApplication.getMainWindow().startTask(taskName);
+                task.run();
+                HierarchyViewerApplication.getMainWindow().endTask();
+            }
+        });
+    }
+
+}
diff --git a/hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/actions/AboutAction.java b/hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/actions/AboutAction.java
new file mode 100644
index 0000000..4aff6e0
--- /dev/null
+++ b/hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/actions/AboutAction.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewer.actions;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewer.AboutDialog;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+import com.android.hierarchyviewerlib.actions.ImageAction;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+public class AboutAction extends Action implements ImageAction {
+
+    private static AboutAction sAction;
+
+    private Image mImage;
+
+    private Shell mShell;
+
+    private AboutAction(Shell shell) {
+        super("&About");
+        this.mShell = shell;
+        setAccelerator(SWT.MOD1 + 'A');
+        ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class);
+        mImage = imageLoader.loadImage("sdk-hierarchyviewer-16.png", Display.getDefault()); //$NON-NLS-1$
+        setImageDescriptor(ImageDescriptor.createFromImage(mImage));
+        setToolTipText("Shows the about dialog");
+    }
+
+    public static AboutAction getAction(Shell shell) {
+        if (sAction == null) {
+            sAction = new AboutAction(shell);
+        }
+        return sAction;
+    }
+
+    @Override
+    public void run() {
+        new AboutDialog(mShell).open();
+    }
+
+    @Override
+    public Image getImage() {
+        return mImage;
+    }
+}
diff --git a/hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/actions/LoadAllViewsAction.java b/hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/actions/LoadAllViewsAction.java
new file mode 100644
index 0000000..fd3ce9e
--- /dev/null
+++ b/hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/actions/LoadAllViewsAction.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewer.actions;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+import com.android.hierarchyviewerlib.actions.ImageAction;
+import com.android.hierarchyviewerlib.actions.TreeViewEnabledAction;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+
+public class LoadAllViewsAction extends TreeViewEnabledAction implements ImageAction {
+
+    private static LoadAllViewsAction sAction;
+
+    private Image mImage;
+
+    private LoadAllViewsAction() {
+        super("Load All &Views");
+        setAccelerator(SWT.MOD1 + 'V');
+        ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class);
+        mImage = imageLoader.loadImage("load-all-views.png", Display.getDefault()); //$NON-NLS-1$
+        setImageDescriptor(ImageDescriptor.createFromImage(mImage));
+        setToolTipText("Load all view images");
+    }
+
+    public static LoadAllViewsAction getAction() {
+        if (sAction == null) {
+            sAction = new LoadAllViewsAction();
+        }
+        return sAction;
+    }
+
+    @Override
+    public void run() {
+        HierarchyViewerDirector.getDirector().loadAllViews();
+    }
+
+    @Override
+    public Image getImage() {
+        return mImage;
+    }
+}
diff --git a/hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/actions/QuitAction.java b/hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/actions/QuitAction.java
new file mode 100644
index 0000000..b5a8c5f
--- /dev/null
+++ b/hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/actions/QuitAction.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewer.actions;
+
+import com.android.hierarchyviewer.HierarchyViewerApplication;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.swt.SWT;
+
+public class QuitAction extends Action {
+
+    private static QuitAction sAction;
+
+    private QuitAction() {
+        super("E&xit");
+        setAccelerator(SWT.MOD1 + 'Q');
+    }
+
+    public static QuitAction getAction() {
+        if (sAction == null) {
+            sAction = new QuitAction();
+        }
+        return sAction;
+    }
+
+    @Override
+    public void run() {
+        HierarchyViewerApplication.getMainWindow().close();
+    }
+}
diff --git a/hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/actions/ShowOverlayAction.java b/hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/actions/ShowOverlayAction.java
new file mode 100644
index 0000000..fb06f36
--- /dev/null
+++ b/hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/actions/ShowOverlayAction.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewer.actions;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewer.HierarchyViewerApplication;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+import com.android.hierarchyviewerlib.actions.ImageAction;
+import com.android.hierarchyviewerlib.models.PixelPerfectModel;
+import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+
+public class ShowOverlayAction extends Action implements ImageAction, IImageChangeListener {
+
+    private static ShowOverlayAction sAction;
+
+    private Image mImage;
+
+    private ShowOverlayAction() {
+        super("Show In &Loupe", Action.AS_CHECK_BOX);
+        setAccelerator(SWT.MOD1 + 'L');
+        ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class);
+        mImage = imageLoader.loadImage("show-overlay.png", Display.getDefault()); //$NON-NLS-1$
+        setImageDescriptor(ImageDescriptor.createFromImage(mImage));
+        setToolTipText("Show the overlay in the loupe view");
+        setEnabled(PixelPerfectModel.getModel().getOverlayImage() != null);
+        PixelPerfectModel.getModel().addImageChangeListener(this);
+    }
+
+    public static ShowOverlayAction getAction() {
+        if (sAction == null) {
+            sAction = new ShowOverlayAction();
+        }
+        return sAction;
+    }
+
+    @Override
+    public void run() {
+        HierarchyViewerApplication.getMainWindow().showOverlayInLoupe(sAction.isChecked());
+    }
+
+    @Override
+    public Image getImage() {
+        return mImage;
+    }
+
+    @Override
+    public void crosshairMoved() {
+        // pass
+    }
+
+    @Override
+    public void treeChanged() {
+        // pass
+    }
+
+    @Override
+    public void imageChanged() {
+        // pass
+    }
+
+    @Override
+    public void imageLoaded() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                Image overlayImage = PixelPerfectModel.getModel().getOverlayImage();
+                setEnabled(overlayImage != null);
+            }
+        });
+    }
+
+    @Override
+    public void overlayChanged() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                setEnabled(PixelPerfectModel.getModel().getOverlayImage() != null);
+            }
+        });
+    }
+
+    @Override
+    public void overlayTransparencyChanged() {
+        // pass
+    }
+
+    @Override
+    public void selectionChanged() {
+        // pass
+    }
+
+    @Override
+    public void zoomChanged() {
+        // pass
+    }
+}
diff --git a/hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/util/ActionButton.java b/hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/util/ActionButton.java
new file mode 100644
index 0000000..cd15efc
--- /dev/null
+++ b/hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/util/ActionButton.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewer.util;
+
+import com.android.hierarchyviewerlib.actions.ImageAction;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.util.IPropertyChangeListener;
+import org.eclipse.jface.util.PropertyChangeEvent;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+
+public class ActionButton implements IPropertyChangeListener, SelectionListener {
+    private Button mButton;
+
+    private Action mAction;
+
+    public ActionButton(Composite parent, ImageAction action) {
+        this.mAction = (Action) action;
+        if (this.mAction.getStyle() == Action.AS_CHECK_BOX) {
+            mButton = new Button(parent, SWT.CHECK);
+        } else {
+            mButton = new Button(parent, SWT.PUSH);
+        }
+        mButton.setText(action.getText());
+        mButton.setImage(action.getImage());
+        this.mAction.addPropertyChangeListener(this);
+        mButton.addSelectionListener(this);
+        mButton.setToolTipText(action.getToolTipText());
+        mButton.setEnabled(this.mAction.isEnabled());
+    }
+
+    @Override
+    public void propertyChange(PropertyChangeEvent e) {
+        if (e.getProperty().toUpperCase().equals("ENABLED")) { //$NON-NLS-1$
+            mButton.setEnabled((Boolean) e.getNewValue());
+        } else if (e.getProperty().toUpperCase().equals("CHECKED")) { //$NON-NLS-1$
+            mButton.setSelection(mAction.isChecked());
+        }
+    }
+
+    public void setLayoutData(Object data) {
+        mButton.setLayoutData(data);
+    }
+
+    @Override
+    public void widgetDefaultSelected(SelectionEvent e) {
+        // pass
+    }
+
+    @Override
+    public void widgetSelected(SelectionEvent e) {
+        if (mAction.getStyle() == Action.AS_CHECK_BOX) {
+            mAction.setChecked(mButton.getSelection());
+        }
+        mAction.run();
+    }
+
+    public void addSelectionListener(SelectionListener listener) {
+        mButton.addSelectionListener(listener);
+    }
+
+    public void setVisible(boolean visible) {
+        mButton.setVisible(visible);
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/.classpath b/hierarchyviewer2/hierarchyviewer2lib/.classpath
new file mode 100644
index 0000000..3cb0312
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/.classpath
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src/main/java"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+	<classpathentry combineaccessrules="false" exported="true" kind="src" path="/ddmlib"/>
+	<classpathentry combineaccessrules="false" exported="true" kind="src" path="/ddmuilib"/>
+	<classpathentry combineaccessrules="false" exported="true" kind="src" path="/common"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/hierarchyviewer2/hierarchyviewer2lib/.project b/hierarchyviewer2/hierarchyviewer2lib/.project
new file mode 100644
index 0000000..11fc283
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>hierarchyviewer2lib</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>
diff --git a/hierarchyviewer2/hierarchyviewer2lib/.settings/README.txt b/hierarchyviewer2/hierarchyviewer2lib/.settings/README.txt
new file mode 100644
index 0000000..9120b20
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/.settings/README.txt
@@ -0,0 +1,2 @@
+Copy this in eclipse project as a .settings folder at the root.
+This ensure proper compilation compliance and warning/error levels.
\ No newline at end of file
diff --git a/hierarchyviewer2/hierarchyviewer2lib/.settings/org.eclipse.jdt.core.prefs b/hierarchyviewer2/hierarchyviewer2lib/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..9dbff07
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,98 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
+org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault
+org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled
+org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
+org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
+org.eclipse.jdt.core.compiler.problem.deadCode=warning
+org.eclipse.jdt.core.compiler.problem.deprecation=warning
+org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
+org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
+org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
+org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning
+org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled
+org.eclipse.jdt.core.compiler.problem.fieldHiding=warning
+org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
+org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=error
+org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
+org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled
+org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
+org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning
+org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning
+org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
+org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
+org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
+org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
+org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
+org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
+org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
+org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
+org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
+org.eclipse.jdt.core.compiler.problem.nullReference=error
+org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning
+org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore
+org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
+org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning
+org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
+org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
+org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
+org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled
+org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
+org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
+org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
+org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled
+org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning
+org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error
+org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning
+org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.unusedImport=warning
+org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
+org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
+org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning
+org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled
+org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
+org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
+org.eclipse.jdt.core.compiler.source=1.6
diff --git a/hierarchyviewer2/hierarchyviewer2lib/NOTICE b/hierarchyviewer2/hierarchyviewer2lib/NOTICE
new file mode 100644
index 0000000..c5b1efa
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/NOTICE
@@ -0,0 +1,190 @@
+
+   Copyright (c) 2005-2008, 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.
+
+   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.
+
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/HierarchyViewerDirector.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/HierarchyViewerDirector.java
new file mode 100644
index 0000000..7c0adce
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/HierarchyViewerDirector.java
@@ -0,0 +1,731 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib;
+
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log;
+import com.android.hierarchyviewerlib.device.DeviceBridge;
+import com.android.hierarchyviewerlib.device.HvDeviceFactory;
+import com.android.hierarchyviewerlib.device.IHvDevice;
+import com.android.hierarchyviewerlib.device.WindowUpdater;
+import com.android.hierarchyviewerlib.device.WindowUpdater.IWindowChangeListener;
+import com.android.hierarchyviewerlib.models.DeviceSelectionModel;
+import com.android.hierarchyviewerlib.models.PixelPerfectModel;
+import com.android.hierarchyviewerlib.models.TreeViewModel;
+import com.android.hierarchyviewerlib.models.ViewNode;
+import com.android.hierarchyviewerlib.models.Window;
+import com.android.hierarchyviewerlib.ui.CaptureDisplay;
+import com.android.hierarchyviewerlib.ui.TreeView;
+import com.android.hierarchyviewerlib.ui.util.DrawableViewNode;
+import com.android.hierarchyviewerlib.ui.util.PsdFile;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.ImageLoader;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Shell;
+
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Timer;
+import java.util.TimerTask;
+
+/**
+ * This is the class where most of the logic resides.
+ */
+public abstract class HierarchyViewerDirector implements IDeviceChangeListener,
+        IWindowChangeListener {
+    private static final boolean sIsUsingDdmProtocol;
+    static {
+        String sHvProtoEnvVar = System.getenv("ANDROID_HVPROTO"); //$NON-NLS-1$
+        sIsUsingDdmProtocol = "ddm".equalsIgnoreCase(sHvProtoEnvVar);
+    }
+
+    protected static HierarchyViewerDirector sDirector;
+
+    public static final String TAG = "hierarchyviewer";
+
+    private int mPixelPerfectRefreshesInProgress = 0;
+
+    private Timer mPixelPerfectRefreshTimer = new Timer();
+
+    private boolean mAutoRefresh = false;
+
+    public static final int DEFAULT_PIXEL_PERFECT_AUTOREFRESH_INTERVAL = 5;
+
+    private int mPixelPerfectAutoRefreshInterval = DEFAULT_PIXEL_PERFECT_AUTOREFRESH_INTERVAL;
+
+    private PixelPerfectAutoRefreshTask mCurrentAutoRefreshTask;
+
+    private String mFilterText = ""; //$NON-NLS-1$
+
+    private static final Object mDevicesLock = new Object();
+    private Map<IDevice, IHvDevice> mDevices = new HashMap<IDevice, IHvDevice>(10);
+
+    public static boolean isUsingDdmProtocol() {
+        return sIsUsingDdmProtocol;
+    }
+
+    public void terminate() {
+        WindowUpdater.terminate();
+        mPixelPerfectRefreshTimer.cancel();
+    }
+
+    public abstract String getAdbLocation();
+
+    public static HierarchyViewerDirector getDirector() {
+        return sDirector;
+    }
+
+    /**
+     * Init the DeviceBridge with an existing {@link AndroidDebugBridge}.
+     * @param bridge the bridge object to use
+     */
+    public void acquireBridge(AndroidDebugBridge bridge) {
+        DeviceBridge.acquireBridge(bridge);
+    }
+
+    /**
+     * Creates an {@link AndroidDebugBridge} connected to adb at the given location.
+     *
+     * If a bridge is already running, this disconnects it and creates a new one.
+     *
+     * @param adbLocation the location to adb.
+     */
+    public void initDebugBridge() {
+        DeviceBridge.initDebugBridge(getAdbLocation());
+    }
+
+    public void stopDebugBridge() {
+        DeviceBridge.terminate();
+    }
+
+    public void populateDeviceSelectionModel() {
+        IDevice[] devices = DeviceBridge.getDevices();
+        for (IDevice device : devices) {
+            deviceConnected(device);
+        }
+    }
+
+    public void startListenForDevices() {
+        DeviceBridge.startListenForDevices(this);
+    }
+
+    public void stopListenForDevices() {
+        DeviceBridge.stopListenForDevices(this);
+    }
+
+    public abstract void executeInBackground(String taskName, Runnable task);
+
+    @Override
+    public void deviceConnected(final IDevice device) {
+        executeInBackground("Connecting device", new Runnable() {
+            @Override
+            public void run() {
+                if (!device.isOnline()) {
+                    return;
+                }
+
+                IHvDevice hvDevice;
+                synchronized (mDevicesLock) {
+                    hvDevice = mDevices.get(device);
+                    if (hvDevice == null) {
+                        hvDevice = HvDeviceFactory.create(device);
+                        hvDevice.initializeViewDebug();
+                        hvDevice.addWindowChangeListener(getDirector());
+                        mDevices.put(device, hvDevice);
+                    } else {
+                        // attempt re-initializing view server if device state has changed
+                        hvDevice.initializeViewDebug();
+                    }
+                }
+
+                DeviceSelectionModel.getModel().addDevice(hvDevice);
+                focusChanged(device);
+            }
+        });
+    }
+
+    @Override
+    public void deviceDisconnected(final IDevice device) {
+        executeInBackground("Disconnecting device", new Runnable() {
+            @Override
+            public void run() {
+                IHvDevice hvDevice;
+                synchronized (mDevicesLock) {
+                    hvDevice = mDevices.get(device);
+                    if (hvDevice != null) {
+                        mDevices.remove(device);
+                    }
+                }
+
+                if (hvDevice == null) {
+                    return;
+                }
+
+                hvDevice.terminateViewDebug();
+                hvDevice.removeWindowChangeListener(getDirector());
+                DeviceSelectionModel.getModel().removeDevice(hvDevice);
+                if (PixelPerfectModel.getModel().getDevice() == device) {
+                    PixelPerfectModel.getModel().setData(null, null, null);
+                }
+                Window treeViewWindow = TreeViewModel.getModel().getWindow();
+                if (treeViewWindow != null && treeViewWindow.getDevice() == device) {
+                    TreeViewModel.getModel().setData(null, null);
+                    mFilterText = ""; //$NON-NLS-1$
+                }
+            }
+        });
+    }
+
+    @Override
+    public void windowsChanged(final IDevice device) {
+        executeInBackground("Refreshing windows", new Runnable() {
+            @Override
+            public void run() {
+                IHvDevice hvDevice = getHvDevice(device);
+                hvDevice.reloadWindows();
+                DeviceSelectionModel.getModel().updateDevice(hvDevice);
+            }
+        });
+    }
+
+    @Override
+    public void focusChanged(final IDevice device) {
+        executeInBackground("Updating focus", new Runnable() {
+            @Override
+            public void run() {
+                IHvDevice hvDevice = getHvDevice(device);
+                int focusedWindow = hvDevice.getFocusedWindow();
+                DeviceSelectionModel.getModel().updateFocusedWindow(hvDevice, focusedWindow);
+            }
+        });
+    }
+
+    @Override
+    public void deviceChanged(IDevice device, int changeMask) {
+        if ((changeMask & IDevice.CHANGE_STATE) != 0 && device.isOnline()) {
+            deviceConnected(device);
+        }
+    }
+
+    public void refreshPixelPerfect() {
+        final IDevice device = PixelPerfectModel.getModel().getDevice();
+        if (device != null) {
+            // Some interesting logic here. We don't want to refresh the pixel
+            // perfect view 1000 times in a row if the focus keeps changing. We
+            // just
+            // want it to refresh following the last focus change.
+            boolean proceed = false;
+            synchronized (this) {
+                if (mPixelPerfectRefreshesInProgress <= 1) {
+                    proceed = true;
+                    mPixelPerfectRefreshesInProgress++;
+                }
+            }
+            if (proceed) {
+                executeInBackground("Refreshing pixel perfect screenshot", new Runnable() {
+                    @Override
+                    public void run() {
+                        Image screenshotImage = getScreenshotImage(getHvDevice(device));
+                        if (screenshotImage != null) {
+                            PixelPerfectModel.getModel().setImage(screenshotImage);
+                        }
+                        synchronized (HierarchyViewerDirector.this) {
+                            mPixelPerfectRefreshesInProgress--;
+                        }
+                    }
+
+                });
+            }
+        }
+    }
+
+    public void refreshPixelPerfectTree() {
+        final IDevice device = PixelPerfectModel.getModel().getDevice();
+        if (device != null) {
+            executeInBackground("Refreshing pixel perfect tree", new Runnable() {
+                @Override
+                public void run() {
+                    IHvDevice hvDevice = getHvDevice(device);
+                    ViewNode viewNode =
+                            hvDevice.loadWindowData(Window.getFocusedWindow(hvDevice));
+                    if (viewNode != null) {
+                        PixelPerfectModel.getModel().setTree(viewNode);
+                    }
+                }
+
+            });
+        }
+    }
+
+    public void loadPixelPerfectData(final IHvDevice hvDevice) {
+        executeInBackground("Loading pixel perfect data", new Runnable() {
+            @Override
+            public void run() {
+                Image screenshotImage = getScreenshotImage(hvDevice);
+                if (screenshotImage != null) {
+                    ViewNode viewNode =
+                            hvDevice.loadWindowData(Window.getFocusedWindow(hvDevice));
+                    if (viewNode != null) {
+                        PixelPerfectModel.getModel().setData(hvDevice.getDevice(),
+                                screenshotImage, viewNode);
+                    }
+                }
+            }
+        });
+    }
+
+    private IHvDevice getHvDevice(IDevice device) {
+        synchronized (mDevicesLock) {
+            return mDevices.get(device);
+        }
+    }
+
+    private Image getScreenshotImage(IHvDevice hvDevice) {
+        return (hvDevice == null) ? null : hvDevice.getScreenshotImage();
+    }
+
+    public void loadViewTreeData(final Window window) {
+        executeInBackground("Loading view hierarchy", new Runnable() {
+            @Override
+            public void run() {
+                mFilterText = ""; //$NON-NLS-1$
+
+                IHvDevice hvDevice = window.getHvDevice();
+                ViewNode viewNode = hvDevice.loadWindowData(window);
+                if (viewNode != null) {
+                    viewNode.setViewCount();
+                    TreeViewModel.getModel().setData(window, viewNode);
+                }
+            }
+        });
+    }
+
+    public void loadOverlay(final Shell shell) {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                FileDialog fileDialog = new FileDialog(shell, SWT.OPEN);
+                fileDialog.setFilterExtensions(new String[] {
+                    "*.jpg;*.jpeg;*.png;*.gif;*.bmp" //$NON-NLS-1$
+                });
+                fileDialog.setFilterNames(new String[] {
+                    "Image (*.jpg, *.jpeg, *.png, *.gif, *.bmp)"
+                });
+                fileDialog.setText("Choose an overlay image");
+                String fileName = fileDialog.open();
+                if (fileName != null) {
+                    try {
+                        Image image = new Image(Display.getDefault(), fileName);
+                        PixelPerfectModel.getModel().setOverlayImage(image);
+                    } catch (SWTException e) {
+                        Log.e(TAG, "Unable to load image from " + fileName);
+                    }
+                }
+            }
+        });
+    }
+
+    public void showCapture(final Shell shell, final ViewNode viewNode) {
+        executeInBackground("Capturing node", new Runnable() {
+            @Override
+            public void run() {
+                final Image image = loadCapture(viewNode);
+                if (image != null) {
+
+                    Display.getDefault().syncExec(new Runnable() {
+                        @Override
+                        public void run() {
+                            CaptureDisplay.show(shell, viewNode, image);
+                        }
+                    });
+                }
+            }
+        });
+    }
+
+    public Image loadCapture(ViewNode viewNode) {
+        IHvDevice hvDevice = viewNode.window.getHvDevice();
+        final Image image = hvDevice.loadCapture(viewNode.window, viewNode);
+        if (image != null) {
+            viewNode.image = image;
+
+            // Force the layout viewer to redraw.
+            TreeViewModel.getModel().notifySelectionChanged();
+        }
+        return image;
+    }
+
+    public void loadCaptureInBackground(final ViewNode viewNode) {
+        executeInBackground("Capturing node", new Runnable() {
+            @Override
+            public void run() {
+                loadCapture(viewNode);
+            }
+        });
+    }
+
+    public void showCapture(Shell shell) {
+        DrawableViewNode viewNode = TreeViewModel.getModel().getSelection();
+        if (viewNode != null) {
+            showCapture(shell, viewNode.viewNode);
+        }
+    }
+
+    public void refreshWindows() {
+        executeInBackground("Refreshing windows", new Runnable() {
+            @Override
+            public void run() {
+                IHvDevice[] hvDevicesA = DeviceSelectionModel.getModel().getDevices();
+                IDevice[] devicesA = new IDevice[hvDevicesA.length];
+                for (int i = 0; i < hvDevicesA.length; i++) {
+                    devicesA[i] = hvDevicesA[i].getDevice();
+                }
+                IDevice[] devicesB = DeviceBridge.getDevices();
+                HashSet<IDevice> deviceSet = new HashSet<IDevice>();
+                for (int i = 0; i < devicesB.length; i++) {
+                    deviceSet.add(devicesB[i]);
+                }
+                for (int i = 0; i < devicesA.length; i++) {
+                    if (deviceSet.contains(devicesA[i])) {
+                        windowsChanged(devicesA[i]);
+                        deviceSet.remove(devicesA[i]);
+                    } else {
+                        deviceDisconnected(devicesA[i]);
+                    }
+                }
+                for (IDevice device : deviceSet) {
+                    deviceConnected(device);
+                }
+            }
+        });
+    }
+
+    public void loadViewHierarchy() {
+        Window window = DeviceSelectionModel.getModel().getSelectedWindow();
+        if (window != null) {
+            loadViewTreeData(window);
+        }
+    }
+
+    public void inspectScreenshot() {
+        IHvDevice device = DeviceSelectionModel.getModel().getSelectedDevice();
+        if (device != null) {
+            loadPixelPerfectData(device);
+        }
+    }
+
+    public void saveTreeView(final Shell shell) {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                final DrawableViewNode viewNode = TreeViewModel.getModel().getTree();
+                if (viewNode != null) {
+                    FileDialog fileDialog = new FileDialog(shell, SWT.SAVE);
+                    fileDialog.setFilterExtensions(new String[] {
+                        "*.png" //$NON-NLS-1$
+                    });
+                    fileDialog.setFilterNames(new String[] {
+                        "Portable Network Graphics File (*.png)"
+                    });
+                    fileDialog.setText("Choose where to save the tree image");
+                    final String fileName = fileDialog.open();
+                    if (fileName != null) {
+                        executeInBackground("Saving tree view", new Runnable() {
+                            @Override
+                            public void run() {
+                                Image image = TreeView.paintToImage(viewNode);
+                                ImageLoader imageLoader = new ImageLoader();
+                                imageLoader.data = new ImageData[] {
+                                    image.getImageData()
+                                };
+                                String extensionedFileName = fileName;
+                                if (!extensionedFileName.toLowerCase().endsWith(".png")) { //$NON-NLS-1$
+                                    extensionedFileName += ".png"; //$NON-NLS-1$
+                                }
+                                try {
+                                    imageLoader.save(extensionedFileName, SWT.IMAGE_PNG);
+                                } catch (SWTException e) {
+                                    Log.e(TAG, "Unable to save tree view as a PNG image at "
+                                            + fileName);
+                                }
+                                image.dispose();
+                            }
+                        });
+                    }
+                }
+            }
+        });
+    }
+
+    public void savePixelPerfect(final Shell shell) {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                Image untouchableImage = PixelPerfectModel.getModel().getImage();
+                if (untouchableImage != null) {
+                    final ImageData imageData = untouchableImage.getImageData();
+                    FileDialog fileDialog = new FileDialog(shell, SWT.SAVE);
+                    fileDialog.setFilterExtensions(new String[] {
+                        "*.png" //$NON-NLS-1$
+                    });
+                    fileDialog.setFilterNames(new String[] {
+                        "Portable Network Graphics File (*.png)"
+                    });
+                    fileDialog.setText("Choose where to save the screenshot");
+                    final String fileName = fileDialog.open();
+                    if (fileName != null) {
+                        executeInBackground("Saving pixel perfect", new Runnable() {
+                            @Override
+                            public void run() {
+                                ImageLoader imageLoader = new ImageLoader();
+                                imageLoader.data = new ImageData[] {
+                                    imageData
+                                };
+                                String extensionedFileName = fileName;
+                                if (!extensionedFileName.toLowerCase().endsWith(".png")) { //$NON-NLS-1$
+                                    extensionedFileName += ".png"; //$NON-NLS-1$
+                                }
+                                try {
+                                    imageLoader.save(extensionedFileName, SWT.IMAGE_PNG);
+                                } catch (SWTException e) {
+                                    Log.e(TAG, "Unable to save tree view as a PNG image at "
+                                            + fileName);
+                                }
+                            }
+                        });
+                    }
+                }
+            }
+        });
+    }
+
+    public void capturePSD(final Shell shell) {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                final Window window = TreeViewModel.getModel().getWindow();
+                if (window != null) {
+                    FileDialog fileDialog = new FileDialog(shell, SWT.SAVE);
+                    fileDialog.setFilterExtensions(new String[] {
+                        "*.psd" //$NON-NLS-1$
+                    });
+                    fileDialog.setFilterNames(new String[] {
+                        "Photoshop Document (*.psd)"
+                    });
+                    fileDialog.setText("Choose where to save the window layers");
+                    final String fileName = fileDialog.open();
+                    if (fileName != null) {
+                        executeInBackground("Saving window layers", new Runnable() {
+                            @Override
+                            public void run() {
+                                IHvDevice hvDevice = getHvDevice(window.getDevice());
+                                PsdFile psdFile = hvDevice.captureLayers(window);
+                                if (psdFile != null) {
+                                    String extensionedFileName = fileName;
+                                    if (!extensionedFileName.toLowerCase().endsWith(".psd")) { //$NON-NLS-1$
+                                        extensionedFileName += ".psd"; //$NON-NLS-1$
+                                    }
+                                    try {
+                                        psdFile.write(new FileOutputStream(extensionedFileName));
+                                    } catch (FileNotFoundException e) {
+                                        Log.e(TAG, "Unable to write to file " + fileName);
+                                    }
+                                }
+                            }
+                        });
+                    }
+                }
+            }
+        });
+    }
+
+    public void reloadViewHierarchy() {
+        Window window = TreeViewModel.getModel().getWindow();
+        if (window != null) {
+            loadViewTreeData(window);
+        }
+    }
+
+    public void invalidateCurrentNode() {
+        final DrawableViewNode selectedNode = TreeViewModel.getModel().getSelection();
+        if (selectedNode != null) {
+            executeInBackground("Invalidating view", new Runnable() {
+                @Override
+                public void run() {
+                    IHvDevice hvDevice = getHvDevice(selectedNode.viewNode.window.getDevice());
+                    hvDevice.invalidateView(selectedNode.viewNode);
+                }
+            });
+        }
+    }
+
+    public void relayoutCurrentNode() {
+        final DrawableViewNode selectedNode = TreeViewModel.getModel().getSelection();
+        if (selectedNode != null) {
+            executeInBackground("Request layout", new Runnable() {
+                @Override
+                public void run() {
+                    IHvDevice hvDevice = getHvDevice(selectedNode.viewNode.window.getDevice());
+                    hvDevice.requestLayout(selectedNode.viewNode);
+                }
+            });
+        }
+    }
+
+    public void dumpDisplayListForCurrentNode() {
+        final DrawableViewNode selectedNode = TreeViewModel.getModel().getSelection();
+        if (selectedNode != null) {
+            executeInBackground("Dump displaylist", new Runnable() {
+                @Override
+                public void run() {
+                    IHvDevice hvDevice = getHvDevice(selectedNode.viewNode.window.getDevice());
+                    hvDevice.outputDisplayList(selectedNode.viewNode);
+                }
+            });
+        }
+    }
+
+    public void profileCurrentNode() {
+        final DrawableViewNode selectedNode = TreeViewModel.getModel().getSelection();
+        if (selectedNode != null) {
+            executeInBackground("Profile Node", new Runnable() {
+                @Override
+                public void run() {
+                    IHvDevice hvDevice = getHvDevice(selectedNode.viewNode.window.getDevice());
+                    hvDevice.loadProfileData(selectedNode.viewNode.window, selectedNode.viewNode);
+                    // Force the layout viewer to redraw.
+                    TreeViewModel.getModel().notifySelectionChanged();
+                }
+            });
+        }
+    }
+
+    public void invokeMethodOnSelectedView(final String method, final List<Object> args) {
+        final DrawableViewNode selectedNode = TreeViewModel.getModel().getSelection();
+        if (selectedNode != null) {
+            executeInBackground("Invoke View Method", new Runnable() {
+                @Override
+                public void run() {
+                    IHvDevice hvDevice = getHvDevice(selectedNode.viewNode.window.getDevice());
+                    hvDevice.invokeViewMethod(selectedNode.viewNode.window, selectedNode.viewNode,
+                            method, args);
+                }
+            });
+        }
+    }
+
+    public void loadAllViews() {
+        executeInBackground("Loading all views", new Runnable() {
+            @Override
+            public void run() {
+                DrawableViewNode tree = TreeViewModel.getModel().getTree();
+                if (tree != null) {
+                    loadViewRecursive(tree.viewNode);
+                    // Force the layout viewer to redraw.
+                    TreeViewModel.getModel().notifySelectionChanged();
+                }
+            }
+        });
+    }
+
+    private void loadViewRecursive(ViewNode viewNode) {
+        IHvDevice hvDevice = getHvDevice(viewNode.window.getDevice());
+        Image image = hvDevice.loadCapture(viewNode.window, viewNode);
+        if (image == null) {
+            return;
+        }
+        viewNode.image = image;
+        final int N = viewNode.children.size();
+        for (int i = 0; i < N; i++) {
+            loadViewRecursive(viewNode.children.get(i));
+        }
+    }
+
+    public void filterNodes(String filterText) {
+        this.mFilterText = filterText;
+        DrawableViewNode tree = TreeViewModel.getModel().getTree();
+        if (tree != null) {
+            tree.viewNode.filter(filterText);
+            // Force redraw
+            TreeViewModel.getModel().notifySelectionChanged();
+        }
+    }
+
+    public String getFilterText() {
+        return mFilterText;
+    }
+
+    private static class PixelPerfectAutoRefreshTask extends TimerTask {
+        @Override
+        public void run() {
+            HierarchyViewerDirector.getDirector().refreshPixelPerfect();
+        }
+    };
+
+    public void setPixelPerfectAutoRefresh(boolean value) {
+        synchronized (mPixelPerfectRefreshTimer) {
+            if (value == mAutoRefresh) {
+                return;
+            }
+            mAutoRefresh = value;
+            if (mAutoRefresh) {
+                mCurrentAutoRefreshTask = new PixelPerfectAutoRefreshTask();
+                mPixelPerfectRefreshTimer.schedule(mCurrentAutoRefreshTask,
+                        mPixelPerfectAutoRefreshInterval * 1000,
+                        mPixelPerfectAutoRefreshInterval * 1000);
+            } else {
+                mCurrentAutoRefreshTask.cancel();
+                mCurrentAutoRefreshTask = null;
+            }
+        }
+    }
+
+    public void setPixelPerfectAutoRefreshInterval(int value) {
+        synchronized (mPixelPerfectRefreshTimer) {
+            if (mPixelPerfectAutoRefreshInterval == value) {
+                return;
+            }
+            mPixelPerfectAutoRefreshInterval = value;
+            if (mAutoRefresh) {
+                mCurrentAutoRefreshTask.cancel();
+                long timeLeft =
+                        Math.max(0, mPixelPerfectAutoRefreshInterval
+                                * 1000
+                                - (System.currentTimeMillis() - mCurrentAutoRefreshTask
+                                        .scheduledExecutionTime()));
+                mCurrentAutoRefreshTask = new PixelPerfectAutoRefreshTask();
+                mPixelPerfectRefreshTimer.schedule(mCurrentAutoRefreshTask, timeLeft,
+                        mPixelPerfectAutoRefreshInterval * 1000);
+            }
+        }
+    }
+
+    public int getPixelPerfectAutoRefreshInverval() {
+        return mPixelPerfectAutoRefreshInterval;
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/CapturePSDAction.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/CapturePSDAction.java
new file mode 100644
index 0000000..f1f7ad6
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/CapturePSDAction.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.actions;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+public class CapturePSDAction extends TreeViewEnabledAction implements ImageAction {
+
+    private static CapturePSDAction sAction;
+
+    private Image mImage;
+
+    private Shell mShell;
+
+    private CapturePSDAction(Shell shell) {
+        super("&Capture Layers");
+        this.mShell = shell;
+        setAccelerator(SWT.MOD1 + 'C');
+        ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class);
+        mImage = imageLoader.loadImage("capture-psd.png", Display.getDefault()); //$NON-NLS-1$
+        setImageDescriptor(ImageDescriptor.createFromImage(mImage));
+        setToolTipText("Capture the window layers as a photoshop document");
+    }
+
+    public static CapturePSDAction getAction(Shell shell) {
+        if (sAction == null) {
+            sAction = new CapturePSDAction(shell);
+        }
+        return sAction;
+    }
+
+    @Override
+    public void run() {
+        HierarchyViewerDirector.getDirector().capturePSD(mShell);
+    }
+
+    @Override
+    public Image getImage() {
+        return mImage;
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DisplayViewAction.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DisplayViewAction.java
new file mode 100644
index 0000000..7da02d7
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DisplayViewAction.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.actions;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+public class DisplayViewAction extends SelectedNodeEnabledAction implements ImageAction {
+
+    private static DisplayViewAction sAction;
+
+    private Image mImage;
+
+    private Shell mShell;
+
+    private DisplayViewAction(Shell shell) {
+        super("&Display View");
+        this.mShell = shell;
+        setAccelerator(SWT.MOD1 + 'D');
+        ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class);
+        mImage = imageLoader.loadImage("display.png", Display.getDefault()); //$NON-NLS-1$
+        setImageDescriptor(ImageDescriptor.createFromImage(mImage));
+        setToolTipText("Display the selected view image in a separate window");
+    }
+
+    public static DisplayViewAction getAction(Shell shell) {
+        if (sAction == null) {
+            sAction = new DisplayViewAction(shell);
+        }
+        return sAction;
+    }
+
+    @Override
+    public void run() {
+        HierarchyViewerDirector.getDirector().showCapture(mShell);
+    }
+
+    @Override
+    public Image getImage() {
+        return mImage;
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DumpDisplayListAction.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DumpDisplayListAction.java
new file mode 100644
index 0000000..fdbc7ef
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DumpDisplayListAction.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.hierarchyviewerlib.actions;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+
+public class DumpDisplayListAction extends SelectedNodeEnabledAction implements ImageAction {
+
+    private static DumpDisplayListAction sAction;
+
+    private Image mImage;
+
+    private DumpDisplayListAction() {
+        super("Dump DisplayList");
+        ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class);
+        mImage = imageLoader.loadImage("load-view-hierarchy.png", Display.getDefault()); //$NON-NLS-1$
+        setImageDescriptor(ImageDescriptor.createFromImage(mImage));
+        setToolTipText("Request the view to output its displaylist to logcat");
+    }
+
+    public static DumpDisplayListAction getAction() {
+        if (sAction == null) {
+            sAction = new DumpDisplayListAction();
+        }
+        return sAction;
+    }
+
+    @Override
+    public void run() {
+        HierarchyViewerDirector.getDirector().dumpDisplayListForCurrentNode();
+    }
+
+    @Override
+    public Image getImage() {
+        return mImage;
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ImageAction.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ImageAction.java
new file mode 100644
index 0000000..08320fd
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ImageAction.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.actions;
+
+import org.eclipse.swt.graphics.Image;
+
+public interface ImageAction {
+    public Image getImage();
+
+    public String getText();
+
+    public String getToolTipText();
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InspectScreenshotAction.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InspectScreenshotAction.java
new file mode 100644
index 0000000..388c057
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InspectScreenshotAction.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.actions;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+import com.android.hierarchyviewerlib.device.IHvDevice;
+import com.android.hierarchyviewerlib.models.DeviceSelectionModel;
+import com.android.hierarchyviewerlib.models.DeviceSelectionModel.IWindowChangeListener;
+import com.android.hierarchyviewerlib.models.Window;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+
+public class InspectScreenshotAction extends Action implements ImageAction, IWindowChangeListener {
+
+    private static InspectScreenshotAction sAction;
+
+    private Image mImage;
+
+    private InspectScreenshotAction() {
+        super("Inspect &Screenshot");
+        setAccelerator(SWT.MOD1 + 'S');
+        ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class);
+        mImage = imageLoader.loadImage("inspect-screenshot.png", Display.getDefault()); //$NON-NLS-1$
+        setImageDescriptor(ImageDescriptor.createFromImage(mImage));
+        setToolTipText("Inspect a screenshot in the pixel perfect view");
+        setEnabled(
+                DeviceSelectionModel.getModel().getSelectedDevice() != null);
+        DeviceSelectionModel.getModel().addWindowChangeListener(this);
+    }
+
+    public static InspectScreenshotAction getAction() {
+        if (sAction == null) {
+            sAction = new InspectScreenshotAction();
+        }
+        return sAction;
+    }
+
+    @Override
+    public void run() {
+        HierarchyViewerDirector.getDirector().inspectScreenshot();
+    }
+
+    @Override
+    public Image getImage() {
+        return mImage;
+    }
+
+    @Override
+    public void deviceChanged(IHvDevice device) {
+        // pass
+    }
+
+    @Override
+    public void deviceConnected(IHvDevice device) {
+        // pass
+    }
+
+    @Override
+    public void deviceDisconnected(IHvDevice device) {
+        // pass
+    }
+
+    @Override
+    public void focusChanged(IHvDevice device) {
+        // pass
+    }
+
+    @Override
+    public void selectionChanged(final IHvDevice device, final Window window) {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                InspectScreenshotAction.getAction().setEnabled(device != null);
+            }
+        });
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InvalidateAction.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InvalidateAction.java
new file mode 100644
index 0000000..b884220
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InvalidateAction.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.actions;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+
+public class InvalidateAction extends SelectedNodeEnabledAction implements ImageAction {
+
+    private static InvalidateAction sAction;
+
+    private Image mImage;
+
+    private InvalidateAction() {
+        super("&Invalidate Layout");
+        setAccelerator(SWT.MOD1 + 'I');
+        ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class);
+        mImage = imageLoader.loadImage("invalidate.png", Display.getDefault()); //$NON-NLS-1$
+        setImageDescriptor(ImageDescriptor.createFromImage(mImage));
+        setToolTipText("Invalidate the layout for the current window");
+    }
+
+    public static InvalidateAction getAction() {
+        if (sAction == null) {
+            sAction = new InvalidateAction();
+        }
+        return sAction;
+    }
+
+    @Override
+    public void run() {
+        HierarchyViewerDirector.getDirector().invalidateCurrentNode();
+    }
+
+    @Override
+    public Image getImage() {
+        return mImage;
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadOverlayAction.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadOverlayAction.java
new file mode 100644
index 0000000..1876358
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadOverlayAction.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.actions;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+public class LoadOverlayAction extends PixelPerfectEnabledAction implements ImageAction {
+
+    private static LoadOverlayAction sAction;
+
+    private Image mImage;
+
+    private Shell mShell;
+
+    private LoadOverlayAction(Shell shell) {
+        super("Load &Overlay");
+        this.mShell = shell;
+        setAccelerator(SWT.MOD1 + 'O');
+        ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class);
+        mImage = imageLoader.loadImage("load-overlay.png", Display.getDefault()); //$NON-NLS-1$
+        setImageDescriptor(ImageDescriptor.createFromImage(mImage));
+        setToolTipText("Load an image to overlay the screenshot");
+    }
+
+    public static LoadOverlayAction getAction(Shell shell) {
+        if (sAction == null) {
+            sAction = new LoadOverlayAction(shell);
+        }
+        return sAction;
+    }
+
+    @Override
+    public void run() {
+        HierarchyViewerDirector.getDirector().loadOverlay(mShell);
+    }
+
+    @Override
+    public Image getImage() {
+        return mImage;
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadViewHierarchyAction.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadViewHierarchyAction.java
new file mode 100644
index 0000000..6666315
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadViewHierarchyAction.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.actions;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+import com.android.hierarchyviewerlib.device.IHvDevice;
+import com.android.hierarchyviewerlib.models.DeviceSelectionModel;
+import com.android.hierarchyviewerlib.models.DeviceSelectionModel.IWindowChangeListener;
+import com.android.hierarchyviewerlib.models.Window;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+
+public class LoadViewHierarchyAction extends Action implements ImageAction, IWindowChangeListener {
+
+    private static LoadViewHierarchyAction sAction;
+
+    private Image mImage;
+
+    private LoadViewHierarchyAction() {
+        super("Load View &Hierarchy");
+        setAccelerator(SWT.MOD1 + 'H');
+        ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class);
+        mImage = imageLoader.loadImage("load-view-hierarchy.png", Display.getDefault()); //$NON-NLS-1$
+        setImageDescriptor(ImageDescriptor.createFromImage(mImage));
+        setToolTipText("Load the view hierarchy into the tree view");
+        setEnabled(
+                DeviceSelectionModel.getModel().getSelectedWindow() != null);
+        DeviceSelectionModel.getModel().addWindowChangeListener(this);
+    }
+
+    public static LoadViewHierarchyAction getAction() {
+        if (sAction == null) {
+            sAction = new LoadViewHierarchyAction();
+        }
+        return sAction;
+    }
+
+    @Override
+    public void run() {
+        HierarchyViewerDirector.getDirector().loadViewHierarchy();
+    }
+
+    @Override
+    public Image getImage() {
+        return mImage;
+    }
+
+    @Override
+    public void deviceChanged(IHvDevice device) {
+        // pass
+    }
+
+    @Override
+    public void deviceConnected(IHvDevice device) {
+        // pass
+    }
+
+    @Override
+    public void deviceDisconnected(IHvDevice device) {
+        // pass
+    }
+
+    @Override
+    public void focusChanged(IHvDevice device) {
+        // pass
+    }
+
+    @Override
+    public void selectionChanged(final IHvDevice device, final Window window) {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                LoadViewHierarchyAction.getAction().setEnabled(window != null);
+            }
+        });
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfectAutoRefreshAction.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfectAutoRefreshAction.java
new file mode 100644
index 0000000..a47c143
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfectAutoRefreshAction.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.actions;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+
+public class PixelPerfectAutoRefreshAction extends PixelPerfectEnabledAction implements ImageAction {
+
+    private static PixelPerfectAutoRefreshAction sAction;
+
+    private Image mImage;
+
+    private PixelPerfectAutoRefreshAction() {
+        super("Auto &Refresh", Action.AS_CHECK_BOX);
+        setAccelerator(SWT.MOD1 + 'R');
+        ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class);
+        mImage = imageLoader.loadImage("auto-refresh.png", Display.getDefault()); //$NON-NLS-1$
+        setImageDescriptor(ImageDescriptor.createFromImage(mImage));
+        setToolTipText("Automatically refresh the screenshot");
+    }
+
+    public static PixelPerfectAutoRefreshAction getAction() {
+        if (sAction == null) {
+            sAction = new PixelPerfectAutoRefreshAction();
+        }
+        return sAction;
+    }
+
+    @Override
+    public void run() {
+        HierarchyViewerDirector.getDirector().setPixelPerfectAutoRefresh(sAction.isChecked());
+    }
+
+    @Override
+    public Image getImage() {
+        return mImage;
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfectEnabledAction.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfectEnabledAction.java
new file mode 100644
index 0000000..33cb343
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfectEnabledAction.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.actions;
+
+import com.android.hierarchyviewerlib.models.PixelPerfectModel;
+import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.swt.widgets.Display;
+
+public class PixelPerfectEnabledAction extends Action implements IImageChangeListener {
+    public PixelPerfectEnabledAction(String name) {
+        super(name);
+        setEnabled(PixelPerfectModel.getModel().getImage() != null);
+        PixelPerfectModel.getModel().addImageChangeListener(this);
+    }
+
+    public PixelPerfectEnabledAction(String name, int type) {
+        super(name, type);
+        setEnabled(PixelPerfectModel.getModel().getImage() != null);
+        PixelPerfectModel.getModel().addImageChangeListener(this);
+    }
+
+    @Override
+    public void crosshairMoved() {
+        // pass
+    }
+
+    @Override
+    public void imageChanged() {
+        //
+    }
+
+    @Override
+    public void imageLoaded() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                setEnabled(PixelPerfectModel.getModel().getImage() != null);
+            }
+        });
+    }
+
+    @Override
+    public void overlayChanged() {
+        // pass
+    }
+
+    @Override
+    public void overlayTransparencyChanged() {
+        // pass
+    }
+
+    @Override
+    public void selectionChanged() {
+        // pass
+    }
+
+    @Override
+    public void treeChanged() {
+        // pass
+    }
+
+    @Override
+    public void zoomChanged() {
+        // pass
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ProfileNodesAction.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ProfileNodesAction.java
new file mode 100644
index 0000000..4bf93e8
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ProfileNodesAction.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2013 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.hierarchyviewerlib.actions;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+
+public class ProfileNodesAction extends SelectedNodeEnabledAction implements ImageAction {
+    private static ProfileNodesAction sAction;
+
+    private Image mImage;
+
+    public ProfileNodesAction() {
+        super("Profile Node");
+        ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class);
+        mImage = imageLoader.loadImage("profile.png", Display.getDefault()); //$NON-NLS-1$
+        setImageDescriptor(ImageDescriptor.createFromImage(mImage));
+        setToolTipText("Obtain layout times for tree rooted at selected node");
+    }
+
+    public static ProfileNodesAction getAction() {
+        if (sAction == null) {
+            sAction = new ProfileNodesAction();
+        }
+        return sAction;
+    }
+
+    @Override
+    public void run() {
+        HierarchyViewerDirector.getDirector().profileCurrentNode();
+    }
+
+    @Override
+    public Image getImage() {
+        return mImage;
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectAction.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectAction.java
new file mode 100644
index 0000000..54f53c8
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectAction.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.actions;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+
+public class RefreshPixelPerfectAction extends PixelPerfectEnabledAction implements ImageAction {
+
+    private static RefreshPixelPerfectAction sAction;
+
+    private Image mImage;
+
+    private RefreshPixelPerfectAction() {
+        super("&Refresh Screenshot");
+        setAccelerator(SWT.F5);
+        ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class);
+        mImage = imageLoader.loadImage("refresh-windows.png", Display.getDefault()); //$NON-NLS-1$
+        setImageDescriptor(ImageDescriptor.createFromImage(mImage));
+        setToolTipText("Refresh the screenshot");
+    }
+
+    public static RefreshPixelPerfectAction getAction() {
+        if (sAction == null) {
+            sAction = new RefreshPixelPerfectAction();
+        }
+        return sAction;
+    }
+
+    @Override
+    public void run() {
+        HierarchyViewerDirector.getDirector().refreshPixelPerfect();
+    }
+
+    @Override
+    public Image getImage() {
+        return mImage;
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectTreeAction.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectTreeAction.java
new file mode 100644
index 0000000..e9d1c56
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectTreeAction.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.actions;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+
+public class RefreshPixelPerfectTreeAction extends PixelPerfectEnabledAction implements ImageAction {
+
+    private static RefreshPixelPerfectTreeAction sAction;
+
+    private Image mImage;
+
+    private RefreshPixelPerfectTreeAction() {
+        super("Refresh &Tree");
+        setAccelerator(SWT.MOD1 + 'T');
+        ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class);
+        mImage = imageLoader.loadImage("load-view-hierarchy.png", Display.getDefault()); //$NON-NLS-1$
+        setImageDescriptor(ImageDescriptor.createFromImage(mImage));
+        setToolTipText("Refresh the tree");
+    }
+
+    public static RefreshPixelPerfectTreeAction getAction() {
+        if (sAction == null) {
+            sAction = new RefreshPixelPerfectTreeAction();
+        }
+        return sAction;
+    }
+
+    @Override
+    public void run() {
+        HierarchyViewerDirector.getDirector().refreshPixelPerfectTree();
+    }
+
+    @Override
+    public Image getImage() {
+        return mImage;
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshViewAction.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshViewAction.java
new file mode 100644
index 0000000..01c2527
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshViewAction.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.actions;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+
+public class RefreshViewAction extends TreeViewEnabledAction implements ImageAction {
+
+    private static RefreshViewAction sAction;
+
+    private Image mImage;
+
+    private RefreshViewAction() {
+        super("Load View &Hierarchy");
+        setAccelerator(SWT.MOD1 + 'H');
+        ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class);
+        mImage = imageLoader.loadImage("load-view-hierarchy.png", Display.getDefault()); //$NON-NLS-1$
+        setImageDescriptor(ImageDescriptor.createFromImage(mImage));
+        setToolTipText("Reload the view hierarchy");
+    }
+
+    public static RefreshViewAction getAction() {
+        if (sAction == null) {
+            sAction = new RefreshViewAction();
+        }
+        return sAction;
+    }
+
+    @Override
+    public void run() {
+        HierarchyViewerDirector.getDirector().reloadViewHierarchy();
+    }
+
+    @Override
+    public Image getImage() {
+        return mImage;
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshWindowsAction.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshWindowsAction.java
new file mode 100644
index 0000000..561f4ea
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshWindowsAction.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.actions;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+
+public class RefreshWindowsAction extends Action implements ImageAction {
+
+    private static RefreshWindowsAction sAction;
+
+    private Image mImage;
+
+    private RefreshWindowsAction() {
+        super("&Refresh");
+        setAccelerator(SWT.F5);
+        ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class);
+        mImage = imageLoader.loadImage("refresh-windows.png", Display.getDefault()); //$NON-NLS-1$
+        setImageDescriptor(ImageDescriptor.createFromImage(mImage));
+        setToolTipText("Refresh the list of devices");
+    }
+
+    public static RefreshWindowsAction getAction() {
+        if (sAction == null) {
+            sAction = new RefreshWindowsAction();
+        }
+        return sAction;
+    }
+
+    @Override
+    public void run() {
+        HierarchyViewerDirector.getDirector().refreshWindows();
+    }
+
+    @Override
+    public Image getImage() {
+        return mImage;
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RequestLayoutAction.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RequestLayoutAction.java
new file mode 100644
index 0000000..6fc7867
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RequestLayoutAction.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.actions;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+
+public class RequestLayoutAction extends SelectedNodeEnabledAction implements ImageAction {
+
+    private static RequestLayoutAction sAction;
+
+    private Image mImage;
+
+    private RequestLayoutAction() {
+        super("Request &Layout");
+        setAccelerator(SWT.MOD1 + 'L');
+        ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class);
+        mImage = imageLoader.loadImage("request-layout.png", Display.getDefault()); //$NON-NLS-1$
+        setImageDescriptor(ImageDescriptor.createFromImage(mImage));
+        setToolTipText("Request the view to lay out");
+    }
+
+    public static RequestLayoutAction getAction() {
+        if (sAction == null) {
+            sAction = new RequestLayoutAction();
+        }
+        return sAction;
+    }
+
+    @Override
+    public void run() {
+        HierarchyViewerDirector.getDirector().relayoutCurrentNode();
+    }
+
+    @Override
+    public Image getImage() {
+        return mImage;
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SavePixelPerfectAction.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SavePixelPerfectAction.java
new file mode 100644
index 0000000..57e0094
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SavePixelPerfectAction.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.actions;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+public class SavePixelPerfectAction extends PixelPerfectEnabledAction implements ImageAction {
+
+    private static SavePixelPerfectAction sAction;
+
+    private Image mImage;
+
+    private Shell mShell;
+
+    private SavePixelPerfectAction(Shell shell) {
+        super("&Save as PNG");
+        this.mShell = shell;
+        setAccelerator(SWT.MOD1 + 'S');
+        ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class);
+        mImage = imageLoader.loadImage("save.png", Display.getDefault()); //$NON-NLS-1$
+        setImageDescriptor(ImageDescriptor.createFromImage(mImage));
+        setToolTipText("Save the screenshot as a PNG image");
+    }
+
+    public static SavePixelPerfectAction getAction(Shell shell) {
+        if (sAction == null) {
+            sAction = new SavePixelPerfectAction(shell);
+        }
+        return sAction;
+    }
+
+    @Override
+    public void run() {
+        HierarchyViewerDirector.getDirector().savePixelPerfect(mShell);
+    }
+
+    @Override
+    public Image getImage() {
+        return mImage;
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SaveTreeViewAction.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SaveTreeViewAction.java
new file mode 100644
index 0000000..9e11919
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SaveTreeViewAction.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.actions;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+public class SaveTreeViewAction extends TreeViewEnabledAction implements ImageAction {
+
+    private static SaveTreeViewAction sAction;
+
+    private Image mImage;
+
+    private Shell mShell;
+
+    private SaveTreeViewAction(Shell shell) {
+        super("&Save as PNG");
+        this.mShell = shell;
+        setAccelerator(SWT.MOD1 + 'S');
+        ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class);
+        mImage = imageLoader.loadImage("save.png", Display.getDefault()); //$NON-NLS-1$
+        setImageDescriptor(ImageDescriptor.createFromImage(mImage));
+        setToolTipText("Save the tree view as a PNG image");
+    }
+
+    public static SaveTreeViewAction getAction(Shell shell) {
+        if (sAction == null) {
+            sAction = new SaveTreeViewAction(shell);
+        }
+        return sAction;
+    }
+
+    @Override
+    public void run() {
+        HierarchyViewerDirector.getDirector().saveTreeView(mShell);
+    }
+
+    @Override
+    public Image getImage() {
+        return mImage;
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SelectedNodeEnabledAction.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SelectedNodeEnabledAction.java
new file mode 100644
index 0000000..eee28b9
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SelectedNodeEnabledAction.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.actions;
+
+import com.android.hierarchyviewerlib.models.TreeViewModel;
+import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.swt.widgets.Display;
+
+public class SelectedNodeEnabledAction extends Action implements ITreeChangeListener {
+    public SelectedNodeEnabledAction(String name) {
+        super(name);
+        setEnabled(TreeViewModel.getModel().getTree() != null
+                && TreeViewModel.getModel().getSelection() != null);
+        TreeViewModel.getModel().addTreeChangeListener(this);
+    }
+
+    @Override
+    public void selectionChanged() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                setEnabled(TreeViewModel.getModel().getTree() != null
+                        && TreeViewModel.getModel().getSelection() != null);
+            }
+        });
+    }
+
+    @Override
+    public void treeChanged() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                setEnabled(TreeViewModel.getModel().getTree() != null
+                        && TreeViewModel.getModel().getSelection() != null);
+            }
+        });
+    }
+
+    @Override
+    public void viewportChanged() {
+    }
+
+    @Override
+    public void zoomChanged() {
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/TreeViewEnabledAction.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/TreeViewEnabledAction.java
new file mode 100644
index 0000000..4b9c02c
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/TreeViewEnabledAction.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.actions;
+
+import com.android.hierarchyviewerlib.models.TreeViewModel;
+import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.swt.widgets.Display;
+
+public class TreeViewEnabledAction extends Action implements ITreeChangeListener {
+    public TreeViewEnabledAction(String name) {
+        super(name);
+        setEnabled(TreeViewModel.getModel().getTree() != null);
+        TreeViewModel.getModel().addTreeChangeListener(this);
+    }
+
+    @Override
+    public void selectionChanged() {
+        // pass
+    }
+
+    @Override
+    public void treeChanged() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                setEnabled(TreeViewModel.getModel().getTree() != null);
+            }
+        });
+    }
+
+    @Override
+    public void viewportChanged() {
+    }
+
+    @Override
+    public void zoomChanged() {
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/AbstractHvDevice.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/AbstractHvDevice.java
new file mode 100644
index 0000000..e330168
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/AbstractHvDevice.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2013 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.hierarchyviewerlib.device;
+
+import com.android.ddmlib.AdbCommandRejectedException;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.RawImage;
+import com.android.ddmlib.TimeoutException;
+
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.PaletteData;
+import org.eclipse.swt.widgets.Display;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicReference;
+
+public abstract class AbstractHvDevice implements IHvDevice {
+    private static final String TAG = "HierarchyViewer";
+
+    @Override
+    public Image getScreenshotImage() {
+        IDevice device = getDevice();
+        final AtomicReference<Image> imageRef = new AtomicReference<Image>();
+
+        try {
+            final RawImage screenshot = device.getScreenshot();
+            if (screenshot == null) {
+                return null;
+            }
+            Display.getDefault().syncExec(new Runnable() {
+                @Override
+                public void run() {
+                    ImageData imageData =
+                            new ImageData(screenshot.width, screenshot.height, screenshot.bpp,
+                                    new PaletteData(screenshot.getRedMask(), screenshot
+                                            .getGreenMask(), screenshot.getBlueMask()), 1,
+                                    screenshot.data);
+                    imageRef.set(new Image(Display.getDefault(), imageData));
+                }
+            });
+            return imageRef.get();
+        } catch (IOException e) {
+            Log.e(TAG, "Unable to load screenshot from device " + device.getName());
+        } catch (TimeoutException e) {
+            Log.e(TAG, "Timeout loading screenshot from device " + device.getName());
+        } catch (AdbCommandRejectedException e) {
+            Log.e(TAG, "Adb rejected command to load screenshot from device " + device.getName());
+        }
+        return null;
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DdmViewDebugDevice.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DdmViewDebugDevice.java
new file mode 100644
index 0000000..0172995
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DdmViewDebugDevice.java
@@ -0,0 +1,417 @@
+/*
+ * Copyright (C) 2013 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.hierarchyviewerlib.device;
+
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData;
+import com.android.ddmlib.HandleViewDebug;
+import com.android.ddmlib.HandleViewDebug.ViewDumpHandler;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log;
+import com.android.hierarchyviewerlib.device.WindowUpdater.IWindowChangeListener;
+import com.android.hierarchyviewerlib.models.ViewNode;
+import com.android.hierarchyviewerlib.models.Window;
+import com.android.hierarchyviewerlib.ui.util.PsdFile;
+
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.StringReader;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class DdmViewDebugDevice extends AbstractHvDevice implements IDeviceChangeListener {
+    private static final String TAG = "DdmViewDebugDevice";
+
+    private final IDevice mDevice;
+    private Map<Client, List<String>> mViewRootsPerClient = new HashMap<Client, List<String>>(40);
+
+    public DdmViewDebugDevice(IDevice device) {
+        mDevice = device;
+    }
+
+    @Override
+    public boolean initializeViewDebug() {
+        AndroidDebugBridge.addDeviceChangeListener(this);
+        return reloadWindows();
+    }
+
+    private static class ListViewRootsHandler extends ViewDumpHandler {
+        private List<String> mViewRoots = Collections.synchronizedList(new ArrayList<String>(10));
+
+        public ListViewRootsHandler() {
+            super(HandleViewDebug.CHUNK_VULW);
+        }
+
+        @Override
+        protected void handleViewDebugResult(ByteBuffer data) {
+            int nWindows = data.getInt();
+
+            for (int i = 0; i < nWindows; i++) {
+                int len = data.getInt();
+                mViewRoots.add(getString(data, len));
+            }
+        }
+
+        public List<String> getViewRoots(long timeout, TimeUnit unit) {
+            waitForResult(timeout, unit);
+            return mViewRoots;
+        }
+    }
+
+    private static class CaptureByteArrayHandler extends ViewDumpHandler {
+        public CaptureByteArrayHandler(int type) {
+            super(type);
+        }
+
+        private AtomicReference<byte[]> mData = new AtomicReference<byte[]>();
+
+        @Override
+        protected void handleViewDebugResult(ByteBuffer data) {
+            byte[] b = new byte[data.remaining()];
+            data.get(b);
+            mData.set(b);
+
+        }
+
+        public byte[] getData(long timeout, TimeUnit unit) {
+            waitForResult(timeout, unit);
+            return mData.get();
+        }
+    }
+
+    private static class CaptureLayersHandler extends ViewDumpHandler {
+        private AtomicReference<PsdFile> mPsd = new AtomicReference<PsdFile>();
+
+        public CaptureLayersHandler() {
+            super(HandleViewDebug.CHUNK_VURT);
+        }
+
+        @Override
+        protected void handleViewDebugResult(ByteBuffer data) {
+            byte[] b = new byte[data.remaining()];
+            data.get(b);
+            DataInputStream dis = new DataInputStream(new ByteArrayInputStream(b));
+            try {
+                mPsd.set(DeviceBridge.parsePsd(dis));
+            } catch (IOException e) {
+                Log.e(TAG, e);
+            }
+        }
+
+        public PsdFile getPsdFile(long timeout, TimeUnit unit) {
+            waitForResult(timeout, unit);
+            return mPsd.get();
+        }
+    }
+
+    @Override
+    public boolean reloadWindows() {
+        mViewRootsPerClient = new HashMap<Client, List<String>>(40);
+
+        for (Client c : mDevice.getClients()) {
+            ClientData cd = c.getClientData();
+            if (cd != null && cd.hasFeature(ClientData.FEATURE_VIEW_HIERARCHY)) {
+                ListViewRootsHandler handler = new ListViewRootsHandler();
+
+                try {
+                    HandleViewDebug.listViewRoots(c, handler);
+                } catch (IOException e) {
+                    Log.i(TAG, "No connection to client: " + cd.getClientDescription());
+                    continue;
+                }
+
+                List<String> viewRoots = new ArrayList<String>(
+                        handler.getViewRoots(200, TimeUnit.MILLISECONDS));
+                mViewRootsPerClient.put(c, viewRoots);
+            }
+        }
+
+        return true;
+    }
+
+    @Override
+    public void terminateViewDebug() {
+        // nothing to terminate
+    }
+
+    @Override
+    public boolean isViewDebugEnabled() {
+        return true;
+    }
+
+    @Override
+    public boolean supportsDisplayListDump() {
+        return true;
+    }
+
+    @Override
+    public Window[] getWindows() {
+        List<Window> windows = new ArrayList<Window>(10);
+
+        for (Client c: mViewRootsPerClient.keySet()) {
+            for (String viewRoot: mViewRootsPerClient.get(c)) {
+                windows.add(new Window(this, viewRoot, c));
+            }
+        }
+
+        return windows.toArray(new Window[windows.size()]);
+    }
+
+    @Override
+    public int getFocusedWindow() {
+        // TODO: add support for identifying view in focus
+        return -1;
+    }
+
+    @Override
+    public IDevice getDevice() {
+        return mDevice;
+    }
+
+    @Override
+    public ViewNode loadWindowData(Window window) {
+        Client c = window.getClient();
+        if (c == null) {
+            return null;
+        }
+
+        String viewRoot = window.getTitle();
+        CaptureByteArrayHandler handler = new CaptureByteArrayHandler(HandleViewDebug.CHUNK_VURT);
+        try {
+            HandleViewDebug.dumpViewHierarchy(c, viewRoot,
+                    false /* skipChildren */,
+                    true  /* includeProperties */,
+                    handler);
+        } catch (IOException e) {
+            Log.e(TAG, e);
+            return null;
+        }
+
+        byte[] data = handler.getData(20, TimeUnit.SECONDS);
+        if (data == null) {
+            return null;
+        }
+
+        String viewHierarchy = new String(data, Charset.forName("UTF-8"));
+        return DeviceBridge.parseViewHierarchy(new BufferedReader(new StringReader(viewHierarchy)),
+                window);
+    }
+
+    @Override
+    public void loadProfileData(Window window, ViewNode viewNode) {
+        Client c = window.getClient();
+        if (c == null) {
+            return;
+        }
+
+        String viewRoot = window.getTitle();
+        CaptureByteArrayHandler handler = new CaptureByteArrayHandler(HandleViewDebug.CHUNK_VUOP);
+        try {
+            HandleViewDebug.profileView(c, viewRoot, viewNode.toString(), handler);
+        } catch (IOException e) {
+            Log.e(TAG, e);
+            return;
+        }
+
+        byte[] data = handler.getData(30, TimeUnit.SECONDS);
+        if (data == null) {
+            Log.e(TAG, "Timed out waiting for profile data");
+            return;
+        }
+
+        try {
+            boolean success = DeviceBridge.loadProfileDataRecursive(viewNode,
+                    new BufferedReader(new StringReader(new String(data))));
+            if (success) {
+                viewNode.setProfileRatings();
+            }
+        } catch (IOException e) {
+            Log.e(TAG, e);
+            return;
+        }
+    }
+
+    @Override
+    public Image loadCapture(Window window, ViewNode viewNode) {
+        Client c = window.getClient();
+        if (c == null) {
+            return null;
+        }
+
+        String viewRoot = window.getTitle();
+        CaptureByteArrayHandler handler = new CaptureByteArrayHandler(HandleViewDebug.CHUNK_VUOP);
+
+        try {
+            HandleViewDebug.captureView(c, viewRoot, viewNode.toString(), handler);
+        } catch (IOException e) {
+            Log.e(TAG, e);
+            return null;
+        }
+
+        byte[] data = handler.getData(10, TimeUnit.SECONDS);
+        return (data == null) ? null :
+            new Image(Display.getDefault(), new ByteArrayInputStream(data));
+    }
+
+    @Override
+    public PsdFile captureLayers(Window window) {
+        Client c = window.getClient();
+        if (c == null) {
+            return null;
+        }
+
+        String viewRoot = window.getTitle();
+        CaptureLayersHandler handler = new CaptureLayersHandler();
+        try {
+            HandleViewDebug.captureLayers(c, viewRoot, handler);
+        } catch (IOException e) {
+            Log.e(TAG, e);
+            return null;
+        }
+
+        return handler.getPsdFile(20, TimeUnit.SECONDS);
+    }
+
+    @Override
+    public void invalidateView(ViewNode viewNode) {
+        Window window = viewNode.window;
+        Client c = window.getClient();
+        if (c == null) {
+            return;
+        }
+
+        String viewRoot = window.getTitle();
+        try {
+            HandleViewDebug.invalidateView(c, viewRoot, viewNode.toString());
+        } catch (IOException e) {
+            Log.e(TAG, e);
+        }
+    }
+
+    @Override
+    public void requestLayout(ViewNode viewNode) {
+        Window window = viewNode.window;
+        Client c = window.getClient();
+        if (c == null) {
+            return;
+        }
+
+        String viewRoot = window.getTitle();
+        try {
+            HandleViewDebug.requestLayout(c, viewRoot, viewNode.toString());
+        } catch (IOException e) {
+            Log.e(TAG, e);
+        }
+    }
+
+    @Override
+    public void outputDisplayList(ViewNode viewNode) {
+        Window window = viewNode.window;
+        Client c = window.getClient();
+        if (c == null) {
+            return;
+        }
+
+        String viewRoot = window.getTitle();
+        try {
+            HandleViewDebug.dumpDisplayList(c, viewRoot, viewNode.toString());
+        } catch (IOException e) {
+            Log.e(TAG, e);
+        }
+    }
+
+    @Override
+    public void addWindowChangeListener(IWindowChangeListener l) {
+        // TODO: add support for listening to view root changes
+    }
+
+    @Override
+    public void removeWindowChangeListener(IWindowChangeListener l) {
+        // TODO: add support for listening to view root changes
+    }
+
+    @Override
+    public void deviceConnected(IDevice device) {
+        // pass
+    }
+
+    @Override
+    public void deviceDisconnected(IDevice device) {
+        // pass
+    }
+
+    @Override
+    public void deviceChanged(IDevice device, int changeMask) {
+        if ((changeMask & IDevice.CHANGE_CLIENT_LIST) != 0) {
+            reloadWindows();
+        }
+    }
+
+    @Override
+    public boolean isViewUpdateEnabled() {
+        return true;
+    }
+
+    @Override
+    public void invokeViewMethod(Window window, ViewNode viewNode, String method,
+            List<?> args) {
+        Client c = window.getClient();
+        if (c == null) {
+            return;
+        }
+
+        String viewRoot = window.getTitle();
+        try {
+            HandleViewDebug.invokeMethod(c, viewRoot, viewNode.toString(), method, args.toArray());
+        } catch (IOException e) {
+            Log.e(TAG, e);
+        }
+    }
+
+    @Override
+    public boolean setLayoutParameter(Window window, ViewNode viewNode, String property,
+            int value) {
+        Client c = window.getClient();
+        if (c == null) {
+            return false;
+        }
+
+        String viewRoot = window.getTitle();
+        try {
+            HandleViewDebug.setLayoutParameter(c, viewRoot, viewNode.toString(), property, value);
+        } catch (IOException e) {
+            Log.e(TAG, e);
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DeviceBridge.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DeviceBridge.java
new file mode 100644
index 0000000..ca3627b
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DeviceBridge.java
@@ -0,0 +1,697 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.device;
+
+import com.android.ddmlib.AdbCommandRejectedException;
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.MultiLineReceiver;
+import com.android.ddmlib.ShellCommandUnresponsiveException;
+import com.android.ddmlib.TimeoutException;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+import com.android.hierarchyviewerlib.models.ViewNode;
+import com.android.hierarchyviewerlib.models.Window;
+import com.android.hierarchyviewerlib.ui.util.PsdFile;
+
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+
+import java.awt.Graphics2D;
+import java.awt.Point;
+import java.awt.image.BufferedImage;
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.imageio.ImageIO;
+
+/**
+ * A bridge to the device.
+ */
+public class DeviceBridge {
+
+    public static final String TAG = "hierarchyviewer";
+
+    private static final int DEFAULT_SERVER_PORT = 4939;
+
+    // These codes must match the auto-generated codes in IWindowManager.java
+    // See IWindowManager.aidl as well
+    private static final int SERVICE_CODE_START_SERVER = 1;
+
+    private static final int SERVICE_CODE_STOP_SERVER = 2;
+
+    private static final int SERVICE_CODE_IS_SERVER_RUNNING = 3;
+
+    private static AndroidDebugBridge sBridge;
+
+    private static final HashMap<IDevice, Integer> sDevicePortMap = new HashMap<IDevice, Integer>();
+
+    private static final HashMap<IDevice, ViewServerInfo> sViewServerInfo =
+            new HashMap<IDevice, ViewServerInfo>();
+
+    private static int sNextLocalPort = DEFAULT_SERVER_PORT;
+
+    public static class ViewServerInfo {
+        public final int protocolVersion;
+
+        public final int serverVersion;
+
+        ViewServerInfo(int serverVersion, int protocolVersion) {
+            this.protocolVersion = protocolVersion;
+            this.serverVersion = serverVersion;
+        }
+    }
+
+    /**
+     * Init the DeviceBridge with an existing {@link AndroidDebugBridge}.
+     * @param bridge the bridge object to use
+     */
+    public static void acquireBridge(AndroidDebugBridge bridge) {
+        sBridge = bridge;
+    }
+
+    /**
+     * Creates an {@link AndroidDebugBridge} connected to adb at the given location.
+     *
+     * If a bridge is already running, this disconnects it and creates a new one.
+     *
+     * @param adbLocation the location to adb.
+     */
+    public static void initDebugBridge(String adbLocation) {
+        if (sBridge == null) {
+            /* debugger support required only if hv is using ddm protocol */
+            AndroidDebugBridge.init(HierarchyViewerDirector.isUsingDdmProtocol());
+        }
+        if (sBridge == null || !sBridge.isConnected()) {
+            sBridge = AndroidDebugBridge.createBridge(adbLocation, true);
+        }
+    }
+
+    /** Disconnects the current {@link AndroidDebugBridge}. */
+    public static void terminate() {
+        AndroidDebugBridge.terminate();
+    }
+
+    public static IDevice[] getDevices() {
+        if (sBridge == null) {
+            return new IDevice[0];
+        }
+        return sBridge.getDevices();
+    }
+
+    /*
+     * This adds a listener to the debug bridge. The listener is notified of
+     * connecting/disconnecting devices, devices coming online, etc.
+     */
+    public static void startListenForDevices(AndroidDebugBridge.IDeviceChangeListener listener) {
+        AndroidDebugBridge.addDeviceChangeListener(listener);
+    }
+
+    public static void stopListenForDevices(AndroidDebugBridge.IDeviceChangeListener listener) {
+        AndroidDebugBridge.removeDeviceChangeListener(listener);
+    }
+
+    /**
+     * Sets up a just-connected device to work with the view server.
+     * <p/>
+     * This starts a port forwarding between a local port and a port on the
+     * device.
+     *
+     * @param device
+     */
+    public static void setupDeviceForward(IDevice device) {
+        synchronized (sDevicePortMap) {
+            if (device.getState() == IDevice.DeviceState.ONLINE) {
+                int localPort = sNextLocalPort++;
+                try {
+                    device.createForward(localPort, DEFAULT_SERVER_PORT);
+                    sDevicePortMap.put(device, localPort);
+                } catch (TimeoutException e) {
+                    Log.e(TAG, "Timeout setting up port forwarding for " + device);
+                } catch (AdbCommandRejectedException e) {
+                    Log.e(TAG, String.format("Adb rejected forward command for device %1$s: %2$s",
+                            device, e.getMessage()));
+                } catch (IOException e) {
+                    Log.e(TAG, String.format("Failed to create forward for device %1$s: %2$s",
+                            device, e.getMessage()));
+                }
+            }
+        }
+    }
+
+    public static void removeDeviceForward(IDevice device) {
+        synchronized (sDevicePortMap) {
+            final Integer localPort = sDevicePortMap.get(device);
+            if (localPort != null) {
+                try {
+                    device.removeForward(localPort, DEFAULT_SERVER_PORT);
+                    sDevicePortMap.remove(device);
+                } catch (TimeoutException e) {
+                    Log.e(TAG, "Timeout removing port forwarding for " + device);
+                } catch (AdbCommandRejectedException e) {
+                    // In this case, we want to fail silently.
+                } catch (IOException e) {
+                    Log.e(TAG, String.format("Failed to remove forward for device %1$s: %2$s",
+                            device, e.getMessage()));
+                }
+            }
+        }
+    }
+
+    public static int getDeviceLocalPort(IDevice device) {
+        synchronized (sDevicePortMap) {
+            Integer port = sDevicePortMap.get(device);
+            if (port != null) {
+                return port;
+            }
+
+            Log.e(TAG, "Missing forwarded port for " + device.getSerialNumber());
+            return -1;
+        }
+
+    }
+
+    public static boolean isViewServerRunning(IDevice device) {
+        final boolean[] result = new boolean[1];
+        try {
+            if (device.isOnline()) {
+                device.executeShellCommand(buildIsServerRunningShellCommand(),
+                        new BooleanResultReader(result));
+                if (!result[0]) {
+                    ViewServerInfo serverInfo = loadViewServerInfo(device);
+                    if (serverInfo != null && serverInfo.protocolVersion > 2) {
+                        result[0] = true;
+                    }
+                }
+            }
+        } catch (TimeoutException e) {
+            Log.e(TAG, "Timeout checking status of view server on device " + device);
+        } catch (IOException e) {
+            Log.e(TAG, "Unable to check status of view server on device " + device);
+        } catch (AdbCommandRejectedException e) {
+            Log.e(TAG, "Adb rejected command to check status of view server on device " + device);
+        } catch (ShellCommandUnresponsiveException e) {
+            Log.e(TAG, "Unable to execute command to check status of view server on device "
+                    + device);
+        }
+        return result[0];
+    }
+
+    public static boolean startViewServer(IDevice device) {
+        return startViewServer(device, DEFAULT_SERVER_PORT);
+    }
+
+    public static boolean startViewServer(IDevice device, int port) {
+        final boolean[] result = new boolean[1];
+        try {
+            if (device.isOnline()) {
+                device.executeShellCommand(buildStartServerShellCommand(port),
+                        new BooleanResultReader(result));
+            }
+        } catch (TimeoutException e) {
+            Log.e(TAG, "Timeout starting view server on device " + device);
+        } catch (IOException e) {
+            Log.e(TAG, "Unable to start view server on device " + device);
+        } catch (AdbCommandRejectedException e) {
+            Log.e(TAG, "Adb rejected command to start view server on device " + device);
+        } catch (ShellCommandUnresponsiveException e) {
+            Log.e(TAG, "Unable to execute command to start view server on device " + device);
+        }
+        return result[0];
+    }
+
+    public static boolean stopViewServer(IDevice device) {
+        final boolean[] result = new boolean[1];
+        try {
+            if (device.isOnline()) {
+                device.executeShellCommand(buildStopServerShellCommand(), new BooleanResultReader(
+                        result));
+            }
+        } catch (TimeoutException e) {
+            Log.e(TAG, "Timeout stopping view server on device " + device);
+        } catch (IOException e) {
+            Log.e(TAG, "Unable to stop view server on device " + device);
+        } catch (AdbCommandRejectedException e) {
+            Log.e(TAG, "Adb rejected command to stop view server on device " + device);
+        } catch (ShellCommandUnresponsiveException e) {
+            Log.e(TAG, "Unable to execute command to stop view server on device " + device);
+        }
+        return result[0];
+    }
+
+    private static String buildStartServerShellCommand(int port) {
+        return String.format("service call window %d i32 %d", SERVICE_CODE_START_SERVER, port); //$NON-NLS-1$
+    }
+
+    private static String buildStopServerShellCommand() {
+        return String.format("service call window %d", SERVICE_CODE_STOP_SERVER); //$NON-NLS-1$
+    }
+
+    private static String buildIsServerRunningShellCommand() {
+        return String.format("service call window %d", SERVICE_CODE_IS_SERVER_RUNNING); //$NON-NLS-1$
+    }
+
+    private static class BooleanResultReader extends MultiLineReceiver {
+        private final boolean[] mResult;
+
+        public BooleanResultReader(boolean[] result) {
+            mResult = result;
+        }
+
+        @Override
+        public void processNewLines(String[] strings) {
+            if (strings.length > 0) {
+                Pattern pattern = Pattern.compile(".*?\\([0-9]{8} ([0-9]{8}).*"); //$NON-NLS-1$
+                Matcher matcher = pattern.matcher(strings[0]);
+                if (matcher.matches()) {
+                    if (Integer.parseInt(matcher.group(1)) == 1) {
+                        mResult[0] = true;
+                    }
+                }
+            }
+        }
+
+        @Override
+        public boolean isCancelled() {
+            return false;
+        }
+    }
+
+    public static ViewServerInfo loadViewServerInfo(IDevice device) {
+        int server = -1;
+        int protocol = -1;
+        DeviceConnection connection = null;
+        try {
+            connection = new DeviceConnection(device);
+            connection.sendCommand("SERVER"); //$NON-NLS-1$
+            String line = connection.getInputStream().readLine();
+            if (line != null) {
+                server = Integer.parseInt(line);
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "Unable to get view server version from device " + device);
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+        }
+        connection = null;
+        try {
+            connection = new DeviceConnection(device);
+            connection.sendCommand("PROTOCOL"); //$NON-NLS-1$
+            String line = connection.getInputStream().readLine();
+            if (line != null) {
+                protocol = Integer.parseInt(line);
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "Unable to get view server protocol version from device " + device);
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+        }
+        if (server == -1 || protocol == -1) {
+            return null;
+        }
+        ViewServerInfo returnValue = new ViewServerInfo(server, protocol);
+        synchronized (sViewServerInfo) {
+            sViewServerInfo.put(device, returnValue);
+        }
+        return returnValue;
+    }
+
+    public static ViewServerInfo getViewServerInfo(IDevice device) {
+        synchronized (sViewServerInfo) {
+            return sViewServerInfo.get(device);
+        }
+    }
+
+    public static void removeViewServerInfo(IDevice device) {
+        synchronized (sViewServerInfo) {
+            sViewServerInfo.remove(device);
+        }
+    }
+
+    /*
+     * This loads the list of windows from the specified device. The format is:
+     * hashCode1 title1 hashCode2 title2 ... hashCodeN titleN DONE.
+     */
+    public static Window[] loadWindows(IHvDevice hvDevice, IDevice device) {
+        ArrayList<Window> windows = new ArrayList<Window>();
+        DeviceConnection connection = null;
+        ViewServerInfo serverInfo = getViewServerInfo(device);
+        try {
+            connection = new DeviceConnection(device);
+            connection.sendCommand("LIST"); //$NON-NLS-1$
+            BufferedReader in = connection.getInputStream();
+            String line;
+            while ((line = in.readLine()) != null) {
+                if ("DONE.".equalsIgnoreCase(line)) { //$NON-NLS-1$
+                    break;
+                }
+
+                int index = line.indexOf(' ');
+                if (index != -1) {
+                    String windowId = line.substring(0, index);
+
+                    int id;
+                    if (serverInfo.serverVersion > 2) {
+                        id = (int) Long.parseLong(windowId, 16);
+                    } else {
+                        id = Integer.parseInt(windowId, 16);
+                    }
+
+                    Window w = new Window(hvDevice, line.substring(index + 1), id);
+                    windows.add(w);
+                }
+            }
+            // Automatic refreshing of windows was added in protocol version 3.
+            // Before, the user needed to specify explicitly that he wants to
+            // get the focused window, which was done using a special type of
+            // window with hash code -1.
+            if (serverInfo.protocolVersion < 3) {
+                windows.add(Window.getFocusedWindow(hvDevice));
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "Unable to load the window list from device " + device);
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+        }
+        // The server returns the list of windows from the window at the bottom
+        // to the top. We want the reverse order to put the top window on top of
+        // the list.
+        Window[] returnValue = new Window[windows.size()];
+        for (int i = windows.size() - 1; i >= 0; i--) {
+            returnValue[returnValue.length - i - 1] = windows.get(i);
+        }
+        return returnValue;
+    }
+
+    /*
+     * This gets the hash code of the window that has focus. Only works with
+     * protocol version 3 and above.
+     */
+    public static int getFocusedWindow(IDevice device) {
+        DeviceConnection connection = null;
+        try {
+            connection = new DeviceConnection(device);
+            connection.sendCommand("GET_FOCUS"); //$NON-NLS-1$
+            String line = connection.getInputStream().readLine();
+            if (line == null || line.length() == 0) {
+                return -1;
+            }
+            return (int) Long.parseLong(line.substring(0, line.indexOf(' ')), 16);
+        } catch (Exception e) {
+            Log.e(TAG, "Unable to get the focused window from device " + device);
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+        }
+        return -1;
+    }
+
+    public static ViewNode loadWindowData(Window window) {
+        DeviceConnection connection = null;
+        try {
+            connection = new DeviceConnection(window.getDevice());
+            connection.sendCommand("DUMP " + window.encode()); //$NON-NLS-1$
+            BufferedReader in = connection.getInputStream();
+            ViewNode currentNode = parseViewHierarchy(in, window);
+            ViewServerInfo serverInfo = getViewServerInfo(window.getDevice());
+            if (serverInfo != null) {
+                currentNode.protocolVersion = serverInfo.protocolVersion;
+            }
+            return currentNode;
+        } catch (Exception e) {
+            Log.e(TAG, "Unable to load window data for window " + window.getTitle() + " on device "
+                    + window.getDevice());
+            Log.e(TAG, e.getMessage());
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+        }
+        return null;
+    }
+
+    public static ViewNode parseViewHierarchy(BufferedReader in, Window window) {
+        ViewNode currentNode = null;
+        int currentDepth = -1;
+        String line;
+        try {
+            while ((line = in.readLine()) != null) {
+                if ("DONE.".equalsIgnoreCase(line)) {
+                    break;
+                }
+                int depth = 0;
+                while (line.charAt(depth) == ' ') {
+                    depth++;
+                }
+                while (depth <= currentDepth) {
+                    if (currentNode != null) {
+                        currentNode = currentNode.parent;
+                    }
+                    currentDepth--;
+                }
+                currentNode = new ViewNode(window, currentNode, line.substring(depth));
+                currentDepth = depth;
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "Error reading view hierarchy stream: " + e.getMessage());
+            return null;
+        }
+        if (currentNode == null) {
+            return null;
+        }
+        while (currentNode.parent != null) {
+            currentNode = currentNode.parent;
+        }
+
+        return currentNode;
+    }
+
+    public static boolean loadProfileData(Window window, ViewNode viewNode) {
+        DeviceConnection connection = null;
+        try {
+            connection = new DeviceConnection(window.getDevice());
+            connection.sendCommand("PROFILE " + window.encode() + " " + viewNode.toString()); //$NON-NLS-1$
+            BufferedReader in = connection.getInputStream();
+            int protocol;
+            synchronized (sViewServerInfo) {
+                protocol = sViewServerInfo.get(window.getDevice()).protocolVersion;
+            }
+            if (protocol < 3) {
+                return loadProfileData(viewNode, in);
+            } else {
+                boolean ret = loadProfileDataRecursive(viewNode, in);
+                if (ret) {
+                    viewNode.setProfileRatings();
+                }
+                return ret;
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "Unable to load profiling data for window " + window.getTitle()
+                    + " on device " + window.getDevice());
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+        }
+        return false;
+    }
+
+    private static boolean loadProfileData(ViewNode node, BufferedReader in) throws IOException {
+        String line;
+        if ((line = in.readLine()) == null || line.equalsIgnoreCase("-1 -1 -1") //$NON-NLS-1$
+                || line.equalsIgnoreCase("DONE.")) { //$NON-NLS-1$
+            return false;
+        }
+        String[] data = line.split(" ");
+        node.measureTime = (Long.parseLong(data[0]) / 1000.0) / 1000.0;
+        node.layoutTime = (Long.parseLong(data[1]) / 1000.0) / 1000.0;
+        node.drawTime = (Long.parseLong(data[2]) / 1000.0) / 1000.0;
+        return true;
+    }
+
+    public static boolean loadProfileDataRecursive(ViewNode node, BufferedReader in)
+            throws IOException {
+        if (!loadProfileData(node, in)) {
+            return false;
+        }
+        for (int i = 0; i < node.children.size(); i++) {
+            if (!loadProfileDataRecursive(node.children.get(i), in)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public static Image loadCapture(Window window, ViewNode viewNode) {
+        DeviceConnection connection = null;
+        try {
+            connection = new DeviceConnection(window.getDevice());
+            connection.getSocket().setSoTimeout(5000);
+            connection.sendCommand("CAPTURE " + window.encode() + " " + viewNode.toString()); //$NON-NLS-1$
+            return new Image(Display.getDefault(), connection.getSocket().getInputStream());
+        } catch (Exception e) {
+            Log.e(TAG, "Unable to capture data for node " + viewNode + " in window "
+                    + window.getTitle() + " on device " + window.getDevice());
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+        }
+        return null;
+    }
+
+    public static PsdFile captureLayers(Window window) {
+        DeviceConnection connection = null;
+        DataInputStream in = null;
+
+        try {
+            connection = new DeviceConnection(window.getDevice());
+            connection.sendCommand("CAPTURE_LAYERS " + window.encode()); //$NON-NLS-1$
+
+            in =
+                    new DataInputStream(new BufferedInputStream(connection.getSocket()
+                            .getInputStream()));
+
+            return parsePsd(in);
+        } catch (IOException e) {
+            Log.e(TAG, "Unable to capture layers for window " + window.getTitle() + " on device "
+                    + window.getDevice());
+        } finally {
+            if (in != null) {
+                try {
+                    in.close();
+                } catch (Exception ex) {
+                }
+            }
+
+            if (connection != null) {
+                connection.close();
+            }
+        }
+
+        return null;
+    }
+
+    public static PsdFile parsePsd(DataInputStream in) throws IOException {
+        int width = in.readInt();
+        int height = in.readInt();
+
+        PsdFile psd = new PsdFile(width, height);
+
+        while (readLayer(in, psd)) {
+        }
+
+        return psd;
+    }
+
+    private static boolean readLayer(DataInputStream in, PsdFile psd) {
+        try {
+            if (in.read() == 2) {
+                return false;
+            }
+            String name = in.readUTF();
+            boolean visible = in.read() == 1;
+            int x = in.readInt();
+            int y = in.readInt();
+            int dataSize = in.readInt();
+
+            byte[] data = new byte[dataSize];
+            int read = 0;
+            while (read < dataSize) {
+                read += in.read(data, read, dataSize - read);
+            }
+
+            ByteArrayInputStream arrayIn = new ByteArrayInputStream(data);
+            BufferedImage chunk = ImageIO.read(arrayIn);
+
+            // Ensure the image is in the right format
+            BufferedImage image =
+                    new BufferedImage(chunk.getWidth(), chunk.getHeight(),
+                            BufferedImage.TYPE_INT_ARGB);
+            Graphics2D g = image.createGraphics();
+            g.drawImage(chunk, null, 0, 0);
+            g.dispose();
+
+            psd.addLayer(name, image, new Point(x, y), visible);
+
+            return true;
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    public static void invalidateView(ViewNode viewNode) {
+        DeviceConnection connection = null;
+        try {
+            connection = new DeviceConnection(viewNode.window.getDevice());
+            connection.sendCommand("INVALIDATE " + viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$
+        } catch (Exception e) {
+            Log.e(TAG, "Unable to invalidate view " + viewNode + " in window " + viewNode.window
+                    + " on device " + viewNode.window.getDevice());
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+        }
+    }
+
+    public static void requestLayout(ViewNode viewNode) {
+        DeviceConnection connection = null;
+        try {
+            connection = new DeviceConnection(viewNode.window.getDevice());
+            connection.sendCommand("REQUEST_LAYOUT " + viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$
+        } catch (Exception e) {
+            Log.e(TAG, "Unable to request layout for node " + viewNode + " in window "
+                    + viewNode.window + " on device " + viewNode.window.getDevice());
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+        }
+    }
+
+    public static void outputDisplayList(ViewNode viewNode) {
+        DeviceConnection connection = null;
+        try {
+            connection = new DeviceConnection(viewNode.window.getDevice());
+            connection.sendCommand("OUTPUT_DISPLAYLIST " +
+                    viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$
+        } catch (Exception e) {
+            Log.e(TAG, "Unable to dump displaylist for node " + viewNode + " in window "
+                    + viewNode.window + " on device " + viewNode.window.getDevice());
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+        }
+    }
+
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DeviceConnection.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DeviceConnection.java
new file mode 100644
index 0000000..f750d5c
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DeviceConnection.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.device;
+
+import com.android.ddmlib.IDevice;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.channels.SocketChannel;
+
+/**
+ * This class is used for connecting to a device in debug mode running the view
+ * server.
+ */
+public class DeviceConnection {
+
+    // Now a socket channel, since socket channels are friendly with interrupts.
+    private SocketChannel mSocketChannel;
+
+    private BufferedReader mIn;
+
+    private BufferedWriter mOut;
+
+    public DeviceConnection(IDevice device) throws IOException {
+        mSocketChannel = SocketChannel.open();
+        int port = DeviceBridge.getDeviceLocalPort(device);
+
+        if (port == -1) {
+            throw new IOException();
+        }
+
+        mSocketChannel.connect(new InetSocketAddress("127.0.0.1", port)); //$NON-NLS-1$
+        mSocketChannel.socket().setSoTimeout(40000);
+    }
+
+    public BufferedReader getInputStream() throws IOException {
+        if (mIn == null) {
+            mIn = new BufferedReader(new InputStreamReader(mSocketChannel.socket().getInputStream()));
+        }
+        return mIn;
+    }
+
+    public BufferedWriter getOutputStream() throws IOException {
+        if (mOut == null) {
+            mOut =
+                    new BufferedWriter(new OutputStreamWriter(mSocketChannel.socket()
+                            .getOutputStream()));
+        }
+        return mOut;
+    }
+
+    public Socket getSocket() {
+        return mSocketChannel.socket();
+    }
+
+    public void sendCommand(String command) throws IOException {
+        BufferedWriter out = getOutputStream();
+        out.write(command);
+        out.newLine();
+        out.flush();
+    }
+
+    public void close() {
+        try {
+            if (mIn != null) {
+                mIn.close();
+            }
+        } catch (IOException e) {
+        }
+        try {
+            if (mOut != null) {
+                mOut.close();
+            }
+        } catch (IOException e) {
+        }
+        try {
+            mSocketChannel.close();
+        } catch (IOException e) {
+        }
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/HvDeviceFactory.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/HvDeviceFactory.java
new file mode 100644
index 0000000..81f567b
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/HvDeviceFactory.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2013 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.hierarchyviewerlib.device;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData;
+import com.android.ddmlib.IDevice;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+
+public class HvDeviceFactory {
+    public static IHvDevice create(IDevice device) {
+        // default to old mechanism until the new one is fully tested
+        if (!HierarchyViewerDirector.isUsingDdmProtocol()) {
+            return new ViewServerDevice(device);
+        }
+
+        // Wait for a few seconds after the device has been connected to
+        // allow all the clients to be initialized. Specifically, we need to wait
+        // until the client data is filled with the list of features supported
+        // by the client.
+        try {
+            Thread.sleep(2000);
+        } catch (InterruptedException e) {
+            // ignore
+        }
+
+        boolean ddmViewHierarchy = false;
+
+        // see if any of the clients on the device support view hierarchy via DDMS
+        for (Client c : device.getClients()) {
+            ClientData cd = c.getClientData();
+            if (cd != null && cd.hasFeature(ClientData.FEATURE_VIEW_HIERARCHY)) {
+                ddmViewHierarchy = true;
+                break;
+            }
+        }
+
+        return ddmViewHierarchy ? new DdmViewDebugDevice(device) : new ViewServerDevice(device);
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/IHvDevice.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/IHvDevice.java
new file mode 100644
index 0000000..6f1fd37
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/IHvDevice.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2013 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.hierarchyviewerlib.device;
+
+import com.android.ddmlib.IDevice;
+import com.android.hierarchyviewerlib.device.WindowUpdater.IWindowChangeListener;
+import com.android.hierarchyviewerlib.models.ViewNode;
+import com.android.hierarchyviewerlib.models.Window;
+import com.android.hierarchyviewerlib.ui.util.PsdFile;
+
+import org.eclipse.swt.graphics.Image;
+
+import java.util.List;
+
+/** Represents a device that can perform view debug operations. */
+public interface IHvDevice {
+    /**
+     * Initializes view debugging on the device.
+     * @return true if the on device component was successfully initialized
+     */
+    boolean initializeViewDebug();
+    boolean reloadWindows();
+
+    void terminateViewDebug();
+    boolean isViewDebugEnabled();
+    boolean supportsDisplayListDump();
+
+    Window[] getWindows();
+    int getFocusedWindow();
+
+    IDevice getDevice();
+
+    Image getScreenshotImage();
+    ViewNode loadWindowData(Window window);
+    void loadProfileData(Window window, ViewNode viewNode);
+    Image loadCapture(Window window, ViewNode viewNode);
+    PsdFile captureLayers(Window window);
+    void invalidateView(ViewNode viewNode);
+    void requestLayout(ViewNode viewNode);
+    void outputDisplayList(ViewNode viewNode);
+
+    boolean isViewUpdateEnabled();
+    void invokeViewMethod(Window window, ViewNode viewNode, String method, List<?> args);
+    boolean setLayoutParameter(Window window, ViewNode viewNode, String property, int value);
+
+    void addWindowChangeListener(IWindowChangeListener l);
+    void removeWindowChangeListener(IWindowChangeListener l);
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/ViewServerDevice.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/ViewServerDevice.java
new file mode 100644
index 0000000..4445e9a
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/ViewServerDevice.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2013 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.hierarchyviewerlib.device;
+
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log;
+import com.android.hierarchyviewerlib.device.DeviceBridge.ViewServerInfo;
+import com.android.hierarchyviewerlib.device.WindowUpdater.IWindowChangeListener;
+import com.android.hierarchyviewerlib.models.ViewNode;
+import com.android.hierarchyviewerlib.models.Window;
+import com.android.hierarchyviewerlib.ui.util.PsdFile;
+
+import org.eclipse.swt.graphics.Image;
+
+import java.util.List;
+
+public class ViewServerDevice extends AbstractHvDevice {
+    static final String TAG = "ViewServerDevice";
+
+    final IDevice mDevice;
+    private ViewServerInfo mViewServerInfo;
+    private Window[] mWindows;
+
+    public ViewServerDevice(IDevice device) {
+        mDevice = device;
+    }
+
+    @Override
+    public boolean initializeViewDebug() {
+        if (!mDevice.isOnline()) {
+            return false;
+        }
+
+        DeviceBridge.setupDeviceForward(mDevice);
+
+        return reloadWindows();
+    }
+
+    @Override
+    public boolean reloadWindows() {
+        if (!DeviceBridge.isViewServerRunning(mDevice)) {
+            if (!DeviceBridge.startViewServer(mDevice)) {
+                Log.e(TAG, "Unable to debug device: " + mDevice.getName());
+                DeviceBridge.removeDeviceForward(mDevice);
+                return false;
+            }
+        }
+
+        mViewServerInfo = DeviceBridge.loadViewServerInfo(mDevice);
+        if (mViewServerInfo == null) {
+            return false;
+        }
+
+        mWindows = DeviceBridge.loadWindows(this, mDevice);
+        return true;
+    }
+
+    @Override
+    public boolean supportsDisplayListDump() {
+        return mViewServerInfo != null && mViewServerInfo.protocolVersion >= 4;
+    }
+
+    @Override
+    public void terminateViewDebug() {
+        DeviceBridge.removeDeviceForward(mDevice);
+        DeviceBridge.removeViewServerInfo(mDevice);
+    }
+
+    @Override
+    public boolean isViewDebugEnabled() {
+        return mViewServerInfo != null;
+    }
+
+    @Override
+    public Window[] getWindows() {
+        return mWindows;
+    }
+
+    @Override
+    public int getFocusedWindow() {
+        return DeviceBridge.getFocusedWindow(mDevice);
+    }
+
+    @Override
+    public IDevice getDevice() {
+        return mDevice;
+    }
+
+    @Override
+    public ViewNode loadWindowData(Window window) {
+        return DeviceBridge.loadWindowData(window);
+    }
+
+    @Override
+    public void loadProfileData(Window window, ViewNode viewNode) {
+        DeviceBridge.loadProfileData(window, viewNode);
+    }
+
+    @Override
+    public Image loadCapture(Window window, ViewNode viewNode) {
+        return DeviceBridge.loadCapture(window, viewNode);
+    }
+
+    @Override
+    public PsdFile captureLayers(Window window) {
+        return DeviceBridge.captureLayers(window);
+    }
+
+    @Override
+    public void invalidateView(ViewNode viewNode) {
+        DeviceBridge.invalidateView(viewNode);
+    }
+
+    @Override
+    public void requestLayout(ViewNode viewNode) {
+        DeviceBridge.requestLayout(viewNode);
+    }
+
+    @Override
+    public void outputDisplayList(ViewNode viewNode) {
+        DeviceBridge.outputDisplayList(viewNode);
+    }
+
+    @Override
+    public void addWindowChangeListener(IWindowChangeListener l) {
+        if (mViewServerInfo != null && mViewServerInfo.protocolVersion >= 3) {
+            WindowUpdater.startListenForWindowChanges(l, mDevice);
+        }
+    }
+
+    @Override
+    public void removeWindowChangeListener(IWindowChangeListener l) {
+        if (mViewServerInfo != null && mViewServerInfo.protocolVersion >= 3) {
+            WindowUpdater.stopListenForWindowChanges(l, mDevice);
+        }
+    }
+
+    @Override
+    public boolean isViewUpdateEnabled() {
+        return false;
+    }
+
+    @Override
+    public void invokeViewMethod(Window window, ViewNode viewNode, String method,
+            List<?> args) {
+        // not supported
+    }
+
+    @Override
+    public boolean setLayoutParameter(Window window, ViewNode viewNode, String property,
+            int value) {
+        // not supported
+        return false;
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/WindowUpdater.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/WindowUpdater.java
new file mode 100644
index 0000000..a67d400
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/WindowUpdater.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.device;
+
+import com.android.ddmlib.IDevice;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * This class handles automatic updating of the list of windows in the device
+ * selector for device with protocol version 3 or above of the view server. It
+ * connects to the devices, keeps the connection open and listens for messages.
+ * It notifies all it's listeners of changes.
+ */
+public class WindowUpdater {
+    private static HashMap<IDevice, ArrayList<IWindowChangeListener>> sWindowChangeListeners =
+            new HashMap<IDevice, ArrayList<IWindowChangeListener>>();
+
+    private static HashMap<IDevice, Thread> sListeningThreads = new HashMap<IDevice, Thread>();
+
+    public static interface IWindowChangeListener {
+        public void windowsChanged(IDevice device);
+
+        public void focusChanged(IDevice device);
+    }
+
+    public static void terminate() {
+        synchronized (sListeningThreads) {
+            for (IDevice device : sListeningThreads.keySet()) {
+                sListeningThreads.get(device).interrupt();
+
+            }
+        }
+    }
+
+    public static void startListenForWindowChanges(IWindowChangeListener listener, IDevice device) {
+        synchronized (sWindowChangeListeners) {
+            // In this case, a listening thread already exists, so we don't need
+            // to create another one.
+            if (sWindowChangeListeners.containsKey(device)) {
+                sWindowChangeListeners.get(device).add(listener);
+                return;
+            }
+            ArrayList<IWindowChangeListener> listeners = new ArrayList<IWindowChangeListener>();
+            listeners.add(listener);
+            sWindowChangeListeners.put(device, listeners);
+        }
+        // Start listening
+        Thread listeningThread = new Thread(new WindowChangeMonitor(device));
+        synchronized (sListeningThreads) {
+            sListeningThreads.put(device, listeningThread);
+        }
+        listeningThread.start();
+    }
+
+    public static void stopListenForWindowChanges(IWindowChangeListener listener, IDevice device) {
+        synchronized (sWindowChangeListeners) {
+            ArrayList<IWindowChangeListener> listeners = sWindowChangeListeners.get(device);
+            if (listeners == null) {
+                return;
+            }
+            listeners.remove(listener);
+            // There are more listeners, so don't stop the listening thread.
+            if (listeners.size() != 0) {
+                return;
+            }
+            sWindowChangeListeners.remove(device);
+        }
+        // Everybody left, so the party's over!
+        Thread listeningThread;
+        synchronized (sListeningThreads) {
+            listeningThread = sListeningThreads.get(device);
+            sListeningThreads.remove(device);
+        }
+        listeningThread.interrupt();
+    }
+
+    private static IWindowChangeListener[] getWindowChangeListenersAsArray(IDevice device) {
+        IWindowChangeListener[] listeners;
+        synchronized (sWindowChangeListeners) {
+            ArrayList<IWindowChangeListener> windowChangeListenerList =
+                    sWindowChangeListeners.get(device);
+            if (windowChangeListenerList == null) {
+                return null;
+            }
+            listeners =
+                    windowChangeListenerList
+                            .toArray(new IWindowChangeListener[windowChangeListenerList.size()]);
+        }
+        return listeners;
+    }
+
+    public static void notifyWindowsChanged(IDevice device) {
+        IWindowChangeListener[] listeners = getWindowChangeListenersAsArray(device);
+        if (listeners != null) {
+            for (int i = 0; i < listeners.length; i++) {
+                listeners[i].windowsChanged(device);
+            }
+        }
+    }
+
+    public static void notifyFocusChanged(IDevice device) {
+        IWindowChangeListener[] listeners = getWindowChangeListenersAsArray(device);
+        if (listeners != null) {
+            for (int i = 0; i < listeners.length; i++) {
+                listeners[i].focusChanged(device);
+            }
+        }
+    }
+
+    private static class WindowChangeMonitor implements Runnable {
+        private IDevice device;
+
+        public WindowChangeMonitor(IDevice device) {
+            this.device = device;
+        }
+
+        @Override
+        public void run() {
+            while (!Thread.currentThread().isInterrupted()) {
+                DeviceConnection connection = null;
+                try {
+                    connection = new DeviceConnection(device);
+                    connection.sendCommand("AUTOLIST");
+                    String line;
+                    while (!Thread.currentThread().isInterrupted()
+                            && (line = connection.getInputStream().readLine()) != null) {
+                        if (line.equalsIgnoreCase("LIST UPDATE")) {
+                            notifyWindowsChanged(device);
+                        } else if (line.equalsIgnoreCase("FOCUS UPDATE")) {
+                            notifyFocusChanged(device);
+                        }
+                    }
+
+                } catch (IOException e) {
+                } finally {
+                    if (connection != null) {
+                        connection.close();
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/DeviceSelectionModel.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/DeviceSelectionModel.java
new file mode 100644
index 0000000..9ac9b40
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/DeviceSelectionModel.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.models;
+
+import com.android.hierarchyviewerlib.device.IHvDevice;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * This class stores the list of windows for each connected device. It notifies
+ * listeners of any changes as well as knows which window is currently selected
+ * in the device selector.
+ */
+public class DeviceSelectionModel {
+    private final Map<IHvDevice, DeviceInfo> mDeviceMap = new HashMap<IHvDevice, DeviceInfo>(10);
+    private final Map<IHvDevice, Integer> mFocusedWindowHashes =
+            new HashMap<IHvDevice, Integer>(20);
+
+    private final ArrayList<IWindowChangeListener> mWindowChangeListeners =
+            new ArrayList<IWindowChangeListener>();
+
+    private IHvDevice mSelectedDevice;
+
+    private Window mSelectedWindow;
+
+    private static DeviceSelectionModel sModel;
+
+    private static class DeviceInfo {
+        Window[] windows;
+
+        private DeviceInfo(Window[] windows) {
+            this.windows = windows;
+        }
+    }
+    public static DeviceSelectionModel getModel() {
+        if (sModel == null) {
+            sModel = new DeviceSelectionModel();
+        }
+        return sModel;
+    }
+
+    public void addDevice(IHvDevice hvDevice) {
+        synchronized (mDeviceMap) {
+            DeviceInfo info = new DeviceInfo(hvDevice.getWindows());
+            mDeviceMap.put(hvDevice, info);
+        }
+
+        notifyDeviceConnected(hvDevice);
+    }
+
+    public void removeDevice(IHvDevice hvDevice) {
+        boolean selectionChanged = false;
+        synchronized (mDeviceMap) {
+            mDeviceMap.remove(hvDevice);
+            mFocusedWindowHashes.remove(hvDevice);
+            if (mSelectedDevice == hvDevice) {
+                mSelectedDevice = null;
+                mSelectedWindow = null;
+                selectionChanged = true;
+            }
+        }
+        notifyDeviceDisconnected(hvDevice);
+        if (selectionChanged) {
+            notifySelectionChanged(mSelectedDevice, mSelectedWindow);
+        }
+    }
+
+    public void updateDevice(IHvDevice hvDevice) {
+        boolean selectionChanged = false;
+        synchronized (mDeviceMap) {
+            Window[] windows = hvDevice.getWindows();
+            mDeviceMap.put(hvDevice, new DeviceInfo(windows));
+
+            // If the selected window no longer exists, we clear the selection.
+            if (mSelectedDevice == hvDevice && mSelectedWindow != null) {
+                boolean windowStillExists = false;
+                for (int i = 0; i < windows.length && !windowStillExists; i++) {
+                    if (windows[i].equals(mSelectedWindow)) {
+                        windowStillExists = true;
+                    }
+                }
+                if (!windowStillExists) {
+                    mSelectedDevice = null;
+                    mSelectedWindow = null;
+                    selectionChanged = true;
+                }
+            }
+        }
+
+        notifyDeviceChanged(hvDevice);
+        if (selectionChanged) {
+            notifySelectionChanged(mSelectedDevice, mSelectedWindow);
+        }
+    }
+
+    /*
+     * Change which window has focus and notify the listeners.
+     */
+    public void updateFocusedWindow(IHvDevice device, int focusedWindow) {
+        Integer oldValue = null;
+        synchronized (mDeviceMap) {
+            oldValue = mFocusedWindowHashes.put(device, new Integer(focusedWindow));
+        }
+        // Only notify if the values are different. It would be cool if Java
+        // containers accepted basic types like int.
+        if (oldValue == null || (oldValue != null && oldValue.intValue() != focusedWindow)) {
+            notifyFocusChanged(device);
+        }
+    }
+
+    public static interface IWindowChangeListener {
+        public void deviceConnected(IHvDevice device);
+
+        public void deviceChanged(IHvDevice device);
+
+        public void deviceDisconnected(IHvDevice device);
+
+        public void focusChanged(IHvDevice device);
+
+        public void selectionChanged(IHvDevice device, Window window);
+    }
+
+    private IWindowChangeListener[] getWindowChangeListenerList() {
+        IWindowChangeListener[] listeners = null;
+        synchronized (mWindowChangeListeners) {
+            if (mWindowChangeListeners.size() == 0) {
+                return null;
+            }
+            listeners =
+                    mWindowChangeListeners.toArray(new IWindowChangeListener[mWindowChangeListeners
+                            .size()]);
+        }
+        return listeners;
+    }
+
+    private void notifyDeviceConnected(IHvDevice device) {
+        IWindowChangeListener[] listeners = getWindowChangeListenerList();
+        if (listeners != null) {
+            for (int i = 0; i < listeners.length; i++) {
+                listeners[i].deviceConnected(device);
+            }
+        }
+    }
+
+    private void notifyDeviceChanged(IHvDevice device) {
+        IWindowChangeListener[] listeners = getWindowChangeListenerList();
+        if (listeners != null) {
+            for (int i = 0; i < listeners.length; i++) {
+                listeners[i].deviceChanged(device);
+            }
+        }
+    }
+
+    private void notifyDeviceDisconnected(IHvDevice device) {
+        IWindowChangeListener[] listeners = getWindowChangeListenerList();
+        if (listeners != null) {
+            for (int i = 0; i < listeners.length; i++) {
+                listeners[i].deviceDisconnected(device);
+            }
+        }
+    }
+
+    private void notifyFocusChanged(IHvDevice device) {
+        IWindowChangeListener[] listeners = getWindowChangeListenerList();
+        if (listeners != null) {
+            for (int i = 0; i < listeners.length; i++) {
+                listeners[i].focusChanged(device);
+            }
+        }
+    }
+
+    private void notifySelectionChanged(IHvDevice device, Window window) {
+        IWindowChangeListener[] listeners = getWindowChangeListenerList();
+        if (listeners != null) {
+            for (int i = 0; i < listeners.length; i++) {
+                listeners[i].selectionChanged(device, window);
+            }
+        }
+    }
+
+    public void addWindowChangeListener(IWindowChangeListener listener) {
+        synchronized (mWindowChangeListeners) {
+            mWindowChangeListeners.add(listener);
+        }
+    }
+
+    public void removeWindowChangeListener(IWindowChangeListener listener) {
+        synchronized (mWindowChangeListeners) {
+            mWindowChangeListeners.remove(listener);
+        }
+    }
+
+    public IHvDevice[] getDevices() {
+        synchronized (mDeviceMap) {
+            Set<IHvDevice> devices = mDeviceMap.keySet();
+            return devices.toArray(new IHvDevice[devices.size()]);
+        }
+    }
+
+    public Window[] getWindows(IHvDevice device) {
+        synchronized (mDeviceMap) {
+            DeviceInfo info = mDeviceMap.get(device);
+            if (info != null) {
+                return info.windows;
+            }
+        }
+
+        return null;
+    }
+
+    // Returns the window that currently has focus or -1. Note that this means
+    // that a window with hashcode -1 gets highlighted. If you remember, this is
+    // the infamous <Focused Window>
+    public int getFocusedWindow(IHvDevice device) {
+        synchronized (mDeviceMap) {
+            Integer focusedWindow = mFocusedWindowHashes.get(device);
+            if (focusedWindow == null) {
+                return -1;
+            }
+            return focusedWindow.intValue();
+        }
+    }
+
+    public void setSelection(IHvDevice device, Window window) {
+        synchronized (mDeviceMap) {
+            mSelectedDevice = device;
+            mSelectedWindow = window;
+        }
+        notifySelectionChanged(device, window);
+    }
+
+    public IHvDevice getSelectedDevice() {
+        synchronized (mDeviceMap) {
+            return mSelectedDevice;
+        }
+    }
+
+    public Window getSelectedWindow() {
+        synchronized (mDeviceMap) {
+            return mSelectedWindow;
+        }
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/PixelPerfectModel.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/PixelPerfectModel.java
new file mode 100644
index 0000000..a425b47
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/PixelPerfectModel.java
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.models;
+
+import com.android.ddmlib.IDevice;
+
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Display;
+
+import java.util.ArrayList;
+
+public class PixelPerfectModel {
+
+    public static final int MIN_ZOOM = 2;
+
+    public static final int MAX_ZOOM = 24;
+
+    public static final int DEFAULT_ZOOM = 8;
+
+    public static final int DEFAULT_OVERLAY_TRANSPARENCY_PERCENTAGE = 50;
+
+    private IDevice mDevice;
+
+    private Image mImage;
+
+    private Point mCrosshairLocation;
+
+    private ViewNode mViewNode;
+
+    private ViewNode mSelectedNode;
+
+    private int mZoom;
+
+    private final ArrayList<IImageChangeListener> mImageChangeListeners =
+            new ArrayList<IImageChangeListener>();
+
+    private Image mOverlayImage;
+
+    private double mOverlayTransparency = DEFAULT_OVERLAY_TRANSPARENCY_PERCENTAGE / 100.0;
+
+    private static PixelPerfectModel sModel;
+
+    public static PixelPerfectModel getModel() {
+        if (sModel == null) {
+            sModel = new PixelPerfectModel();
+        }
+        return sModel;
+    }
+
+    public void setData(final IDevice device, final Image image, final ViewNode viewNode) {
+        final Image toDispose = this.mImage;
+        final Image toDispose2 = this.mOverlayImage;
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                synchronized (PixelPerfectModel.this) {
+                    PixelPerfectModel.this.mDevice = device;
+                    PixelPerfectModel.this.mImage = image;
+                    PixelPerfectModel.this.mViewNode = viewNode;
+                    if (image != null) {
+                        PixelPerfectModel.this.mCrosshairLocation =
+                                new Point(image.getBounds().width / 2, image.getBounds().height / 2);
+                    } else {
+                        PixelPerfectModel.this.mCrosshairLocation = null;
+                    }
+                    mOverlayImage = null;
+                    PixelPerfectModel.this.mSelectedNode = null;
+                    mZoom = DEFAULT_ZOOM;
+                }
+            }
+        });
+        notifyImageLoaded();
+        if (toDispose != null) {
+            Display.getDefault().syncExec(new Runnable() {
+                @Override
+                public void run() {
+                    toDispose.dispose();
+                }
+            });
+        }
+        if (toDispose2 != null) {
+            Display.getDefault().syncExec(new Runnable() {
+                @Override
+                public void run() {
+                    toDispose2.dispose();
+                }
+            });
+        }
+
+    }
+
+    public void setCrosshairLocation(int x, int y) {
+        synchronized (this) {
+            mCrosshairLocation = new Point(x, y);
+        }
+        notifyCrosshairMoved();
+    }
+
+    public void setSelected(ViewNode selected) {
+        synchronized (this) {
+            this.mSelectedNode = selected;
+        }
+        notifySelectionChanged();
+    }
+
+    public void setTree(final ViewNode viewNode) {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                synchronized (PixelPerfectModel.this) {
+                    PixelPerfectModel.this.mViewNode = viewNode;
+                    PixelPerfectModel.this.mSelectedNode = null;
+                }
+            }
+        });
+        notifyTreeChanged();
+    }
+
+    public void setImage(final Image image) {
+        final Image toDispose = this.mImage;
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                synchronized (PixelPerfectModel.this) {
+                    PixelPerfectModel.this.mImage = image;
+                }
+            }
+        });
+        notifyImageChanged();
+        if (toDispose != null) {
+            Display.getDefault().syncExec(new Runnable() {
+                @Override
+                public void run() {
+                    toDispose.dispose();
+                }
+            });
+        }
+    }
+
+    public void setZoom(int newZoom) {
+        synchronized (this) {
+            if (newZoom < MIN_ZOOM) {
+                newZoom = MIN_ZOOM;
+            }
+            if (newZoom > MAX_ZOOM) {
+                newZoom = MAX_ZOOM;
+            }
+            mZoom = newZoom;
+        }
+        notifyZoomChanged();
+    }
+
+    public void setOverlayImage(final Image overlayImage) {
+        final Image toDispose = this.mOverlayImage;
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                synchronized (PixelPerfectModel.this) {
+                    PixelPerfectModel.this.mOverlayImage = overlayImage;
+                }
+            }
+        });
+        notifyOverlayChanged();
+        if (toDispose != null) {
+            Display.getDefault().syncExec(new Runnable() {
+                @Override
+                public void run() {
+                    toDispose.dispose();
+                }
+            });
+        }
+    }
+
+    public void setOverlayTransparency(double value) {
+        synchronized (this) {
+            value = Math.max(value, 0);
+            value = Math.min(value, 1);
+            mOverlayTransparency = value;
+        }
+        notifyOverlayTransparencyChanged();
+    }
+
+    public ViewNode getViewNode() {
+        synchronized (this) {
+            return mViewNode;
+        }
+    }
+
+    public Point getCrosshairLocation() {
+        synchronized (this) {
+            return mCrosshairLocation;
+        }
+    }
+
+    public Image getImage() {
+        synchronized (this) {
+            return mImage;
+        }
+    }
+
+    public ViewNode getSelected() {
+        synchronized (this) {
+            return mSelectedNode;
+        }
+    }
+
+    public IDevice getDevice() {
+        synchronized (this) {
+            return mDevice;
+        }
+    }
+
+    public int getZoom() {
+        synchronized (this) {
+            return mZoom;
+        }
+    }
+
+    public Image getOverlayImage() {
+        synchronized (this) {
+            return mOverlayImage;
+        }
+    }
+
+    public double getOverlayTransparency() {
+        synchronized (this) {
+            return mOverlayTransparency;
+        }
+    }
+
+    public static interface IImageChangeListener {
+        public void imageLoaded();
+
+        public void imageChanged();
+
+        public void crosshairMoved();
+
+        public void selectionChanged();
+
+        public void treeChanged();
+
+        public void zoomChanged();
+
+        public void overlayChanged();
+
+        public void overlayTransparencyChanged();
+    }
+
+    private IImageChangeListener[] getImageChangeListenerList() {
+        IImageChangeListener[] listeners = null;
+        synchronized (mImageChangeListeners) {
+            if (mImageChangeListeners.size() == 0) {
+                return null;
+            }
+            listeners =
+                    mImageChangeListeners.toArray(new IImageChangeListener[mImageChangeListeners
+                            .size()]);
+        }
+        return listeners;
+    }
+
+    public void notifyImageLoaded() {
+        IImageChangeListener[] listeners = getImageChangeListenerList();
+        if (listeners != null) {
+            for (int i = 0; i < listeners.length; i++) {
+                listeners[i].imageLoaded();
+            }
+        }
+    }
+
+    public void notifyImageChanged() {
+        IImageChangeListener[] listeners = getImageChangeListenerList();
+        if (listeners != null) {
+            for (int i = 0; i < listeners.length; i++) {
+                listeners[i].imageChanged();
+            }
+        }
+    }
+
+    public void notifyCrosshairMoved() {
+        IImageChangeListener[] listeners = getImageChangeListenerList();
+        if (listeners != null) {
+            for (int i = 0; i < listeners.length; i++) {
+                listeners[i].crosshairMoved();
+            }
+        }
+    }
+
+    public void notifySelectionChanged() {
+        IImageChangeListener[] listeners = getImageChangeListenerList();
+        if (listeners != null) {
+            for (int i = 0; i < listeners.length; i++) {
+                listeners[i].selectionChanged();
+            }
+        }
+    }
+
+    public void notifyTreeChanged() {
+        IImageChangeListener[] listeners = getImageChangeListenerList();
+        if (listeners != null) {
+            for (int i = 0; i < listeners.length; i++) {
+                listeners[i].treeChanged();
+            }
+        }
+    }
+
+    public void notifyZoomChanged() {
+        IImageChangeListener[] listeners = getImageChangeListenerList();
+        if (listeners != null) {
+            for (int i = 0; i < listeners.length; i++) {
+                listeners[i].zoomChanged();
+            }
+        }
+    }
+
+    public void notifyOverlayChanged() {
+        IImageChangeListener[] listeners = getImageChangeListenerList();
+        if (listeners != null) {
+            for (int i = 0; i < listeners.length; i++) {
+                listeners[i].overlayChanged();
+            }
+        }
+    }
+
+    public void notifyOverlayTransparencyChanged() {
+        IImageChangeListener[] listeners = getImageChangeListenerList();
+        if (listeners != null) {
+            for (int i = 0; i < listeners.length; i++) {
+                listeners[i].overlayTransparencyChanged();
+            }
+        }
+    }
+
+    public void addImageChangeListener(IImageChangeListener listener) {
+        synchronized (mImageChangeListeners) {
+            mImageChangeListeners.add(listener);
+        }
+    }
+
+    public void removeImageChangeListener(IImageChangeListener listener) {
+        synchronized (mImageChangeListeners) {
+            mImageChangeListeners.remove(listener);
+        }
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/TreeViewModel.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/TreeViewModel.java
new file mode 100644
index 0000000..6dac1e6
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/TreeViewModel.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.models;
+
+import com.android.hierarchyviewerlib.ui.util.DrawableViewNode;
+import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Point;
+import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Rectangle;
+
+import java.util.ArrayList;
+
+public class TreeViewModel {
+    public static final double MAX_ZOOM = 2;
+
+    public static final double MIN_ZOOM = 0.2;
+
+    private Window mWindow;
+
+    private DrawableViewNode mTree;
+
+    private DrawableViewNode mSelectedNode;
+
+    private Rectangle mViewport;
+
+    private double mZoom;
+
+    private final ArrayList<ITreeChangeListener> mTreeChangeListeners =
+            new ArrayList<ITreeChangeListener>();
+
+    private static TreeViewModel sModel;
+
+    public static TreeViewModel getModel() {
+        if (sModel == null) {
+            sModel = new TreeViewModel();
+        }
+        return sModel;
+    }
+
+    public void setData(Window window, ViewNode viewNode) {
+        synchronized (this) {
+            if (mTree != null) {
+                mTree.viewNode.dispose();
+            }
+            this.mWindow = window;
+            if (viewNode == null) {
+                mTree = null;
+            } else {
+                mTree = new DrawableViewNode(viewNode);
+                mTree.setLeft();
+                mTree.placeRoot();
+            }
+            mViewport = null;
+            mZoom = 1;
+            mSelectedNode = null;
+        }
+        notifyTreeChanged();
+    }
+
+    public void setSelection(DrawableViewNode selectedNode) {
+        synchronized (this) {
+            this.mSelectedNode = selectedNode;
+        }
+        notifySelectionChanged();
+    }
+
+    public void setViewport(Rectangle viewport) {
+        synchronized (this) {
+            this.mViewport = viewport;
+        }
+        notifyViewportChanged();
+    }
+
+    public void setZoom(double newZoom) {
+        Point zoomPoint = null;
+        synchronized (this) {
+            if (mTree != null && mViewport != null) {
+                zoomPoint =
+                        new Point(mViewport.x + mViewport.width / 2, mViewport.y + mViewport.height / 2);
+            }
+        }
+        zoomOnPoint(newZoom, zoomPoint);
+    }
+
+    public void zoomOnPoint(double newZoom, Point zoomPoint) {
+        synchronized (this) {
+            if (mTree != null && this.mViewport != null) {
+                if (newZoom < MIN_ZOOM) {
+                    newZoom = MIN_ZOOM;
+                }
+                if (newZoom > MAX_ZOOM) {
+                    newZoom = MAX_ZOOM;
+                }
+                mViewport.x = zoomPoint.x - (zoomPoint.x - mViewport.x) * mZoom / newZoom;
+                mViewport.y = zoomPoint.y - (zoomPoint.y - mViewport.y) * mZoom / newZoom;
+                mViewport.width = mViewport.width * mZoom / newZoom;
+                mViewport.height = mViewport.height * mZoom / newZoom;
+                mZoom = newZoom;
+            }
+        }
+        notifyZoomChanged();
+    }
+
+    public DrawableViewNode getTree() {
+        synchronized (this) {
+            return mTree;
+        }
+    }
+
+    public Window getWindow() {
+        synchronized (this) {
+            return mWindow;
+        }
+    }
+
+    public Rectangle getViewport() {
+        synchronized (this) {
+            return mViewport;
+        }
+    }
+
+    public double getZoom() {
+        synchronized (this) {
+            return mZoom;
+        }
+    }
+
+    public DrawableViewNode getSelection() {
+        synchronized (this) {
+            return mSelectedNode;
+        }
+    }
+
+    public static interface ITreeChangeListener {
+        public void treeChanged();
+
+        public void selectionChanged();
+
+        public void viewportChanged();
+
+        public void zoomChanged();
+    }
+
+    private ITreeChangeListener[] getTreeChangeListenerList() {
+        ITreeChangeListener[] listeners = null;
+        synchronized (mTreeChangeListeners) {
+            if (mTreeChangeListeners.size() == 0) {
+                return null;
+            }
+            listeners =
+                    mTreeChangeListeners.toArray(new ITreeChangeListener[mTreeChangeListeners.size()]);
+        }
+        return listeners;
+    }
+
+    public void notifyTreeChanged() {
+        ITreeChangeListener[] listeners = getTreeChangeListenerList();
+        if (listeners != null) {
+            for (int i = 0; i < listeners.length; i++) {
+                listeners[i].treeChanged();
+            }
+        }
+    }
+
+    public void notifySelectionChanged() {
+        ITreeChangeListener[] listeners = getTreeChangeListenerList();
+        if (listeners != null) {
+            for (int i = 0; i < listeners.length; i++) {
+                listeners[i].selectionChanged();
+            }
+        }
+    }
+
+    public void notifyViewportChanged() {
+        ITreeChangeListener[] listeners = getTreeChangeListenerList();
+        if (listeners != null) {
+            for (int i = 0; i < listeners.length; i++) {
+                listeners[i].viewportChanged();
+            }
+        }
+    }
+
+    public void notifyZoomChanged() {
+        ITreeChangeListener[] listeners = getTreeChangeListenerList();
+        if (listeners != null) {
+            for (int i = 0; i < listeners.length; i++) {
+                listeners[i].zoomChanged();
+            }
+        }
+    }
+
+    public void addTreeChangeListener(ITreeChangeListener listener) {
+        synchronized (mTreeChangeListeners) {
+            mTreeChangeListeners.add(listener);
+        }
+    }
+
+    public void removeTreeChangeListener(ITreeChangeListener listener) {
+        synchronized (mTreeChangeListeners) {
+            mTreeChangeListeners.remove(listener);
+        }
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/ViewNode.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/ViewNode.java
new file mode 100644
index 0000000..e38da00
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/ViewNode.java
@@ -0,0 +1,369 @@
+/*
+ * Copyright (C) 2008 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.hierarchyviewerlib.models;
+
+import org.eclipse.swt.graphics.Image;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+public class ViewNode {
+
+    public static enum ProfileRating {
+        RED, YELLOW, GREEN, NONE
+    };
+
+    private static final double RED_THRESHOLD = 0.8;
+
+    private static final double YELLOW_THRESHOLD = 0.5;
+
+    public static final String MISCELLANIOUS = "miscellaneous";
+
+    public String id;
+
+    public String name;
+
+    public String hashCode;
+
+    public List<Property> properties = new ArrayList<Property>();
+
+    public Map<String, Property> namedProperties = new HashMap<String, Property>();
+
+    public ViewNode parent;
+
+    public List<ViewNode> children = new ArrayList<ViewNode>();
+
+    public int left;
+
+    public int top;
+
+    public int width;
+
+    public int height;
+
+    public int scrollX;
+
+    public int scrollY;
+
+    public int paddingLeft;
+
+    public int paddingRight;
+
+    public int paddingTop;
+
+    public int paddingBottom;
+
+    public int marginLeft;
+
+    public int marginRight;
+
+    public int marginTop;
+
+    public int marginBottom;
+
+    public int baseline;
+
+    public boolean willNotDraw;
+
+    public boolean hasMargins;
+
+    public boolean hasFocus;
+
+    public int index;
+
+    public double measureTime;
+
+    public double layoutTime;
+
+    public double drawTime;
+
+    public ProfileRating measureRating = ProfileRating.NONE;
+
+    public ProfileRating layoutRating = ProfileRating.NONE;
+
+    public ProfileRating drawRating = ProfileRating.NONE;
+
+    public Set<String> categories = new TreeSet<String>();
+
+    public Window window;
+
+    public Image image;
+
+    public int imageReferences = 1;
+
+    public int viewCount;
+
+    public boolean filtered;
+
+    public int protocolVersion;
+
+    public ViewNode(Window window, ViewNode parent, String data) {
+        this.window = window;
+        this.parent = parent;
+        index = this.parent == null ? 0 : this.parent.children.size();
+        if (this.parent != null) {
+            this.parent.children.add(this);
+        }
+        int delimIndex = data.indexOf('@');
+        if (delimIndex < 0) {
+            throw new IllegalArgumentException("Invalid format for ViewNode, missing @: " + data);
+        }
+        name = data.substring(0, delimIndex);
+        data = data.substring(delimIndex + 1);
+        delimIndex = data.indexOf(' ');
+        hashCode = data.substring(0, delimIndex);
+
+        if (data.length() > delimIndex + 1) {
+            loadProperties(data.substring(delimIndex + 1).trim());
+        } else {
+            // defaults in case properties are not available
+            id = "unknown";
+            width = height = 10;
+        }
+
+        measureTime = -1;
+        layoutTime = -1;
+        drawTime = -1;
+    }
+
+    public void dispose() {
+        final int N = children.size();
+        for (int i = 0; i < N; i++) {
+            children.get(i).dispose();
+        }
+        dereferenceImage();
+    }
+
+    public void referenceImage() {
+        imageReferences++;
+    }
+
+    public void dereferenceImage() {
+        imageReferences--;
+        if (image != null && imageReferences == 0) {
+            image.dispose();
+        }
+    }
+
+    private void loadProperties(String data) {
+        int start = 0;
+        boolean stop;
+        do {
+            int index = data.indexOf('=', start);
+            ViewNode.Property property = new ViewNode.Property();
+            property.name = data.substring(start, index);
+
+            int index2 = data.indexOf(',', index + 1);
+            int length = Integer.parseInt(data.substring(index + 1, index2));
+            start = index2 + 1 + length;
+            property.value = data.substring(index2 + 1, index2 + 1 + length);
+
+            properties.add(property);
+            namedProperties.put(property.name, property);
+
+            stop = start >= data.length();
+            if (!stop) {
+                start += 1;
+            }
+        } while (!stop);
+
+        Collections.sort(properties, new Comparator<ViewNode.Property>() {
+            @Override
+            public int compare(ViewNode.Property source, ViewNode.Property destination) {
+                return source.name.compareTo(destination.name);
+            }
+        });
+
+        id = namedProperties.get("mID").value; //$NON-NLS-1$
+
+        left =
+ namedProperties.containsKey("mLeft") ? getInt("mLeft", 0) : getInt("layout:mLeft", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+                        0);
+        top = namedProperties.containsKey("mTop") ? getInt("mTop", 0) : getInt("layout:mTop", 0); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+        width =
+                namedProperties.containsKey("getWidth()") ? getInt("getWidth()", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$
+                        "layout:getWidth()", 0); //$NON-NLS-1$
+        height =
+                namedProperties.containsKey("getHeight()") ? getInt("getHeight()", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$
+                        "layout:getHeight()", 0); //$NON-NLS-1$
+        scrollX =
+                namedProperties.containsKey("mScrollX") ? getInt("mScrollX", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$
+                        "scrolling:mScrollX", 0); //$NON-NLS-1$
+        scrollY =
+                namedProperties.containsKey("mScrollY") ? getInt("mScrollY", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$
+                        "scrolling:mScrollY", 0); //$NON-NLS-1$
+        paddingLeft =
+                namedProperties.containsKey("mPaddingLeft") ? getInt("mPaddingLeft", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$
+                        "padding:mPaddingLeft", 0); //$NON-NLS-1$
+        paddingRight =
+                namedProperties.containsKey("mPaddingRight") ? getInt("mPaddingRight", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$
+                        "padding:mPaddingRight", 0); //$NON-NLS-1$
+        paddingTop =
+                namedProperties.containsKey("mPaddingTop") ? getInt("mPaddingTop", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$
+                        "padding:mPaddingTop", 0); //$NON-NLS-1$
+        paddingBottom =
+                namedProperties.containsKey("mPaddingBottom") ? getInt("mPaddingBottom", 0) //$NON-NLS-1$ //$NON-NLS-2$
+                        : getInt("padding:mPaddingBottom", 0); //$NON-NLS-1$
+        marginLeft =
+                namedProperties.containsKey("layout_leftMargin") ? getInt("layout_leftMargin", //$NON-NLS-1$ //$NON-NLS-2$
+                        Integer.MIN_VALUE) : getInt("layout:layout_leftMargin", Integer.MIN_VALUE); //$NON-NLS-1$
+        marginRight =
+                namedProperties.containsKey("layout_rightMargin") ? getInt("layout_rightMargin", //$NON-NLS-1$ //$NON-NLS-2$
+                        Integer.MIN_VALUE) : getInt("layout:layout_rightMargin", Integer.MIN_VALUE); //$NON-NLS-1$
+        marginTop =
+                namedProperties.containsKey("layout_topMargin") ? getInt("layout_topMargin", //$NON-NLS-1$ //$NON-NLS-2$
+                        Integer.MIN_VALUE) : getInt("layout:layout_topMargin", Integer.MIN_VALUE); //$NON-NLS-1$
+        marginBottom =
+                namedProperties.containsKey("layout_bottomMargin") ? getInt("layout_bottomMargin", //$NON-NLS-1$ //$NON-NLS-2$
+                        Integer.MIN_VALUE)
+                        : getInt("layout:layout_bottomMargin", Integer.MIN_VALUE); //$NON-NLS-1$
+        baseline =
+                namedProperties.containsKey("getBaseline()") ? getInt("getBaseline()", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$
+                        "layout:getBaseline()", 0); //$NON-NLS-1$
+        willNotDraw =
+                namedProperties.containsKey("willNotDraw()") ? getBoolean("willNotDraw()", false) //$NON-NLS-1$ //$NON-NLS-2$
+                        : getBoolean("drawing:willNotDraw()", false); //$NON-NLS-1$
+        hasFocus =
+                namedProperties.containsKey("hasFocus()") ? getBoolean("hasFocus()", false) //$NON-NLS-1$ //$NON-NLS-2$
+                        : getBoolean("focus:hasFocus()", false); //$NON-NLS-1$
+
+        hasMargins =
+                marginLeft != Integer.MIN_VALUE && marginRight != Integer.MIN_VALUE
+                        && marginTop != Integer.MIN_VALUE && marginBottom != Integer.MIN_VALUE;
+
+        for (String name : namedProperties.keySet()) {
+            int index = name.indexOf(':');
+            if (index != -1) {
+                categories.add(name.substring(0, index));
+            }
+        }
+        if (categories.size() != 0) {
+            categories.add(MISCELLANIOUS);
+        }
+    }
+
+    public void setProfileRatings() {
+        final int N = children.size();
+        if (N > 1) {
+            double totalMeasure = 0;
+            double totalLayout = 0;
+            double totalDraw = 0;
+            for (int i = 0; i < N; i++) {
+                ViewNode child = children.get(i);
+                totalMeasure += child.measureTime;
+                totalLayout += child.layoutTime;
+                totalDraw += child.drawTime;
+            }
+            for (int i = 0; i < N; i++) {
+                ViewNode child = children.get(i);
+                if (child.measureTime / totalMeasure >= RED_THRESHOLD) {
+                    child.measureRating = ProfileRating.RED;
+                } else if (child.measureTime / totalMeasure >= YELLOW_THRESHOLD) {
+                    child.measureRating = ProfileRating.YELLOW;
+                } else {
+                    child.measureRating = ProfileRating.GREEN;
+                }
+                if (child.layoutTime / totalLayout >= RED_THRESHOLD) {
+                    child.layoutRating = ProfileRating.RED;
+                } else if (child.layoutTime / totalLayout >= YELLOW_THRESHOLD) {
+                    child.layoutRating = ProfileRating.YELLOW;
+                } else {
+                    child.layoutRating = ProfileRating.GREEN;
+                }
+                if (child.drawTime / totalDraw >= RED_THRESHOLD) {
+                    child.drawRating = ProfileRating.RED;
+                } else if (child.drawTime / totalDraw >= YELLOW_THRESHOLD) {
+                    child.drawRating = ProfileRating.YELLOW;
+                } else {
+                    child.drawRating = ProfileRating.GREEN;
+                }
+            }
+        }
+        for (int i = 0; i < N; i++) {
+            children.get(i).setProfileRatings();
+        }
+    }
+
+    public void setViewCount() {
+        viewCount = 1;
+        final int N = children.size();
+        for (int i = 0; i < N; i++) {
+            ViewNode child = children.get(i);
+            child.setViewCount();
+            viewCount += child.viewCount;
+        }
+    }
+
+    public void filter(String text) {
+        int dotIndex = name.lastIndexOf('.');
+        String shortName = (dotIndex == -1) ? name : name.substring(dotIndex + 1);
+        filtered =
+                !text.equals("") //$NON-NLS-1$
+                        && (shortName.toLowerCase().contains(text.toLowerCase()) || (!id
+                                .equals("NO_ID") && id.toLowerCase().contains(text.toLowerCase()))); //$NON-NLS-1$
+        final int N = children.size();
+        for (int i = 0; i < N; i++) {
+            children.get(i).filter(text);
+        }
+    }
+
+    private boolean getBoolean(String name, boolean defaultValue) {
+        Property p = namedProperties.get(name);
+        if (p != null) {
+            try {
+                return Boolean.parseBoolean(p.value);
+            } catch (NumberFormatException e) {
+                return defaultValue;
+            }
+        }
+        return defaultValue;
+    }
+
+    private int getInt(String name, int defaultValue) {
+        Property p = namedProperties.get(name);
+        if (p != null) {
+            try {
+                return Integer.parseInt(p.value);
+            } catch (NumberFormatException e) {
+                return defaultValue;
+            }
+        }
+        return defaultValue;
+    }
+
+    @Override
+    public String toString() {
+        return name + "@" + hashCode; //$NON-NLS-1$
+    }
+
+    public static class Property {
+        public String name;
+
+        public String value;
+
+        @Override
+        public String toString() {
+            return name + '=' + value;
+        }
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/Window.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/Window.java
new file mode 100644
index 0000000..4e260a9
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/Window.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2008 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.hierarchyviewerlib.models;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.IDevice;
+import com.android.hierarchyviewerlib.device.IHvDevice;
+
+/**
+ * Used for storing a window from the window manager service on the device.
+ * These are the windows that the device selector shows.
+ */
+public class Window {
+    private final String mTitle;
+    private final int mHashCode;
+    private final IHvDevice mHvDevice;
+    private final Client mClient;
+
+    public Window(IHvDevice device, String title, int hashCode) {
+        mHvDevice = device;
+        mTitle = title;
+        mHashCode = hashCode;
+        mClient = null;
+    }
+
+    public Window(IHvDevice device, String title, Client c) {
+        mHvDevice = device;
+        mTitle = title;
+        mClient = c;
+        mHashCode = c.hashCode();
+    }
+
+    public String getTitle() {
+        return mTitle;
+    }
+
+    public int getHashCode() {
+        return mHashCode;
+    }
+
+    public String encode() {
+        return Integer.toHexString(mHashCode);
+    }
+
+    @Override
+    public String toString() {
+        return mTitle;
+    }
+
+    public IHvDevice getHvDevice() {
+        return mHvDevice;
+    }
+
+    public IDevice getDevice() {
+        return mHvDevice.getDevice();
+    }
+
+    public Client getClient() {
+        return mClient;
+    }
+
+    public static Window getFocusedWindow(IHvDevice device) {
+        return new Window(device, "<Focused Window>", -1);
+    }
+
+    /*
+     * After each refresh of the windows in the device selector, the windows are
+     * different instances and automatically reselecting the same window doesn't
+     * work in the device selector unless the equals method is defined here.
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+
+        Window other = (Window) obj;
+        if (mHvDevice == null) {
+            if (other.mHvDevice != null)
+                return false;
+        } else if (!mHvDevice.getDevice().getSerialNumber().equals(
+                other.mHvDevice.getDevice().getSerialNumber()))
+            return false;
+
+        if (mHashCode != other.mHashCode)
+            return false;
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result +
+                ((mHvDevice == null) ? 0 : mHvDevice.getDevice().getSerialNumber().hashCode());
+        result = prime * result + mHashCode;
+        return result;
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/CaptureDisplay.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/CaptureDisplay.java
new file mode 100644
index 0000000..7d4fdba
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/CaptureDisplay.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2008 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.hierarchyviewerlib.ui;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+import com.android.hierarchyviewerlib.models.ViewNode;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.events.ShellAdapter;
+import org.eclipse.swt.events.ShellEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+public class CaptureDisplay {
+    private static Shell sShell;
+
+    private static Canvas sCanvas;
+
+    private static Image sImage;
+
+    private static ViewNode sViewNode;
+
+    private static Composite sButtonBar;
+
+    private static Button sOnWhite;
+
+    private static Button sOnBlack;
+
+    private static Button sShowExtras;
+
+    public static void show(Shell parentShell, ViewNode viewNode, Image image) {
+        if (sShell == null) {
+            createShell();
+        }
+        if (sShell.isVisible() && CaptureDisplay.sViewNode != null) {
+            CaptureDisplay.sViewNode.dereferenceImage();
+        }
+        CaptureDisplay.sImage = image;
+        CaptureDisplay.sViewNode = viewNode;
+        viewNode.referenceImage();
+        sShell.setText(viewNode.name);
+
+        boolean shellVisible = sShell.isVisible();
+        if (!shellVisible) {
+            sShell.setSize(0, 0);
+        }
+        Rectangle bounds =
+                sShell.computeTrim(0, 0, Math.max(sButtonBar.getBounds().width,
+                        image.getBounds().width), sButtonBar.getBounds().height
+                        + image.getBounds().height + 5);
+        sShell.setSize(bounds.width, bounds.height);
+        if (!shellVisible) {
+            sShell.setLocation(parentShell.getBounds().x
+                    + (parentShell.getBounds().width - bounds.width) / 2, parentShell.getBounds().y
+                    + (parentShell.getBounds().height - bounds.height) / 2);
+        }
+        sShell.open();
+        if (shellVisible) {
+            sCanvas.redraw();
+        }
+    }
+
+    private static void createShell() {
+        sShell = new Shell(Display.getDefault(), SWT.CLOSE | SWT.TITLE);
+        GridLayout gridLayout = new GridLayout();
+        gridLayout.marginWidth = 0;
+        gridLayout.marginHeight = 0;
+        sShell.setLayout(gridLayout);
+
+        sButtonBar = new Composite(sShell, SWT.NONE);
+        RowLayout rowLayout = new RowLayout(SWT.HORIZONTAL);
+        rowLayout.pack = true;
+        rowLayout.center = true;
+        sButtonBar.setLayout(rowLayout);
+        Composite buttons = new Composite(sButtonBar, SWT.NONE);
+        buttons.setLayout(new FillLayout());
+
+        sOnWhite = new Button(buttons, SWT.TOGGLE);
+        sOnWhite.setText("On White");
+        sOnBlack = new Button(buttons, SWT.TOGGLE);
+        sOnBlack.setText("On Black");
+        sOnBlack.setSelection(true);
+        sOnWhite.addSelectionListener(sWhiteSelectionListener);
+        sOnBlack.addSelectionListener(sBlackSelectionListener);
+
+        sShowExtras = new Button(sButtonBar, SWT.CHECK);
+        sShowExtras.setText("Show Extras");
+        sShowExtras.addSelectionListener(sExtrasSelectionListener);
+
+        sCanvas = new Canvas(sShell, SWT.NONE);
+        sCanvas.setLayoutData(new GridData(GridData.FILL_BOTH));
+        sCanvas.addPaintListener(sPaintListener);
+
+        sShell.addShellListener(sShellListener);
+
+        ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class);
+        Image image = imageLoader.loadImage("display.png", Display.getDefault()); //$NON-NLS-1$
+        sShell.setImage(image);
+    }
+
+    private static PaintListener sPaintListener = new PaintListener() {
+
+        @Override
+        public void paintControl(PaintEvent e) {
+            if (sOnWhite.getSelection()) {
+                e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
+            } else {
+                e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
+            }
+            e.gc.fillRectangle(0, 0, sCanvas.getBounds().width, sCanvas.getBounds().height);
+            if (sImage != null) {
+                int width = sImage.getBounds().width;
+                int height = sImage.getBounds().height;
+                int x = (sCanvas.getBounds().width - width) / 2;
+                int y = (sCanvas.getBounds().height - height) / 2;
+                e.gc.drawImage(sImage, x, y);
+                if (sShowExtras.getSelection()) {
+                    if ((sViewNode.paddingLeft | sViewNode.paddingRight | sViewNode.paddingTop | sViewNode.paddingBottom) != 0) {
+                        e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLUE));
+                        e.gc.drawRectangle(x + sViewNode.paddingLeft, y + sViewNode.paddingTop, width
+                                - sViewNode.paddingLeft - sViewNode.paddingRight - 1, height
+                                - sViewNode.paddingTop - sViewNode.paddingBottom - 1);
+                    }
+                    if (sViewNode.hasMargins) {
+                        e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_GREEN));
+                        e.gc.drawRectangle(x - sViewNode.marginLeft, y - sViewNode.marginTop, width
+                                + sViewNode.marginLeft + sViewNode.marginRight - 1, height
+                                + sViewNode.marginTop + sViewNode.marginBottom - 1);
+                    }
+                    if (sViewNode.baseline != -1) {
+                        e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_RED));
+                        e.gc.drawLine(x, y + sViewNode.baseline, x + width - 1, sViewNode.baseline);
+                    }
+                }
+            }
+        }
+    };
+
+    private static ShellAdapter sShellListener = new ShellAdapter() {
+        @Override
+        public void shellClosed(ShellEvent e) {
+            e.doit = false;
+            sShell.setVisible(false);
+            if (sViewNode != null) {
+                sViewNode.dereferenceImage();
+            }
+        }
+
+    };
+
+    private static SelectionListener sWhiteSelectionListener = new SelectionListener() {
+        @Override
+        public void widgetDefaultSelected(SelectionEvent e) {
+            // pass
+        }
+
+        @Override
+        public void widgetSelected(SelectionEvent e) {
+            sOnWhite.setSelection(true);
+            sOnBlack.setSelection(false);
+            sCanvas.redraw();
+        }
+    };
+
+    private static SelectionListener sBlackSelectionListener = new SelectionListener() {
+        @Override
+        public void widgetDefaultSelected(SelectionEvent e) {
+            // pass
+        }
+
+        @Override
+        public void widgetSelected(SelectionEvent e) {
+            sOnBlack.setSelection(true);
+            sOnWhite.setSelection(false);
+            sCanvas.redraw();
+        }
+    };
+
+    private static SelectionListener sExtrasSelectionListener = new SelectionListener() {
+        @Override
+        public void widgetDefaultSelected(SelectionEvent e) {
+            // pass
+        }
+
+        @Override
+        public void widgetSelected(SelectionEvent e) {
+            sCanvas.redraw();
+        }
+    };
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DevicePropertyEditingSupport.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DevicePropertyEditingSupport.java
new file mode 100644
index 0000000..1bbc97f
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DevicePropertyEditingSupport.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2013 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.hierarchyviewerlib.ui;
+
+import com.android.SdkConstants;
+import com.android.hierarchyviewerlib.device.IHvDevice;
+import com.android.hierarchyviewerlib.models.ViewNode;
+import com.android.hierarchyviewerlib.models.ViewNode.Property;
+import com.android.utils.SdkUtils;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableSet;
+
+import java.text.ParseException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+public class DevicePropertyEditingSupport {
+    public enum PropertyType {
+        INTEGER,
+        INTEGER_OR_CONSTANT,
+        ENUM,
+    };
+
+    private static final List<IDevicePropertyEditor> sDevicePropertyEditors = Arrays.asList(
+                new LayoutPropertyEditor(),
+                new PaddingPropertyEditor()
+            );
+
+    public boolean canEdit(Property p) {
+        return getPropertyEditorFor(p) != null;
+    }
+
+    private IDevicePropertyEditor getPropertyEditorFor(Property p) {
+        for (IDevicePropertyEditor pe: sDevicePropertyEditors) {
+            if (pe.canEdit(p)) {
+                return pe;
+            }
+        }
+
+        return null;
+    }
+
+    public PropertyType getPropertyType(Property p) {
+        return getPropertyEditorFor(p).getType(p);
+    }
+
+    public String[] getPropertyRange(Property p) {
+        return getPropertyEditorFor(p).getPropertyRange(p);
+    }
+
+    public boolean setValue(Collection<Property> properties, Property p, Object newValue,
+            ViewNode viewNode, IHvDevice device) {
+        return getPropertyEditorFor(p).setValue(properties, p, newValue, viewNode, device);
+    }
+
+    private static String stripCategoryPrefix(String name) {
+        return name.substring(name.indexOf(':') + 1);
+    }
+
+    private interface IDevicePropertyEditor {
+        boolean canEdit(Property p);
+        PropertyType getType(Property p);
+        String[] getPropertyRange(Property p);
+        boolean setValue(Collection<Property> properties, Property p, Object newValue,
+                ViewNode viewNode, IHvDevice device);
+    }
+
+    private static class LayoutPropertyEditor implements IDevicePropertyEditor {
+        private static final Set<String> sLayoutPropertiesWithStringValues =
+                ImmutableSet.of(SdkConstants.ATTR_LAYOUT_WIDTH,
+                        SdkConstants.ATTR_LAYOUT_HEIGHT,
+                        SdkConstants.ATTR_LAYOUT_GRAVITY);
+
+        private static final int MATCH_PARENT = -1;
+        private static final int FILL_PARENT = -1;
+        private static final int WRAP_CONTENT = -2;
+
+        private enum LayoutGravity {
+            top(0x30),
+            bottom(0x50),
+            left(0x03),
+            right(0x05),
+            center_vertical(0x10),
+            fill_vertical(0x70),
+            center_horizontal(0x01),
+            fill_horizontal(0x07),
+            center(0x11),
+            fill(0x77),
+            clip_vertical(0x80),
+            clip_horizontal(0x08),
+            start(0x00800003),
+            end(0x00800005);
+
+            private final int mValue;
+
+            private LayoutGravity(int v) {
+                mValue = v;
+            }
+        }
+
+        /**
+         * Returns true if this is a layout property with either a known string value, or an
+         * integer value.
+         */
+        @Override
+        public boolean canEdit(Property p) {
+            String name = stripCategoryPrefix(p.name);
+            if (!name.startsWith(SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX)) {
+                return false;
+            }
+
+            if (sLayoutPropertiesWithStringValues.contains(name)) {
+                return true;
+            }
+
+            try {
+                SdkUtils.parseLocalizedInt(p.value);
+                return true;
+            } catch (ParseException e) {
+                return false;
+            }
+        }
+
+        @Override
+        public PropertyType getType(Property p) {
+            String name = stripCategoryPrefix(p.name);
+            if (sLayoutPropertiesWithStringValues.contains(name)) {
+                return PropertyType.INTEGER_OR_CONSTANT;
+            } else {
+                return PropertyType.INTEGER;
+            }
+        }
+
+        @Override
+        public String[] getPropertyRange(Property p) {
+            return new String[0];
+        }
+
+        @Override
+        public boolean setValue(Collection<Property> properties, Property p, Object newValue,
+                ViewNode viewNode, IHvDevice device) {
+            String name = stripCategoryPrefix(p.name);
+
+            // nothing to do if same as current value
+            if (p.value.equals(newValue)) {
+                return false;
+            }
+
+            int value = -1;
+            String textValue = null;
+
+            if (SdkConstants.ATTR_LAYOUT_GRAVITY.equals(name)) {
+                value = 0;
+                StringBuilder sb = new StringBuilder(20);
+                for (String attr: Splitter.on('|').split((String) newValue)) {
+                    LayoutGravity g;
+                    try {
+                        g = LayoutGravity.valueOf(attr);
+                    } catch (IllegalArgumentException e) {
+                        // ignore this gravity attribute
+                        continue;
+                    }
+
+                    value |= g.mValue;
+
+                    if (sb.length() > 0) {
+                        sb.append('|');
+                    }
+                    sb.append(g.name());
+                }
+                textValue = sb.toString();
+            } else if (SdkConstants.ATTR_LAYOUT_HEIGHT.equals(name)
+                    || SdkConstants.ATTR_LAYOUT_WIDTH.equals(name)) {
+                // newValue is of type string, but its contents may be a named constant or a integer
+                String s = (String) newValue;
+                if (s.equalsIgnoreCase(SdkConstants.VALUE_MATCH_PARENT)) {
+                    textValue = SdkConstants.VALUE_MATCH_PARENT;
+                    value = MATCH_PARENT;
+                } else if (s.equalsIgnoreCase(SdkConstants.VALUE_FILL_PARENT)) {
+                    textValue = SdkConstants.VALUE_FILL_PARENT;
+                    value = FILL_PARENT;
+                } else if (s.equalsIgnoreCase(SdkConstants.VALUE_WRAP_CONTENT)) {
+                    textValue = SdkConstants.VALUE_WRAP_CONTENT;
+                    value = WRAP_CONTENT;
+                }
+            }
+
+            if (textValue == null) {
+                try {
+                    value = Integer.parseInt((String) newValue);
+                } catch (NumberFormatException e) {
+                    return false;
+                }
+            }
+
+            // attempt to set the value on the device
+            name = name.substring(SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX.length());
+            if (device.setLayoutParameter(viewNode.window, viewNode, name, value)) {
+                p.value = textValue != null ? textValue : (String) newValue;
+            }
+
+            return true;
+        }
+    }
+
+    private static class PaddingPropertyEditor implements IDevicePropertyEditor {
+        // These names should match the field names used for padding in the Framework's View class
+        private static final String PADDING_LEFT = "mPaddingLeft";      //$NON-NLS-1$
+        private static final String PADDING_RIGHT = "mPaddingRight";    //$NON-NLS-1$
+        private static final String PADDING_TOP = "mPaddingTop";        //$NON-NLS-1$
+        private static final String PADDING_BOTTOM = "mPaddingBottom";  //$NON-NLS-1$
+
+        private static final Set<String> sPaddingProperties = ImmutableSet.of(
+                PADDING_LEFT, PADDING_RIGHT, PADDING_TOP, PADDING_BOTTOM);
+
+        @Override
+        public boolean canEdit(Property p) {
+            return sPaddingProperties.contains(stripCategoryPrefix(p.name));
+        }
+
+        @Override
+        public PropertyType getType(Property p) {
+            return PropertyType.INTEGER;
+        }
+
+        @Override
+        public String[] getPropertyRange(Property p) {
+            return new String[0];
+        }
+
+        /**
+         * Set padding: Since the only view method is setPadding(l, t, r, b), we need access
+         * to all 4 padding's to update any particular one.
+         */
+        @Override
+        public boolean setValue(Collection<Property> properties, Property prop, Object newValue,
+                ViewNode viewNode, IHvDevice device) {
+            int v;
+            try {
+                v = Integer.parseInt((String) newValue);
+            } catch (NumberFormatException e) {
+                return false;
+            }
+
+            int pLeft = 0;
+            int pRight = 0;
+            int pTop = 0;
+            int pBottom = 0;
+
+            String propName = stripCategoryPrefix(prop.name);
+            for (Property p: properties) {
+                String name = stripCategoryPrefix(p.name);
+                if (!sPaddingProperties.contains(name)) {
+                    continue;
+                }
+
+                if (name.equals(PADDING_LEFT)) {
+                    pLeft = propName.equals(PADDING_LEFT) ?
+                            v : SdkUtils.parseLocalizedInt(p.value, 0);
+                } else if (name.equals(PADDING_RIGHT)) {
+                    pRight = propName.equals(PADDING_RIGHT) ?
+                            v : SdkUtils.parseLocalizedInt(p.value, 0);
+                } else if (name.equals(PADDING_TOP)) {
+                    pTop = propName.equals(PADDING_TOP) ?
+                            v : SdkUtils.parseLocalizedInt(p.value, 0);
+                } else if (name.equals(PADDING_BOTTOM)) {
+                    pBottom = propName.equals(PADDING_BOTTOM) ?
+                            v : SdkUtils.parseLocalizedInt(p.value, 0);
+                }
+            }
+
+            // invoke setPadding() on the device
+            device.invokeViewMethod(viewNode.window, viewNode, "setPadding", Arrays.asList(
+                    Integer.valueOf(pLeft),
+                    Integer.valueOf(pTop),
+                    Integer.valueOf(pRight),
+                    Integer.valueOf(pBottom)
+            ));
+
+            // update the value set in the property (to avoid reading all properties back from
+            // the device)
+            prop.value = Integer.toString(v);
+            return true;
+        }
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DeviceSelector.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DeviceSelector.java
new file mode 100644
index 0000000..ae8ad26
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DeviceSelector.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.ui;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+import com.android.hierarchyviewerlib.device.IHvDevice;
+import com.android.hierarchyviewerlib.models.DeviceSelectionModel;
+import com.android.hierarchyviewerlib.models.DeviceSelectionModel.IWindowChangeListener;
+import com.android.hierarchyviewerlib.models.Window;
+
+import org.eclipse.jface.viewers.IFontProvider;
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.TreeSelection;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.ControlListener;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeItem;
+
+public class DeviceSelector extends Composite implements IWindowChangeListener, SelectionListener {
+    private TreeViewer mTreeViewer;
+
+    private Tree mTree;
+
+    private DeviceSelectionModel mModel;
+
+    private Font mBoldFont;
+
+    private Image mDeviceImage;
+
+    private Image mEmulatorImage;
+
+    private final static int ICON_WIDTH = 16;
+
+    private boolean mDoTreeViewStuff;
+
+    private boolean mDoPixelPerfectStuff;
+
+    private class ContentProvider implements ITreeContentProvider, ILabelProvider, IFontProvider {
+        @Override
+        public Object[] getChildren(Object parentElement) {
+            if (parentElement instanceof IHvDevice && mDoTreeViewStuff) {
+                Window[] list = mModel.getWindows((IHvDevice) parentElement);
+                if (list != null) {
+                    return list;
+                }
+            }
+            return new Object[0];
+        }
+
+        @Override
+        public Object getParent(Object element) {
+            if (element instanceof Window) {
+                return ((Window) element).getDevice();
+            }
+            return null;
+        }
+
+        @Override
+        public boolean hasChildren(Object element) {
+            if (element instanceof IHvDevice && mDoTreeViewStuff) {
+                Window[] list = mModel.getWindows((IHvDevice) element);
+                if (list != null) {
+                    return list.length != 0;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public Object[] getElements(Object inputElement) {
+            if (inputElement instanceof DeviceSelectionModel) {
+                return mModel.getDevices();
+            }
+            return new Object[0];
+        }
+
+        @Override
+        public void dispose() {
+            // pass
+        }
+
+        @Override
+        public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+            // pass
+        }
+
+        @Override
+        public Image getImage(Object element) {
+            if (element instanceof IHvDevice) {
+                if (((IHvDevice) element).getDevice().isEmulator()) {
+                    return mEmulatorImage;
+                }
+                return mDeviceImage;
+            }
+            return null;
+        }
+
+        @Override
+        public String getText(Object element) {
+            if (element instanceof IHvDevice) {
+                return ((IHvDevice) element).getDevice().getName();
+            } else if (element instanceof Window) {
+                return ((Window) element).getTitle();
+            }
+            return null;
+        }
+
+        @Override
+        public Font getFont(Object element) {
+            if (element instanceof Window) {
+                int focusedWindow = mModel.getFocusedWindow(((Window) element).getHvDevice());
+                if (focusedWindow == ((Window) element).getHashCode()) {
+                    return mBoldFont;
+                }
+            }
+            return null;
+        }
+
+        @Override
+        public void addListener(ILabelProviderListener listener) {
+            // pass
+        }
+
+        @Override
+        public boolean isLabelProperty(Object element, String property) {
+            // pass
+            return false;
+        }
+
+        @Override
+        public void removeListener(ILabelProviderListener listener) {
+            // pass
+        }
+    }
+
+    public DeviceSelector(Composite parent, boolean doTreeViewStuff, boolean doPixelPerfectStuff) {
+        super(parent, SWT.NONE);
+        this.mDoTreeViewStuff = doTreeViewStuff;
+        this.mDoPixelPerfectStuff = doPixelPerfectStuff;
+        setLayout(new FillLayout());
+        mTreeViewer = new TreeViewer(this, SWT.SINGLE);
+        mTreeViewer.setAutoExpandLevel(TreeViewer.ALL_LEVELS);
+
+        mTree = mTreeViewer.getTree();
+        mTree.setLinesVisible(true);
+        mTree.addSelectionListener(this);
+
+        addDisposeListener(mDisposeListener);
+
+        loadResources();
+
+        mModel = DeviceSelectionModel.getModel();
+        ContentProvider contentProvider = new ContentProvider();
+        mTreeViewer.setContentProvider(contentProvider);
+        mTreeViewer.setLabelProvider(contentProvider);
+        mModel.addWindowChangeListener(this);
+        mTreeViewer.setInput(mModel);
+
+        addControlListener(mControlListener);
+    }
+
+    public void loadResources() {
+        Display display = Display.getDefault();
+        Font systemFont = display.getSystemFont();
+        FontData[] fontData = systemFont.getFontData();
+        FontData[] newFontData = new FontData[fontData.length];
+        for (int i = 0; i < fontData.length; i++) {
+            newFontData[i] =
+                    new FontData(fontData[i].getName(), fontData[i].getHeight(), fontData[i]
+                            .getStyle()
+                            | SWT.BOLD);
+        }
+        mBoldFont = new Font(Display.getDefault(), newFontData);
+
+        ImageLoader loader = ImageLoader.getDdmUiLibLoader();
+        mDeviceImage =
+                loader.loadImage(display, "device.png", ICON_WIDTH, ICON_WIDTH, display //$NON-NLS-1$
+                        .getSystemColor(SWT.COLOR_RED));
+
+        mEmulatorImage =
+                loader.loadImage(display, "emulator.png", ICON_WIDTH, ICON_WIDTH, display //$NON-NLS-1$
+                        .getSystemColor(SWT.COLOR_BLUE));
+    }
+
+    private DisposeListener mDisposeListener = new DisposeListener() {
+        @Override
+        public void widgetDisposed(DisposeEvent e) {
+            mModel.removeWindowChangeListener(DeviceSelector.this);
+            mBoldFont.dispose();
+        }
+    };
+
+    // If the window gets too small, hide the data, otherwise SWT throws an
+    // ERROR.
+
+    private ControlListener mControlListener = new ControlAdapter() {
+        private boolean noInput = false;
+
+        @Override
+        public void controlResized(ControlEvent e) {
+            if (getBounds().height <= 38) {
+                mTreeViewer.setInput(null);
+                noInput = true;
+            } else if (noInput) {
+                mTreeViewer.setInput(mModel);
+                noInput = false;
+            }
+        }
+    };
+
+    @Override
+    public boolean setFocus() {
+        return mTree.setFocus();
+    }
+
+    public void setMode(boolean doTreeViewStuff, boolean doPixelPerfectStuff) {
+        if (this.mDoTreeViewStuff != doTreeViewStuff
+                || this.mDoPixelPerfectStuff != doPixelPerfectStuff) {
+            final boolean expandAll = !this.mDoTreeViewStuff && doTreeViewStuff;
+            this.mDoTreeViewStuff = doTreeViewStuff;
+            this.mDoPixelPerfectStuff = doPixelPerfectStuff;
+            Display.getDefault().syncExec(new Runnable() {
+                @Override
+                public void run() {
+                    mTreeViewer.refresh();
+                    if (expandAll) {
+                        mTreeViewer.expandAll();
+                    }
+                }
+            });
+        }
+    }
+
+    @Override
+    public void deviceConnected(final IHvDevice device) {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                mTreeViewer.refresh();
+                mTreeViewer.setExpandedState(device, true);
+            }
+        });
+    }
+
+    @Override
+    public void deviceChanged(final IHvDevice device) {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                TreeSelection selection = (TreeSelection) mTreeViewer.getSelection();
+                mTreeViewer.refresh(device);
+                if (selection.getFirstElement() instanceof Window
+                        && ((Window) selection.getFirstElement()).getDevice() == device) {
+                    mTreeViewer.setSelection(selection, true);
+                }
+            }
+        });
+    }
+
+    @Override
+    public void deviceDisconnected(final IHvDevice device) {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                mTreeViewer.refresh();
+            }
+        });
+    }
+
+    @Override
+    public void focusChanged(final IHvDevice device) {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                TreeSelection selection = (TreeSelection) mTreeViewer.getSelection();
+                mTreeViewer.refresh(device);
+                if (selection.getFirstElement() instanceof Window
+                        && ((Window) selection.getFirstElement()).getDevice() == device) {
+                    mTreeViewer.setSelection(selection, true);
+                }
+            }
+        });
+    }
+
+    @Override
+    public void selectionChanged(IHvDevice device, Window window) {
+        // pass
+    }
+
+    @Override
+    public void widgetDefaultSelected(SelectionEvent e) {
+        Object selection = ((TreeItem) e.item).getData();
+        if (selection instanceof IHvDevice && mDoPixelPerfectStuff) {
+            HierarchyViewerDirector.getDirector().loadPixelPerfectData((IHvDevice) selection);
+        } else if (selection instanceof Window && mDoTreeViewStuff) {
+            HierarchyViewerDirector.getDirector().loadViewTreeData((Window) selection);
+        }
+    }
+
+    @Override
+    public void widgetSelected(SelectionEvent e) {
+        TreeItem item = (TreeItem) e.item;
+        if (item == null) return;
+        Object selection = item.getData();
+        if (selection instanceof IHvDevice) {
+            mModel.setSelection((IHvDevice) selection, null);
+        } else if (selection instanceof Window) {
+            mModel.setSelection(((Window) selection).getHvDevice(), (Window) selection);
+        }
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/InvokeMethodPrompt.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/InvokeMethodPrompt.java
new file mode 100644
index 0000000..944a57a
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/InvokeMethodPrompt.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2013 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.hierarchyviewerlib.ui;
+
+import com.android.ddmlib.Log;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+import com.android.hierarchyviewerlib.device.IHvDevice;
+import com.android.hierarchyviewerlib.models.TreeViewModel;
+import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener;
+import com.android.hierarchyviewerlib.models.ViewNode;
+import com.android.hierarchyviewerlib.ui.util.DrawableViewNode;
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Splitter;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+public class InvokeMethodPrompt extends Composite implements ITreeChangeListener {
+    private TreeViewModel mModel;
+    private DrawableViewNode mSelectedNode;
+    private Text mText;
+    private static final Splitter CMD_SPLITTER = Splitter.on(CharMatcher.anyOf(", "))
+                                                         .trimResults().omitEmptyStrings();
+
+    public InvokeMethodPrompt(Composite parent) {
+        super(parent, SWT.NONE);
+        setLayout(new FillLayout());
+
+        mText = new Text(this, SWT.BORDER);
+        mText.addKeyListener(new KeyListener() {
+            @Override
+            public void keyReleased(KeyEvent ke) {
+            }
+
+            @Override
+            public void keyPressed(KeyEvent ke) {
+                onKeyPress(ke);
+            }
+        });
+
+        mModel = TreeViewModel.getModel();
+        mModel.addTreeChangeListener(this);
+    }
+
+    private void onKeyPress(KeyEvent ke) {
+        if (ke.keyCode == SWT.CR) {
+            String cmd = mText.getText().trim();
+            if (!cmd.isEmpty()) {
+                invokeViewMethod(cmd);
+            }
+            mText.setText("");
+        }
+    }
+
+    private void invokeViewMethod(String cmd) {
+        Iterator<String> segmentIterator = CMD_SPLITTER.split(cmd).iterator();
+
+        String method = null;
+        if (segmentIterator.hasNext()) {
+            method = segmentIterator.next();
+        } else {
+            return;
+        }
+
+        List<Object> args = new ArrayList<Object>(10);
+        while (segmentIterator.hasNext()) {
+            String arg = segmentIterator.next();
+
+            // check for boolean
+            if (arg.equalsIgnoreCase("true")) {
+                args.add(Boolean.TRUE);
+                continue;
+            } else if (arg.equalsIgnoreCase("false")) {
+                args.add(Boolean.FALSE);
+                continue;
+            }
+
+            // see if last character gives a clue regarding the argument type
+            char typeSpecifier = Character.toUpperCase(arg.charAt(arg.length() - 1));
+            try {
+                switch (typeSpecifier) {
+                    case 'L':
+                        args.add(Long.valueOf(arg.substring(0, arg.length())));
+                        break;
+                    case 'D':
+                        args.add(Double.valueOf(arg.substring(0, arg.length())));
+                        break;
+                    case 'F':
+                        args.add(Float.valueOf(arg.substring(0, arg.length())));
+                        break;
+                    case 'S':
+                        args.add(Short.valueOf(arg.substring(0, arg.length())));
+                        break;
+                    case 'B':
+                        args.add(Byte.valueOf(arg.substring(0, arg.length())));
+                        break;
+                    default: // default to integer
+                        args.add(Integer.valueOf(arg));
+                        break;
+                }
+            } catch (NumberFormatException e) {
+                Log.e("hv", "Unable to parse method argument: " + arg);
+                return;
+            }
+        }
+
+        HierarchyViewerDirector.getDirector().invokeMethodOnSelectedView(method, args);
+    }
+
+    @Override
+    public void selectionChanged() {
+        mSelectedNode = mModel.getSelection();
+        refresh();
+    }
+
+    private boolean isViewUpdateEnabled(ViewNode viewNode) {
+        IHvDevice device = viewNode.window.getHvDevice();
+        return device != null && device.isViewUpdateEnabled();
+    }
+
+    private void refresh() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                mText.setEnabled(mSelectedNode != null
+                        && isViewUpdateEnabled(mSelectedNode.viewNode));
+            }
+        });
+    }
+
+    @Override
+    public void treeChanged() {
+        selectionChanged();
+    }
+
+    @Override
+    public void viewportChanged() {
+    }
+
+    @Override
+    public void zoomChanged() {
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/LayoutViewer.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/LayoutViewer.java
new file mode 100644
index 0000000..95c7a29
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/LayoutViewer.java
@@ -0,0 +1,372 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.ui;
+
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+import com.android.hierarchyviewerlib.models.TreeViewModel;
+import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener;
+import com.android.hierarchyviewerlib.ui.util.DrawableViewNode;
+import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Point;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.graphics.Transform;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+
+import java.util.ArrayList;
+
+public class LayoutViewer extends Canvas implements ITreeChangeListener {
+
+    private TreeViewModel mModel;
+
+    private DrawableViewNode mTree;
+
+    private DrawableViewNode mSelectedNode;
+
+    private Transform mTransform;
+
+    private Transform mInverse;
+
+    private double mScale;
+
+    private boolean mShowExtras = false;
+
+    private boolean mOnBlack = true;
+
+    public LayoutViewer(Composite parent) {
+        super(parent, SWT.NONE);
+        mModel = TreeViewModel.getModel();
+        mModel.addTreeChangeListener(this);
+
+        addDisposeListener(mDisposeListener);
+        addPaintListener(mPaintListener);
+        addListener(SWT.Resize, mResizeListener);
+        addMouseListener(mMouseListener);
+
+        mTransform = new Transform(Display.getDefault());
+        mInverse = new Transform(Display.getDefault());
+
+        treeChanged();
+    }
+
+    public void setShowExtras(boolean show) {
+        mShowExtras = show;
+        doRedraw();
+    }
+
+    public void setOnBlack(boolean value) {
+        mOnBlack = value;
+        doRedraw();
+    }
+
+    public boolean getOnBlack() {
+        return mOnBlack;
+    }
+
+    private DisposeListener mDisposeListener = new DisposeListener() {
+        @Override
+        public void widgetDisposed(DisposeEvent e) {
+            mModel.removeTreeChangeListener(LayoutViewer.this);
+            mTransform.dispose();
+            mInverse.dispose();
+            if (mSelectedNode != null) {
+                mSelectedNode.viewNode.dereferenceImage();
+            }
+        }
+    };
+
+    private Listener mResizeListener = new Listener() {
+        @Override
+        public void handleEvent(Event e) {
+            synchronized (this) {
+                setTransform();
+            }
+        }
+    };
+
+    private MouseListener mMouseListener = new MouseListener() {
+
+        @Override
+        public void mouseDoubleClick(MouseEvent e) {
+            if (mSelectedNode != null) {
+                HierarchyViewerDirector.getDirector()
+                        .showCapture(getShell(), mSelectedNode.viewNode);
+            }
+        }
+
+        @Override
+        public void mouseDown(MouseEvent e) {
+            boolean selectionChanged = false;
+            DrawableViewNode newSelection = null;
+            synchronized (LayoutViewer.this) {
+                if (mTree != null) {
+                    float[] pt = {
+                            e.x, e.y
+                    };
+                    mInverse.transform(pt);
+                    newSelection =
+                            updateSelection(mTree, pt[0], pt[1], 0, 0, 0, 0, mTree.viewNode.width,
+                                    mTree.viewNode.height);
+                    if (mSelectedNode != newSelection) {
+                        selectionChanged = true;
+                    }
+                }
+            }
+            if (selectionChanged) {
+                mModel.setSelection(newSelection);
+            }
+        }
+
+        @Override
+        public void mouseUp(MouseEvent e) {
+            // pass
+        }
+    };
+
+    private DrawableViewNode updateSelection(DrawableViewNode node, float x, float y, int left,
+            int top, int clipX, int clipY, int clipWidth, int clipHeight) {
+        if (!node.treeDrawn) {
+            return null;
+        }
+        // Update the clip
+        int x1 = Math.max(left, clipX);
+        int x2 = Math.min(left + node.viewNode.width, clipX + clipWidth);
+        int y1 = Math.max(top, clipY);
+        int y2 = Math.min(top + node.viewNode.height, clipY + clipHeight);
+        clipX = x1;
+        clipY = y1;
+        clipWidth = x2 - x1;
+        clipHeight = y2 - y1;
+        if (x < clipX || x > clipX + clipWidth || y < clipY || y > clipY + clipHeight) {
+            return null;
+        }
+        final int N = node.children.size();
+        for (int i = N - 1; i >= 0; i--) {
+            DrawableViewNode child = node.children.get(i);
+            DrawableViewNode ret =
+                    updateSelection(child, x, y,
+                            left + child.viewNode.left - node.viewNode.scrollX, top
+                                    + child.viewNode.top - node.viewNode.scrollY, clipX, clipY,
+                            clipWidth, clipHeight);
+            if (ret != null) {
+                return ret;
+            }
+        }
+        return node;
+    }
+
+    private PaintListener mPaintListener = new PaintListener() {
+        @Override
+        public void paintControl(PaintEvent e) {
+            synchronized (LayoutViewer.this) {
+                if (mOnBlack) {
+                    e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
+                } else {
+                    e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
+                }
+                e.gc.fillRectangle(0, 0, getBounds().width, getBounds().height);
+                if (mTree != null) {
+                    e.gc.setLineWidth((int) Math.ceil(0.3 / mScale));
+                    e.gc.setTransform(mTransform);
+                    if (mOnBlack) {
+                        e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
+                    } else {
+                        e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
+                    }
+                    Rectangle parentClipping = e.gc.getClipping();
+                    e.gc.setClipping(0, 0, mTree.viewNode.width + (int) Math.ceil(0.3 / mScale),
+                            mTree.viewNode.height + (int) Math.ceil(0.3 / mScale));
+                    paintRecursive(e.gc, mTree, 0, 0, true);
+
+                    if (mSelectedNode != null) {
+                        e.gc.setClipping(parentClipping);
+
+                        // w00t, let's be nice and display the whole path in
+                        // light red and the selected node in dark red.
+                        ArrayList<Point> rightLeftDistances = new ArrayList<Point>();
+                        int left = 0;
+                        int top = 0;
+                        DrawableViewNode currentNode = mSelectedNode;
+                        while (currentNode != mTree) {
+                            left += currentNode.viewNode.left;
+                            top += currentNode.viewNode.top;
+                            currentNode = currentNode.parent;
+                            left -= currentNode.viewNode.scrollX;
+                            top -= currentNode.viewNode.scrollY;
+                            rightLeftDistances.add(new Point(left, top));
+                        }
+                        e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_DARK_RED));
+                        currentNode = mSelectedNode.parent;
+                        final int N = rightLeftDistances.size();
+                        for (int i = 0; i < N; i++) {
+                            e.gc.drawRectangle((int) (left - rightLeftDistances.get(i).x),
+                                    (int) (top - rightLeftDistances.get(i).y),
+                                    currentNode.viewNode.width, currentNode.viewNode.height);
+                            currentNode = currentNode.parent;
+                        }
+
+                        if (mShowExtras && mSelectedNode.viewNode.image != null) {
+                            e.gc.drawImage(mSelectedNode.viewNode.image, left, top);
+                            if (mOnBlack) {
+                                e.gc.setForeground(Display.getDefault().getSystemColor(
+                                        SWT.COLOR_WHITE));
+                            } else {
+                                e.gc.setForeground(Display.getDefault().getSystemColor(
+                                        SWT.COLOR_BLACK));
+                            }
+                            paintRecursive(e.gc, mSelectedNode, left, top, true);
+
+                        }
+
+                        e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_RED));
+                        e.gc.setLineWidth((int) Math.ceil(2 / mScale));
+                        e.gc.drawRectangle(left, top, mSelectedNode.viewNode.width,
+                                mSelectedNode.viewNode.height);
+                    }
+                }
+            }
+        }
+    };
+
+    private void paintRecursive(GC gc, DrawableViewNode node, int left, int top, boolean root) {
+        if (!node.treeDrawn) {
+            return;
+        }
+        // Don't shift the root
+        if (!root) {
+            left += node.viewNode.left;
+            top += node.viewNode.top;
+        }
+        Rectangle parentClipping = gc.getClipping();
+        int x1 = Math.max(parentClipping.x, left);
+        int x2 =
+                Math.min(parentClipping.x + parentClipping.width, left + node.viewNode.width
+                        + (int) Math.ceil(0.3 / mScale));
+        int y1 = Math.max(parentClipping.y, top);
+        int y2 =
+                Math.min(parentClipping.y + parentClipping.height, top + node.viewNode.height
+                        + (int) Math.ceil(0.3 / mScale));
+
+        // Clipping is weird... You set it to -5 and it comes out 17 or
+        // something.
+        if (x2 <= x1 || y2 <= y1) {
+            return;
+        }
+        gc.setClipping(x1, y1, x2 - x1, y2 - y1);
+        final int N = node.children.size();
+        for (int i = 0; i < N; i++) {
+            paintRecursive(gc, node.children.get(i), left - node.viewNode.scrollX, top
+                    - node.viewNode.scrollY, false);
+        }
+        gc.setClipping(parentClipping);
+        if (!node.viewNode.willNotDraw) {
+            gc.drawRectangle(left, top, node.viewNode.width, node.viewNode.height);
+        }
+
+    }
+
+    private void doRedraw() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                redraw();
+            }
+        });
+    }
+
+    private void setTransform() {
+        if (mTree != null) {
+            Rectangle bounds = getBounds();
+            int leftRightPadding = bounds.width <= 30 ? 0 : 5;
+            int topBottomPadding = bounds.height <= 30 ? 0 : 5;
+            mScale =
+                    Math.min(1.0 * (bounds.width - leftRightPadding * 2) / mTree.viewNode.width, 1.0
+                            * (bounds.height - topBottomPadding * 2) / mTree.viewNode.height);
+            int scaledWidth = (int) Math.ceil(mTree.viewNode.width * mScale);
+            int scaledHeight = (int) Math.ceil(mTree.viewNode.height * mScale);
+
+            mTransform.identity();
+            mInverse.identity();
+            mTransform.translate((bounds.width - scaledWidth) / 2.0f,
+                    (bounds.height - scaledHeight) / 2.0f);
+            mInverse.translate((bounds.width - scaledWidth) / 2.0f,
+                    (bounds.height - scaledHeight) / 2.0f);
+            mTransform.scale((float) mScale, (float) mScale);
+            mInverse.scale((float) mScale, (float) mScale);
+            if (bounds.width != 0 && bounds.height != 0) {
+                mInverse.invert();
+            }
+        }
+    }
+
+    @Override
+    public void selectionChanged() {
+        synchronized (this) {
+            if (mSelectedNode != null) {
+                mSelectedNode.viewNode.dereferenceImage();
+            }
+            mSelectedNode = mModel.getSelection();
+            if (mSelectedNode != null) {
+                mSelectedNode.viewNode.referenceImage();
+            }
+        }
+        doRedraw();
+    }
+
+    // Note the syncExec and then synchronized... It avoids deadlock
+    @Override
+    public void treeChanged() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                synchronized (this) {
+                    if (mSelectedNode != null) {
+                        mSelectedNode.viewNode.dereferenceImage();
+                    }
+                    mTree = mModel.getTree();
+                    mSelectedNode = mModel.getSelection();
+                    if (mSelectedNode != null) {
+                        mSelectedNode.viewNode.referenceImage();
+                    }
+                    setTransform();
+                }
+            }
+        });
+        doRedraw();
+    }
+
+    @Override
+    public void viewportChanged() {
+        // pass
+    }
+
+    @Override
+    public void zoomChanged() {
+        // pass
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfect.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfect.java
new file mode 100644
index 0000000..069fb61
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfect.java
@@ -0,0 +1,392 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.ui;
+
+import com.android.hierarchyviewerlib.models.PixelPerfectModel;
+import com.android.hierarchyviewerlib.models.ViewNode;
+import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.events.MouseMoveListener;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+
+public class PixelPerfect extends ScrolledComposite implements IImageChangeListener {
+    private Canvas mCanvas;
+
+    private PixelPerfectModel mModel;
+
+    private Image mImage;
+
+    private Color mCrosshairColor;
+
+    private Color mMarginColor;
+
+    private Color mBorderColor;
+
+    private Color mPaddingColor;
+
+    private int mWidth;
+
+    private int mHeight;
+
+    private Point mCrosshairLocation;
+
+    private ViewNode mSelectedNode;
+
+    private Image mOverlayImage;
+
+    private double mOverlayTransparency;
+
+    public PixelPerfect(Composite parent) {
+        super(parent, SWT.H_SCROLL | SWT.V_SCROLL);
+        mCanvas = new Canvas(this, SWT.NONE);
+        setContent(mCanvas);
+        setExpandHorizontal(true);
+        setExpandVertical(true);
+        mModel = PixelPerfectModel.getModel();
+        mModel.addImageChangeListener(this);
+
+        mCanvas.addPaintListener(mPaintListener);
+        mCanvas.addMouseListener(mMouseListener);
+        mCanvas.addMouseMoveListener(mMouseMoveListener);
+        mCanvas.addKeyListener(mKeyListener);
+
+        addDisposeListener(mDisposeListener);
+
+        mCrosshairColor = new Color(Display.getDefault(), new RGB(0, 255, 255));
+        mBorderColor = new Color(Display.getDefault(), new RGB(255, 0, 0));
+        mMarginColor = new Color(Display.getDefault(), new RGB(0, 255, 0));
+        mPaddingColor = new Color(Display.getDefault(), new RGB(0, 0, 255));
+
+        imageLoaded();
+    }
+
+    private DisposeListener mDisposeListener = new DisposeListener() {
+        @Override
+        public void widgetDisposed(DisposeEvent e) {
+            mModel.removeImageChangeListener(PixelPerfect.this);
+            mCrosshairColor.dispose();
+            mBorderColor.dispose();
+            mPaddingColor.dispose();
+        }
+    };
+
+    @Override
+    public boolean setFocus() {
+        return mCanvas.setFocus();
+    }
+
+    private MouseListener mMouseListener = new MouseListener() {
+
+        @Override
+        public void mouseDoubleClick(MouseEvent e) {
+            // pass
+        }
+
+        @Override
+        public void mouseDown(MouseEvent e) {
+            handleMouseEvent(e);
+        }
+
+        @Override
+        public void mouseUp(MouseEvent e) {
+            handleMouseEvent(e);
+        }
+
+    };
+
+    private MouseMoveListener mMouseMoveListener = new MouseMoveListener() {
+        @Override
+        public void mouseMove(MouseEvent e) {
+            if (e.stateMask != 0) {
+                handleMouseEvent(e);
+            }
+        }
+    };
+
+    private void handleMouseEvent(MouseEvent e) {
+        synchronized (PixelPerfect.this) {
+            if (mImage == null) {
+                return;
+            }
+            int leftOffset = mCanvas.getSize().x / 2 - mWidth / 2;
+            int topOffset = mCanvas.getSize().y / 2 - mHeight / 2;
+            e.x -= leftOffset;
+            e.y -= topOffset;
+            e.x = Math.max(e.x, 0);
+            e.x = Math.min(e.x, mWidth - 1);
+            e.y = Math.max(e.y, 0);
+            e.y = Math.min(e.y, mHeight - 1);
+        }
+        mModel.setCrosshairLocation(e.x, e.y);
+    }
+
+    private KeyListener mKeyListener = new KeyListener() {
+
+        @Override
+        public void keyPressed(KeyEvent e) {
+            boolean crosshairMoved = false;
+            synchronized (PixelPerfect.this) {
+                if (mImage != null) {
+                    switch (e.keyCode) {
+                        case SWT.ARROW_UP:
+                            if (mCrosshairLocation.y != 0) {
+                                mCrosshairLocation.y--;
+                                crosshairMoved = true;
+                            }
+                            break;
+                        case SWT.ARROW_DOWN:
+                            if (mCrosshairLocation.y != mHeight - 1) {
+                                mCrosshairLocation.y++;
+                                crosshairMoved = true;
+                            }
+                            break;
+                        case SWT.ARROW_LEFT:
+                            if (mCrosshairLocation.x != 0) {
+                                mCrosshairLocation.x--;
+                                crosshairMoved = true;
+                            }
+                            break;
+                        case SWT.ARROW_RIGHT:
+                            if (mCrosshairLocation.x != mWidth - 1) {
+                                mCrosshairLocation.x++;
+                                crosshairMoved = true;
+                            }
+                            break;
+                    }
+                }
+            }
+            if (crosshairMoved) {
+                mModel.setCrosshairLocation(mCrosshairLocation.x, mCrosshairLocation.y);
+            }
+        }
+
+        @Override
+        public void keyReleased(KeyEvent e) {
+            // pass
+        }
+
+    };
+
+    private PaintListener mPaintListener = new PaintListener() {
+        @Override
+        public void paintControl(PaintEvent e) {
+            synchronized (PixelPerfect.this) {
+                e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
+                e.gc.fillRectangle(0, 0, mCanvas.getSize().x, mCanvas.getSize().y);
+                if (mImage != null) {
+                    // Let's be cool and put it in the center...
+                    int leftOffset = mCanvas.getSize().x / 2 - mWidth / 2;
+                    int topOffset = mCanvas.getSize().y / 2 - mHeight / 2;
+                    e.gc.drawImage(mImage, leftOffset, topOffset);
+                    if (mOverlayImage != null) {
+                        e.gc.setAlpha((int) (mOverlayTransparency * 255));
+                        int overlayTopOffset =
+                                mCanvas.getSize().y / 2 + mHeight / 2
+                                        - mOverlayImage.getBounds().height;
+                        e.gc.drawImage(mOverlayImage, leftOffset, overlayTopOffset);
+                        e.gc.setAlpha(255);
+                    }
+
+                    if (mSelectedNode != null) {
+                        // If the screen is in landscape mode, the
+                        // coordinates are backwards.
+                        int leftShift = 0;
+                        int topShift = 0;
+                        int nodeLeft = mSelectedNode.left;
+                        int nodeTop = mSelectedNode.top;
+                        int nodeWidth = mSelectedNode.width;
+                        int nodeHeight = mSelectedNode.height;
+                        int nodeMarginLeft = mSelectedNode.marginLeft;
+                        int nodeMarginTop = mSelectedNode.marginTop;
+                        int nodeMarginRight = mSelectedNode.marginRight;
+                        int nodeMarginBottom = mSelectedNode.marginBottom;
+                        int nodePadLeft = mSelectedNode.paddingLeft;
+                        int nodePadTop = mSelectedNode.paddingTop;
+                        int nodePadRight = mSelectedNode.paddingRight;
+                        int nodePadBottom = mSelectedNode.paddingBottom;
+                        ViewNode cur = mSelectedNode;
+                        while (cur.parent != null) {
+                            leftShift += cur.parent.left - cur.parent.scrollX;
+                            topShift += cur.parent.top - cur.parent.scrollY;
+                            cur = cur.parent;
+                        }
+
+                        // Everything is sideways.
+                        if (cur.width > cur.height) {
+                            e.gc.setForeground(mPaddingColor);
+                            e.gc.drawRectangle(leftOffset + mWidth - nodeTop - topShift - nodeHeight
+                                    + nodePadBottom,
+                                    topOffset + leftShift + nodeLeft + nodePadLeft, nodeHeight
+                                            - nodePadBottom - nodePadTop, nodeWidth - nodePadRight
+                                            - nodePadLeft);
+                            e.gc.setForeground(mMarginColor);
+                            e.gc.drawRectangle(leftOffset + mWidth - nodeTop - topShift - nodeHeight
+                                    - nodeMarginBottom, topOffset + leftShift + nodeLeft
+                                    - nodeMarginLeft,
+                                    nodeHeight + nodeMarginBottom + nodeMarginTop, nodeWidth
+                                            + nodeMarginRight + nodeMarginLeft);
+                            e.gc.setForeground(mBorderColor);
+                            e.gc.drawRectangle(
+                                    leftOffset + mWidth - nodeTop - topShift - nodeHeight, topOffset
+                                            + leftShift + nodeLeft, nodeHeight, nodeWidth);
+                        } else {
+                            e.gc.setForeground(mPaddingColor);
+                            e.gc.drawRectangle(leftOffset + leftShift + nodeLeft + nodePadLeft,
+                                    topOffset + topShift + nodeTop + nodePadTop, nodeWidth
+                                            - nodePadRight - nodePadLeft, nodeHeight
+                                            - nodePadBottom - nodePadTop);
+                            e.gc.setForeground(mMarginColor);
+                            e.gc.drawRectangle(leftOffset + leftShift + nodeLeft - nodeMarginLeft,
+                                    topOffset + topShift + nodeTop - nodeMarginTop, nodeWidth
+                                            + nodeMarginRight + nodeMarginLeft, nodeHeight
+                                            + nodeMarginBottom + nodeMarginTop);
+                            e.gc.setForeground(mBorderColor);
+                            e.gc.drawRectangle(leftOffset + leftShift + nodeLeft, topOffset
+                                    + topShift + nodeTop, nodeWidth, nodeHeight);
+                        }
+                    }
+                    if (mCrosshairLocation != null) {
+                        e.gc.setForeground(mCrosshairColor);
+                        e.gc.drawLine(leftOffset, topOffset + mCrosshairLocation.y, leftOffset
+                                + mWidth - 1, topOffset + mCrosshairLocation.y);
+                        e.gc.drawLine(leftOffset + mCrosshairLocation.x, topOffset, leftOffset
+                                + mCrosshairLocation.x, topOffset + mHeight - 1);
+                    }
+                }
+            }
+        }
+    };
+
+    private void doRedraw() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                mCanvas.redraw();
+            }
+        });
+    }
+
+    private void loadImage() {
+        mImage = mModel.getImage();
+        if (mImage != null) {
+            mWidth = mImage.getBounds().width;
+            mHeight = mImage.getBounds().height;
+        } else {
+            mWidth = 0;
+            mHeight = 0;
+        }
+        setMinSize(mWidth, mHeight);
+    }
+
+    @Override
+    public void imageLoaded() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                synchronized (this) {
+                    loadImage();
+                    mCrosshairLocation = mModel.getCrosshairLocation();
+                    mSelectedNode = mModel.getSelected();
+                    mOverlayImage = mModel.getOverlayImage();
+                    mOverlayTransparency = mModel.getOverlayTransparency();
+                }
+            }
+        });
+        doRedraw();
+    }
+
+    @Override
+    public void imageChanged() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                synchronized (this) {
+                    loadImage();
+                }
+            }
+        });
+        doRedraw();
+    }
+
+    @Override
+    public void crosshairMoved() {
+        synchronized (this) {
+            mCrosshairLocation = mModel.getCrosshairLocation();
+        }
+        doRedraw();
+    }
+
+    @Override
+    public void selectionChanged() {
+        synchronized (this) {
+            mSelectedNode = mModel.getSelected();
+        }
+        doRedraw();
+    }
+
+    // Note the syncExec and then synchronized... It avoids deadlock
+    @Override
+    public void treeChanged() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                synchronized (this) {
+                    mSelectedNode = mModel.getSelected();
+                }
+            }
+        });
+        doRedraw();
+    }
+
+    @Override
+    public void zoomChanged() {
+        // pass
+    }
+
+    @Override
+    public void overlayChanged() {
+        synchronized (this) {
+            mOverlayImage = mModel.getOverlayImage();
+            mOverlayTransparency = mModel.getOverlayTransparency();
+        }
+        doRedraw();
+    }
+
+    @Override
+    public void overlayTransparencyChanged() {
+        synchronized (this) {
+            mOverlayTransparency = mModel.getOverlayTransparency();
+        }
+        doRedraw();
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectControls.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectControls.java
new file mode 100644
index 0000000..6054088
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectControls.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.ui;
+
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+import com.android.hierarchyviewerlib.models.PixelPerfectModel;
+import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Slider;
+
+public class PixelPerfectControls extends Composite implements IImageChangeListener {
+
+    private Slider mOverlaySlider;
+
+    private Slider mZoomSlider;
+
+    private Slider mAutoRefreshSlider;
+
+    public PixelPerfectControls(Composite parent) {
+        super(parent, SWT.NONE);
+        setLayout(new FormLayout());
+
+        Label overlayTransparencyRight = new Label(this, SWT.NONE);
+        overlayTransparencyRight.setText("100%");
+        FormData overlayTransparencyRightData = new FormData();
+        overlayTransparencyRightData.right = new FormAttachment(100, -2);
+        overlayTransparencyRightData.top = new FormAttachment(0, 2);
+        overlayTransparencyRight.setLayoutData(overlayTransparencyRightData);
+
+        Label refreshRight = new Label(this, SWT.NONE);
+        refreshRight.setText("40s");
+        FormData refreshRightData = new FormData();
+        refreshRightData.right = new FormAttachment(100, -2);
+        refreshRightData.top = new FormAttachment(overlayTransparencyRight, 2);
+        refreshRightData.left = new FormAttachment(overlayTransparencyRight, 0, SWT.LEFT);
+        refreshRight.setLayoutData(refreshRightData);
+
+        Label zoomRight = new Label(this, SWT.NONE);
+        zoomRight.setText("24x");
+        FormData zoomRightData = new FormData();
+        zoomRightData.right = new FormAttachment(100, -2);
+        zoomRightData.top = new FormAttachment(refreshRight, 2);
+        zoomRightData.left = new FormAttachment(overlayTransparencyRight, 0, SWT.LEFT);
+        zoomRight.setLayoutData(zoomRightData);
+
+        Label overlayTransparency = new Label(this, SWT.NONE);
+        Label refresh = new Label(this, SWT.NONE);
+
+        overlayTransparency.setText("Overlay:");
+        FormData overlayTransparencyData = new FormData();
+        overlayTransparencyData.left = new FormAttachment(0, 2);
+        overlayTransparencyData.top = new FormAttachment(0, 2);
+        overlayTransparencyData.right = new FormAttachment(refresh, 0, SWT.RIGHT);
+        overlayTransparency.setLayoutData(overlayTransparencyData);
+
+        refresh.setText("Refresh Rate:");
+        FormData refreshData = new FormData();
+        refreshData.top = new FormAttachment(overlayTransparency, 2);
+        refreshData.left = new FormAttachment(0, 2);
+        refresh.setLayoutData(refreshData);
+
+        Label zoom = new Label(this, SWT.NONE);
+        zoom.setText("Zoom:");
+        FormData zoomData = new FormData();
+        zoomData.right = new FormAttachment(refresh, 0, SWT.RIGHT);
+        zoomData.top = new FormAttachment(refresh, 2);
+        zoomData.left = new FormAttachment(0, 2);
+        zoom.setLayoutData(zoomData);
+
+        Label overlayTransparencyLeft = new Label(this, SWT.RIGHT);
+        overlayTransparencyLeft.setText("0%");
+        FormData overlayTransparencyLeftData = new FormData();
+        overlayTransparencyLeftData.top = new FormAttachment(0, 2);
+        overlayTransparencyLeftData.left = new FormAttachment(overlayTransparency, 2);
+        overlayTransparencyLeft.setLayoutData(overlayTransparencyLeftData);
+
+        Label refreshLeft = new Label(this, SWT.RIGHT);
+        refreshLeft.setText("1s");
+        FormData refreshLeftData = new FormData();
+        refreshLeftData.top = new FormAttachment(overlayTransparencyLeft, 2);
+        refreshLeftData.left = new FormAttachment(refresh, 2);
+        refreshLeft.setLayoutData(refreshLeftData);
+
+        Label zoomLeft = new Label(this, SWT.RIGHT);
+        zoomLeft.setText("2x");
+        FormData zoomLeftData = new FormData();
+        zoomLeftData.top = new FormAttachment(refreshLeft, 2);
+        zoomLeftData.left = new FormAttachment(zoom, 2);
+        zoomLeft.setLayoutData(zoomLeftData);
+
+        mOverlaySlider = new Slider(this, SWT.HORIZONTAL);
+        mOverlaySlider.setMinimum(0);
+        mOverlaySlider.setMaximum(101);
+        mOverlaySlider.setThumb(1);
+        mOverlaySlider.setSelection((int) Math.round(PixelPerfectModel.getModel()
+                .getOverlayTransparency() * 100));
+
+        Image overlayImage = PixelPerfectModel.getModel().getOverlayImage();
+        mOverlaySlider.setEnabled(overlayImage != null);
+        FormData overlaySliderData = new FormData();
+        overlaySliderData.right = new FormAttachment(overlayTransparencyRight, -4);
+        overlaySliderData.top = new FormAttachment(0, 2);
+        overlaySliderData.left = new FormAttachment(overlayTransparencyLeft, 4);
+        mOverlaySlider.setLayoutData(overlaySliderData);
+
+        mOverlaySlider.addSelectionListener(overlaySliderSelectionListener);
+
+        mAutoRefreshSlider = new Slider(this, SWT.HORIZONTAL);
+        mAutoRefreshSlider.setMinimum(1);
+        mAutoRefreshSlider.setMaximum(41);
+        mAutoRefreshSlider.setThumb(1);
+        mAutoRefreshSlider.setSelection(HierarchyViewerDirector.getDirector()
+                .getPixelPerfectAutoRefreshInverval());
+        FormData refreshSliderData = new FormData();
+        refreshSliderData.right = new FormAttachment(overlayTransparencyRight, -4);
+        refreshSliderData.top = new FormAttachment(overlayTransparencyRight, 2);
+        refreshSliderData.left = new FormAttachment(mOverlaySlider, 0, SWT.LEFT);
+        mAutoRefreshSlider.setLayoutData(refreshSliderData);
+
+        mAutoRefreshSlider.addSelectionListener(mRefreshSliderSelectionListener);
+
+        mZoomSlider = new Slider(this, SWT.HORIZONTAL);
+        mZoomSlider.setMinimum(2);
+        mZoomSlider.setMaximum(25);
+        mZoomSlider.setThumb(1);
+        mZoomSlider.setSelection(PixelPerfectModel.getModel().getZoom());
+        FormData zoomSliderData = new FormData();
+        zoomSliderData.right = new FormAttachment(overlayTransparencyRight, -4);
+        zoomSliderData.top = new FormAttachment(refreshRight, 2);
+        zoomSliderData.left = new FormAttachment(mOverlaySlider, 0, SWT.LEFT);
+        mZoomSlider.setLayoutData(zoomSliderData);
+
+        mZoomSlider.addSelectionListener(mZoomSliderSelectionListener);
+
+        addDisposeListener(mDisposeListener);
+
+        PixelPerfectModel.getModel().addImageChangeListener(this);
+    }
+
+    private DisposeListener mDisposeListener = new DisposeListener() {
+        @Override
+        public void widgetDisposed(DisposeEvent e) {
+            PixelPerfectModel.getModel().removeImageChangeListener(PixelPerfectControls.this);
+        }
+    };
+
+    private SelectionListener overlaySliderSelectionListener = new SelectionListener() {
+        private int oldValue;
+
+        @Override
+        public void widgetDefaultSelected(SelectionEvent e) {
+            // pass
+        }
+
+        @Override
+        public void widgetSelected(SelectionEvent e) {
+            int newValue = mOverlaySlider.getSelection();
+            if (oldValue != newValue) {
+                PixelPerfectModel.getModel().removeImageChangeListener(PixelPerfectControls.this);
+                PixelPerfectModel.getModel().setOverlayTransparency(newValue / 100.0);
+                PixelPerfectModel.getModel().addImageChangeListener(PixelPerfectControls.this);
+                oldValue = newValue;
+            }
+        }
+    };
+
+    private SelectionListener mRefreshSliderSelectionListener = new SelectionListener() {
+        private int oldValue;
+
+        @Override
+        public void widgetDefaultSelected(final SelectionEvent e) {
+            // pass
+        }
+
+        @Override
+        public void widgetSelected(SelectionEvent e) {
+            int newValue = mAutoRefreshSlider.getSelection();
+            if (oldValue != newValue) {
+                HierarchyViewerDirector.getDirector().setPixelPerfectAutoRefreshInterval(newValue);
+            }
+        }
+    };
+
+    private SelectionListener mZoomSliderSelectionListener = new SelectionListener() {
+        private int oldValue;
+
+        @Override
+        public void widgetDefaultSelected(SelectionEvent e) {
+            // pass
+        }
+
+        @Override
+        public void widgetSelected(SelectionEvent e) {
+            int newValue = mZoomSlider.getSelection();
+            if (oldValue != newValue) {
+                PixelPerfectModel.getModel().removeImageChangeListener(PixelPerfectControls.this);
+                PixelPerfectModel.getModel().setZoom(newValue);
+                PixelPerfectModel.getModel().addImageChangeListener(PixelPerfectControls.this);
+                oldValue = newValue;
+            }
+        }
+    };
+
+    @Override
+    public void crosshairMoved() {
+        // pass
+    }
+
+    @Override
+    public void treeChanged() {
+        // pass
+    }
+
+    @Override
+    public void imageChanged() {
+        // pass
+    }
+
+    @Override
+    public void imageLoaded() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                Image overlayImage = PixelPerfectModel.getModel().getOverlayImage();
+                mOverlaySlider.setEnabled(overlayImage != null);
+                if (PixelPerfectModel.getModel().getImage() == null) {
+                } else {
+                    mZoomSlider.setSelection(PixelPerfectModel.getModel().getZoom());
+                }
+            }
+        });
+    }
+
+    @Override
+    public void overlayChanged() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                Image overlayImage = PixelPerfectModel.getModel().getOverlayImage();
+                mOverlaySlider.setEnabled(overlayImage != null);
+            }
+        });
+    }
+
+    @Override
+    public void overlayTransparencyChanged() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                mOverlaySlider.setSelection((int) (PixelPerfectModel.getModel()
+                        .getOverlayTransparency() * 100));
+            }
+        });
+    }
+
+    @Override
+    public void selectionChanged() {
+        // pass
+    }
+
+    @Override
+    public void zoomChanged() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                mZoomSlider.setSelection(PixelPerfectModel.getModel().getZoom());
+            }
+        });
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectLoupe.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectLoupe.java
new file mode 100644
index 0000000..ac3d66e
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectLoupe.java
@@ -0,0 +1,391 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.ui;
+
+import com.android.hierarchyviewerlib.models.PixelPerfectModel;
+import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.events.MouseWheelListener;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.PaletteData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.graphics.Transform;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+
+public class PixelPerfectLoupe extends Canvas implements IImageChangeListener {
+    private PixelPerfectModel mModel;
+
+    private Image mImage;
+
+    private Image mGrid;
+
+    private Color mCrosshairColor;
+
+    private int mWidth;
+
+    private int mHeight;
+
+    private Point mCrosshairLocation;
+
+    private int mZoom;
+
+    private Transform mTransform;
+
+    private int mCanvasWidth;
+
+    private int mCanvasHeight;
+
+    private Image mOverlayImage;
+
+    private double mOverlayTransparency;
+
+    private boolean mShowOverlay = false;
+
+    public PixelPerfectLoupe(Composite parent) {
+        super(parent, SWT.NONE);
+        mModel = PixelPerfectModel.getModel();
+        mModel.addImageChangeListener(this);
+
+        addPaintListener(mPaintListener);
+        addMouseListener(mMouseListener);
+        addMouseWheelListener(mMouseWheelListener);
+        addDisposeListener(mDisposeListener);
+        addKeyListener(mKeyListener);
+
+        mCrosshairColor = new Color(Display.getDefault(), new RGB(255, 94, 254));
+
+        mTransform = new Transform(Display.getDefault());
+
+        imageLoaded();
+    }
+
+    public void setShowOverlay(boolean value) {
+        synchronized (this) {
+            mShowOverlay = value;
+        }
+        doRedraw();
+    }
+
+    private DisposeListener mDisposeListener = new DisposeListener() {
+        @Override
+        public void widgetDisposed(DisposeEvent e) {
+            mModel.removeImageChangeListener(PixelPerfectLoupe.this);
+            mCrosshairColor.dispose();
+            mTransform.dispose();
+            if (mGrid != null) {
+                mGrid.dispose();
+            }
+        }
+    };
+
+    private MouseListener mMouseListener = new MouseListener() {
+
+        @Override
+        public void mouseDoubleClick(MouseEvent e) {
+            // pass
+        }
+
+        @Override
+        public void mouseDown(MouseEvent e) {
+            handleMouseEvent(e);
+        }
+
+        @Override
+        public void mouseUp(MouseEvent e) {
+            //
+        }
+
+    };
+
+    private MouseWheelListener mMouseWheelListener = new MouseWheelListener() {
+        @Override
+        public void mouseScrolled(MouseEvent e) {
+            int newZoom = -1;
+            synchronized (PixelPerfectLoupe.this) {
+                if (mImage != null && mCrosshairLocation != null) {
+                    if (e.count > 0) {
+                        newZoom = mZoom + 1;
+                    } else {
+                        newZoom = mZoom - 1;
+                    }
+                }
+            }
+            if (newZoom != -1) {
+                mModel.setZoom(newZoom);
+            }
+        }
+    };
+
+    private void handleMouseEvent(MouseEvent e) {
+        int newX = -1;
+        int newY = -1;
+        synchronized (PixelPerfectLoupe.this) {
+            if (mImage == null) {
+                return;
+            }
+            int zoomedX = -mCrosshairLocation.x * mZoom - mZoom / 2 + getBounds().width / 2;
+            int zoomedY = -mCrosshairLocation.y * mZoom - mZoom / 2 + getBounds().height / 2;
+            int x = (e.x - zoomedX) / mZoom;
+            int y = (e.y - zoomedY) / mZoom;
+            if (x >= 0 && x < mWidth && y >= 0 && y < mHeight) {
+                newX = x;
+                newY = y;
+            }
+        }
+        if (newX != -1) {
+            mModel.setCrosshairLocation(newX, newY);
+        }
+    }
+
+    private KeyListener mKeyListener = new KeyListener() {
+
+        @Override
+        public void keyPressed(KeyEvent e) {
+            boolean crosshairMoved = false;
+            synchronized (PixelPerfectLoupe.this) {
+                if (mImage != null) {
+                    switch (e.keyCode) {
+                        case SWT.ARROW_UP:
+                            if (mCrosshairLocation.y != 0) {
+                                mCrosshairLocation.y--;
+                                crosshairMoved = true;
+                            }
+                            break;
+                        case SWT.ARROW_DOWN:
+                            if (mCrosshairLocation.y != mHeight - 1) {
+                                mCrosshairLocation.y++;
+                                crosshairMoved = true;
+                            }
+                            break;
+                        case SWT.ARROW_LEFT:
+                            if (mCrosshairLocation.x != 0) {
+                                mCrosshairLocation.x--;
+                                crosshairMoved = true;
+                            }
+                            break;
+                        case SWT.ARROW_RIGHT:
+                            if (mCrosshairLocation.x != mWidth - 1) {
+                                mCrosshairLocation.x++;
+                                crosshairMoved = true;
+                            }
+                            break;
+                    }
+                }
+            }
+            if (crosshairMoved) {
+                mModel.setCrosshairLocation(mCrosshairLocation.x, mCrosshairLocation.y);
+            }
+        }
+
+        @Override
+        public void keyReleased(KeyEvent e) {
+            // pass
+        }
+
+    };
+
+    private PaintListener mPaintListener = new PaintListener() {
+        @Override
+        public void paintControl(PaintEvent e) {
+            synchronized (PixelPerfectLoupe.this) {
+                e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
+                e.gc.fillRectangle(0, 0, getSize().x, getSize().y);
+                if (mImage != null && mCrosshairLocation != null) {
+                    int zoomedX = -mCrosshairLocation.x * mZoom - mZoom / 2 + getBounds().width / 2;
+                    int zoomedY = -mCrosshairLocation.y * mZoom - mZoom / 2 + getBounds().height / 2;
+                    mTransform.translate(zoomedX, zoomedY);
+                    mTransform.scale(mZoom, mZoom);
+                    e.gc.setInterpolation(SWT.NONE);
+                    e.gc.setTransform(mTransform);
+                    e.gc.drawImage(mImage, 0, 0);
+                    if (mShowOverlay && mOverlayImage != null) {
+                        e.gc.setAlpha((int) (mOverlayTransparency * 255));
+                        e.gc.drawImage(mOverlayImage, 0, mHeight - mOverlayImage.getBounds().height);
+                        e.gc.setAlpha(255);
+                    }
+
+                    mTransform.identity();
+                    e.gc.setTransform(mTransform);
+
+                    // If the size of the canvas has changed, we need to make
+                    // another grid.
+                    if (mGrid != null
+                            && (mCanvasWidth != getBounds().width || mCanvasHeight != getBounds().height)) {
+                        mGrid.dispose();
+                        mGrid = null;
+                    }
+                    mCanvasWidth = getBounds().width;
+                    mCanvasHeight = getBounds().height;
+                    if (mGrid == null) {
+                        // Make a transparent image;
+                        ImageData imageData =
+                                new ImageData(mCanvasWidth + mZoom + 1, mCanvasHeight + mZoom + 1, 1,
+                                        new PaletteData(new RGB[] {
+                                            new RGB(0, 0, 0)
+                                        }));
+                        imageData.transparentPixel = 0;
+
+                        // Draw the grid.
+                        mGrid = new Image(Display.getDefault(), imageData);
+                        GC gc = new GC(mGrid);
+                        gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
+                        for (int x = 0; x <= mCanvasWidth + mZoom; x += mZoom) {
+                            gc.drawLine(x, 0, x, mCanvasHeight + mZoom);
+                        }
+                        for (int y = 0; y <= mCanvasHeight + mZoom; y += mZoom) {
+                            gc.drawLine(0, y, mCanvasWidth + mZoom, y);
+                        }
+                        gc.dispose();
+                    }
+
+                    e.gc.setClipping(new Rectangle(zoomedX, zoomedY, mWidth * mZoom + 1, mHeight
+                            * mZoom + 1));
+                    e.gc.setAlpha(76);
+                    e.gc.drawImage(mGrid, (mCanvasWidth / 2 - mZoom / 2) % mZoom - mZoom,
+                            (mCanvasHeight / 2 - mZoom / 2) % mZoom - mZoom);
+                    e.gc.setAlpha(255);
+
+                    e.gc.setForeground(mCrosshairColor);
+                    e.gc.drawLine(0, mCanvasHeight / 2, mCanvasWidth - 1, mCanvasHeight / 2);
+                    e.gc.drawLine(mCanvasWidth / 2, 0, mCanvasWidth / 2, mCanvasHeight - 1);
+                }
+            }
+        }
+    };
+
+    private void doRedraw() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                redraw();
+            }
+        });
+    }
+
+    private void loadImage() {
+        mImage = mModel.getImage();
+        if (mImage != null) {
+            mWidth = mImage.getBounds().width;
+            mHeight = mImage.getBounds().height;
+        } else {
+            mWidth = 0;
+            mHeight = 0;
+        }
+    }
+
+    // Note the syncExec and then synchronized... It avoids deadlock
+    @Override
+    public void imageLoaded() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                synchronized (this) {
+                    loadImage();
+                    mCrosshairLocation = mModel.getCrosshairLocation();
+                    mZoom = mModel.getZoom();
+                    mOverlayImage = mModel.getOverlayImage();
+                    mOverlayTransparency = mModel.getOverlayTransparency();
+                }
+            }
+        });
+        doRedraw();
+    }
+
+    @Override
+    public void imageChanged() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                synchronized (this) {
+                    loadImage();
+                }
+            }
+        });
+        doRedraw();
+    }
+
+    @Override
+    public void crosshairMoved() {
+        synchronized (this) {
+            mCrosshairLocation = mModel.getCrosshairLocation();
+        }
+        doRedraw();
+    }
+
+    @Override
+    public void selectionChanged() {
+        // pass
+    }
+
+    @Override
+    public void treeChanged() {
+        // pass
+    }
+
+    @Override
+    public void zoomChanged() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                synchronized (this) {
+                    if (mGrid != null) {
+                        // To notify that the zoom level has changed, we get rid
+                        // of the
+                        // grid.
+                        mGrid.dispose();
+                        mGrid = null;
+                    }
+                    mZoom = mModel.getZoom();
+                }
+            }
+        });
+        doRedraw();
+    }
+
+    @Override
+    public void overlayChanged() {
+        synchronized (this) {
+            mOverlayImage = mModel.getOverlayImage();
+            mOverlayTransparency = mModel.getOverlayTransparency();
+        }
+        doRedraw();
+    }
+
+    @Override
+    public void overlayTransparencyChanged() {
+        synchronized (this) {
+            mOverlayTransparency = mModel.getOverlayTransparency();
+        }
+        doRedraw();
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectPixelPanel.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectPixelPanel.java
new file mode 100644
index 0000000..d1ff6d9
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectPixelPanel.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.ui;
+
+import com.android.hierarchyviewerlib.models.PixelPerfectModel;
+import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+
+public class PixelPerfectPixelPanel extends Canvas implements IImageChangeListener {
+    private PixelPerfectModel mModel;
+
+    private Image mImage;
+
+    private Image mOverlayImage;
+
+    private Point mCrosshairLocation;
+
+    public static final int PREFERRED_WIDTH = 180;
+
+    public static final int PREFERRED_HEIGHT = 52;
+
+    public PixelPerfectPixelPanel(Composite parent) {
+        super(parent, SWT.NONE);
+        mModel = PixelPerfectModel.getModel();
+        mModel.addImageChangeListener(this);
+
+        addPaintListener(mPaintListener);
+        addDisposeListener(mDisposeListener);
+
+        imageLoaded();
+    }
+
+    @Override
+    public Point computeSize(int wHint, int hHint, boolean changed) {
+        int height = PREFERRED_HEIGHT;
+        int width = (wHint == SWT.DEFAULT) ? PREFERRED_WIDTH : wHint;
+        return new Point(width, height);
+    }
+
+    private DisposeListener mDisposeListener = new DisposeListener() {
+        @Override
+        public void widgetDisposed(DisposeEvent e) {
+            mModel.removeImageChangeListener(PixelPerfectPixelPanel.this);
+        }
+    };
+
+    private PaintListener mPaintListener = new PaintListener() {
+        @Override
+        public void paintControl(PaintEvent e) {
+            synchronized (PixelPerfectPixelPanel.this) {
+                e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
+                e.gc.fillRectangle(0, 0, getBounds().width, getBounds().height);
+                if (mImage != null) {
+                    RGB pixel =
+                            mImage.getImageData().palette.getRGB(mImage.getImageData().getPixel(
+                                    mCrosshairLocation.x, mCrosshairLocation.y));
+                    Color rgbColor = new Color(Display.getDefault(), pixel);
+                    e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
+                    e.gc.setBackground(rgbColor);
+                    e.gc.drawRectangle(4, 4, 60, 30);
+                    e.gc.fillRectangle(5, 5, 59, 29);
+                    rgbColor.dispose();
+                    e.gc.drawText("#"
+                            + Integer
+                                    .toHexString(
+                                            (1 << 24) + (pixel.red << 16) + (pixel.green << 8)
+                                                    + pixel.blue).substring(1), 4, 35, true);
+                    e.gc.drawText("R:", 80, 4, true);
+                    e.gc.drawText("G:", 80, 20, true);
+                    e.gc.drawText("B:", 80, 35, true);
+                    e.gc.drawText(Integer.toString(pixel.red), 97, 4, true);
+                    e.gc.drawText(Integer.toString(pixel.green), 97, 20, true);
+                    e.gc.drawText(Integer.toString(pixel.blue), 97, 35, true);
+                    e.gc.drawText("X:", 132, 4, true);
+                    e.gc.drawText("Y:", 132, 20, true);
+                    e.gc.drawText(Integer.toString(mCrosshairLocation.x) + " px", 149, 4, true);
+                    e.gc.drawText(Integer.toString(mCrosshairLocation.y) + " px", 149, 20, true);
+
+                    if (mOverlayImage != null) {
+                        int xInOverlay = mCrosshairLocation.x;
+                        int yInOverlay =
+                                mCrosshairLocation.y
+                                        - (mImage.getBounds().height - mOverlayImage.getBounds().height);
+                        if (xInOverlay >= 0 && yInOverlay >= 0
+                                && xInOverlay < mOverlayImage.getBounds().width
+                                && yInOverlay < mOverlayImage.getBounds().height) {
+                            pixel =
+                                    mOverlayImage.getImageData().palette.getRGB(mOverlayImage
+                                            .getImageData().getPixel(xInOverlay, yInOverlay));
+                            rgbColor = new Color(Display.getDefault(), pixel);
+                            e.gc
+                                    .setForeground(Display.getDefault().getSystemColor(
+                                            SWT.COLOR_WHITE));
+                            e.gc.setBackground(rgbColor);
+                            e.gc.drawRectangle(204, 4, 60, 30);
+                            e.gc.fillRectangle(205, 5, 59, 29);
+                            rgbColor.dispose();
+                            e.gc.drawText("#"
+                                    + Integer.toHexString(
+                                            (1 << 24) + (pixel.red << 16) + (pixel.green << 8)
+                                                    + pixel.blue).substring(1), 204, 35, true);
+                            e.gc.drawText("R:", 280, 4, true);
+                            e.gc.drawText("G:", 280, 20, true);
+                            e.gc.drawText("B:", 280, 35, true);
+                            e.gc.drawText(Integer.toString(pixel.red), 297, 4, true);
+                            e.gc.drawText(Integer.toString(pixel.green), 297, 20, true);
+                            e.gc.drawText(Integer.toString(pixel.blue), 297, 35, true);
+                        }
+                    }
+                }
+            }
+        }
+    };
+
+    private void doRedraw() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                redraw();
+            }
+        });
+    }
+
+    @Override
+    public void crosshairMoved() {
+        synchronized (this) {
+            mCrosshairLocation = mModel.getCrosshairLocation();
+        }
+        doRedraw();
+    }
+
+    @Override
+    public void imageChanged() {
+        synchronized (this) {
+            mImage = mModel.getImage();
+        }
+        doRedraw();
+    }
+
+    @Override
+    public void imageLoaded() {
+        synchronized (this) {
+            mImage = mModel.getImage();
+            mCrosshairLocation = mModel.getCrosshairLocation();
+            mOverlayImage = mModel.getOverlayImage();
+        }
+        doRedraw();
+    }
+
+    @Override
+    public void overlayChanged() {
+        synchronized (this) {
+            mOverlayImage = mModel.getOverlayImage();
+        }
+        doRedraw();
+    }
+
+    @Override
+    public void overlayTransparencyChanged() {
+        // pass
+    }
+
+    @Override
+    public void selectionChanged() {
+        // pass
+    }
+
+    @Override
+    public void treeChanged() {
+        // pass
+    }
+
+    @Override
+    public void zoomChanged() {
+        // pass
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectTree.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectTree.java
new file mode 100644
index 0000000..f2b0189
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectTree.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.ui;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewerlib.models.PixelPerfectModel;
+import com.android.hierarchyviewerlib.models.ViewNode;
+import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener;
+
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.TreeSelection;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Tree;
+
+import java.util.List;
+
+public class PixelPerfectTree extends Composite implements IImageChangeListener, SelectionListener {
+
+    private TreeViewer mTreeViewer;
+
+    private Tree mTree;
+
+    private PixelPerfectModel mModel;
+
+    private Image mFolderImage;
+
+    private Image mFileImage;
+
+    private class ContentProvider implements ITreeContentProvider, ILabelProvider {
+        @Override
+        public Object[] getChildren(Object element) {
+            if (element instanceof ViewNode) {
+                List<ViewNode> children = ((ViewNode) element).children;
+                return children.toArray(new ViewNode[children.size()]);
+            }
+            return null;
+        }
+
+        @Override
+        public Object getParent(Object element) {
+            if (element instanceof ViewNode) {
+                return ((ViewNode) element).parent;
+            }
+            return null;
+        }
+
+        @Override
+        public boolean hasChildren(Object element) {
+            if (element instanceof ViewNode) {
+                return ((ViewNode) element).children.size() != 0;
+            }
+            return false;
+        }
+
+        @Override
+        public Object[] getElements(Object element) {
+            if (element instanceof PixelPerfectModel) {
+                ViewNode viewNode = ((PixelPerfectModel) element).getViewNode();
+                if (viewNode == null) {
+                    return new Object[0];
+                }
+                return new Object[] {
+                    viewNode
+                };
+            }
+            return new Object[0];
+        }
+
+        @Override
+        public void dispose() {
+            // pass
+        }
+
+        @Override
+        public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+            // pass
+        }
+
+        @Override
+        public Image getImage(Object element) {
+            if (element instanceof ViewNode) {
+                if (hasChildren(element)) {
+                    return mFolderImage;
+                }
+                return mFileImage;
+            }
+            return null;
+        }
+
+        @Override
+        public String getText(Object element) {
+            if (element instanceof ViewNode) {
+                return ((ViewNode) element).name;
+            }
+            return null;
+        }
+
+        @Override
+        public void addListener(ILabelProviderListener listener) {
+            // pass
+        }
+
+        @Override
+        public boolean isLabelProperty(Object element, String property) {
+            // pass
+            return false;
+        }
+
+        @Override
+        public void removeListener(ILabelProviderListener listener) {
+            // pass
+        }
+    }
+
+    public PixelPerfectTree(Composite parent) {
+        super(parent, SWT.NONE);
+        setLayout(new FillLayout());
+        mTreeViewer = new TreeViewer(this, SWT.SINGLE);
+        mTreeViewer.setAutoExpandLevel(TreeViewer.ALL_LEVELS);
+
+        mTree = mTreeViewer.getTree();
+        mTree.addSelectionListener(this);
+
+        loadResources();
+
+        addDisposeListener(mDisposeListener);
+
+        mModel = PixelPerfectModel.getModel();
+        ContentProvider contentProvider = new ContentProvider();
+        mTreeViewer.setContentProvider(contentProvider);
+        mTreeViewer.setLabelProvider(contentProvider);
+        mTreeViewer.setInput(mModel);
+        mModel.addImageChangeListener(this);
+
+    }
+
+    private void loadResources() {
+        ImageLoader loader = ImageLoader.getDdmUiLibLoader();
+        mFileImage = loader.loadImage("file.png", Display.getDefault());
+        mFolderImage = loader.loadImage("folder.png", Display.getDefault());
+    }
+
+    private DisposeListener mDisposeListener = new DisposeListener() {
+        @Override
+        public void widgetDisposed(DisposeEvent e) {
+            mModel.removeImageChangeListener(PixelPerfectTree.this);
+        }
+    };
+
+    @Override
+    public boolean setFocus() {
+        return mTree.setFocus();
+    }
+
+    @Override
+    public void imageLoaded() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                mTreeViewer.refresh();
+                mTreeViewer.expandAll();
+            }
+        });
+    }
+
+    @Override
+    public void imageChanged() {
+        // pass
+    }
+
+    @Override
+    public void crosshairMoved() {
+        // pass
+    }
+
+    @Override
+    public void selectionChanged() {
+        // pass
+    }
+
+    @Override
+    public void treeChanged() {
+        imageLoaded();
+    }
+
+    @Override
+    public void widgetDefaultSelected(SelectionEvent e) {
+        // pass
+    }
+
+    @Override
+    public void widgetSelected(SelectionEvent e) {
+        // To combat phantom selection...
+        if (((TreeSelection) mTreeViewer.getSelection()).isEmpty()) {
+            mModel.setSelected(null);
+        } else {
+            mModel.setSelected((ViewNode) e.item.getData());
+        }
+    }
+
+    @Override
+    public void zoomChanged() {
+        // pass
+    }
+
+    @Override
+    public void overlayChanged() {
+        // pass
+    }
+
+    @Override
+    public void overlayTransparencyChanged() {
+        // pass
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PropertyViewer.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PropertyViewer.java
new file mode 100644
index 0000000..9456a0a
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PropertyViewer.java
@@ -0,0 +1,391 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.ui;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+import com.android.hierarchyviewerlib.device.IHvDevice;
+import com.android.hierarchyviewerlib.models.TreeViewModel;
+import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener;
+import com.android.hierarchyviewerlib.models.ViewNode;
+import com.android.hierarchyviewerlib.models.ViewNode.Property;
+import com.android.hierarchyviewerlib.ui.DevicePropertyEditingSupport.PropertyType;
+import com.android.hierarchyviewerlib.ui.util.DrawableViewNode;
+import com.android.hierarchyviewerlib.ui.util.TreeColumnResizer;
+
+import org.eclipse.jface.viewers.CellEditor;
+import org.eclipse.jface.viewers.ColumnViewer;
+import org.eclipse.jface.viewers.ComboBoxCellEditor;
+import org.eclipse.jface.viewers.EditingSupport;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.TextCellEditor;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.TreeViewerColumn;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.ControlListener;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeColumn;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+public class PropertyViewer extends Composite implements ITreeChangeListener {
+    private TreeViewModel mModel;
+
+    private TreeViewer mTreeViewer;
+    private Tree mTree;
+    private TreeViewerColumn mValueColumn;
+    private PropertyValueEditingSupport mPropertyValueEditingSupport;
+
+    private Image mImage;
+
+    private DrawableViewNode mSelectedNode;
+
+    private class ContentProvider implements ITreeContentProvider, ITableLabelProvider {
+
+        @Override
+        public Object[] getChildren(Object parentElement) {
+            synchronized (PropertyViewer.this) {
+                if (mSelectedNode != null && parentElement instanceof String) {
+                    String category = (String) parentElement;
+                    ArrayList<Property> returnValue = new ArrayList<Property>();
+                    for (Property property : mSelectedNode.viewNode.properties) {
+                        if (category.equals(ViewNode.MISCELLANIOUS)) {
+                            if (property.name.indexOf(':') == -1) {
+                                returnValue.add(property);
+                            }
+                        } else {
+                            if (property.name.startsWith(((String) parentElement) + ":")) {
+                                returnValue.add(property);
+                            }
+                        }
+                    }
+                    return returnValue.toArray(new Property[returnValue.size()]);
+                }
+                return new Object[0];
+            }
+        }
+
+        @Override
+        public Object getParent(Object element) {
+            synchronized (PropertyViewer.this) {
+                if (mSelectedNode != null && element instanceof Property) {
+                    if (mSelectedNode.viewNode.categories.size() == 0) {
+                        return null;
+                    }
+                    String name = ((Property) element).name;
+                    int index = name.indexOf(':');
+                    if (index == -1) {
+                        return ViewNode.MISCELLANIOUS;
+                    }
+                    return name.substring(0, index);
+                }
+                return null;
+            }
+        }
+
+        @Override
+        public boolean hasChildren(Object element) {
+            synchronized (PropertyViewer.this) {
+                if (mSelectedNode != null && element instanceof String) {
+                    String category = (String) element;
+                    for (String name : mSelectedNode.viewNode.namedProperties.keySet()) {
+                        if (category.equals(ViewNode.MISCELLANIOUS)) {
+                            if (name.indexOf(':') == -1) {
+                                return true;
+                            }
+                        } else {
+                            if (name.startsWith(((String) element) + ":")) {
+                                return true;
+                            }
+                        }
+                    }
+                }
+                return false;
+            }
+        }
+
+        @Override
+        public Object[] getElements(Object inputElement) {
+            synchronized (PropertyViewer.this) {
+                if (mSelectedNode != null && inputElement instanceof TreeViewModel) {
+                    if (mSelectedNode.viewNode.categories.size() == 0) {
+                        return mSelectedNode.viewNode.properties
+                                .toArray(new Property[mSelectedNode.viewNode.properties.size()]);
+                    } else {
+                        return mSelectedNode.viewNode.categories
+                                .toArray(new String[mSelectedNode.viewNode.categories.size()]);
+                    }
+                }
+                return new Object[0];
+            }
+        }
+
+        @Override
+        public void dispose() {
+            // pass
+        }
+
+        @Override
+        public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+            // pass
+        }
+
+        @Override
+        public Image getColumnImage(Object element, int column) {
+            if (mSelectedNode == null) {
+                return null;
+            }
+            if (column == 1 && mPropertyValueEditingSupport.canEdit(element)) {
+                return mImage;
+            }
+
+            return null;
+        }
+
+        @Override
+        public String getColumnText(Object element, int column) {
+            synchronized (PropertyViewer.this) {
+                if (mSelectedNode != null) {
+                    if (element instanceof String && column == 0) {
+                        String category = (String) element;
+                        return Character.toUpperCase(category.charAt(0)) + category.substring(1);
+                    } else if (element instanceof Property) {
+                        if (column == 0) {
+                            String returnValue = ((Property) element).name;
+                            int index = returnValue.indexOf(':');
+                            if (index != -1) {
+                                return returnValue.substring(index + 1);
+                            }
+                            return returnValue;
+                        } else if (column == 1) {
+                            return ((Property) element).value;
+                        }
+                    }
+                }
+                return "";
+            }
+        }
+
+        @Override
+        public void addListener(ILabelProviderListener listener) {
+            // pass
+        }
+
+        @Override
+        public boolean isLabelProperty(Object element, String property) {
+            // pass
+            return false;
+        }
+
+        @Override
+        public void removeListener(ILabelProviderListener listener) {
+            // pass
+        }
+    }
+
+    private class PropertyValueEditingSupport extends EditingSupport {
+        private DevicePropertyEditingSupport mDevicePropertyEditingSupport =
+                new DevicePropertyEditingSupport();
+
+        public PropertyValueEditingSupport(ColumnViewer viewer) {
+            super(viewer);
+        }
+
+        @Override
+        protected boolean canEdit(Object element) {
+            if (mSelectedNode == null) {
+                return false;
+            }
+
+            return element instanceof Property
+                    && mSelectedNode.viewNode.window.getHvDevice().isViewUpdateEnabled()
+                    && mDevicePropertyEditingSupport.canEdit((Property) element);
+        }
+
+        @Override
+        protected CellEditor getCellEditor(Object element) {
+            Property p = (Property) element;
+            PropertyType type = mDevicePropertyEditingSupport.getPropertyType(p);
+            Composite parent = (Composite) getViewer().getControl();
+
+            switch (type) {
+                case INTEGER:
+                case INTEGER_OR_CONSTANT:
+                    return new TextCellEditor(parent);
+                case ENUM:
+                    String[] items = mDevicePropertyEditingSupport.getPropertyRange(p);
+                    return new ComboBoxCellEditor(parent, items, SWT.READ_ONLY);
+            }
+
+            return null;
+        }
+
+        @Override
+        protected Object getValue(Object element) {
+            Property p = (Property) element;
+            PropertyType type = mDevicePropertyEditingSupport.getPropertyType(p);
+
+            if (type == PropertyType.ENUM) {
+                // for enums, return the index of the current value in the list of possible values
+                String[] items = mDevicePropertyEditingSupport.getPropertyRange(p);
+                return Integer.valueOf(indexOf(p.value, items));
+            }
+
+            return ((Property) element).value;
+        }
+
+        private int indexOf(String item, String[] items) {
+            for (int i = 0; i < items.length; i++) {
+                if (items[i].equals(item)) {
+                    return i;
+                }
+            }
+
+            return -1;
+        }
+
+        @Override
+        protected void setValue(Object element, Object newValue) {
+            Property p = (Property) element;
+            IHvDevice device = mSelectedNode.viewNode.window.getHvDevice();
+            Collection<Property> properties = mSelectedNode.viewNode.namedProperties.values();
+            if (mDevicePropertyEditingSupport.setValue(properties, p, newValue,
+                    mSelectedNode.viewNode, device)) {
+                doRefresh();
+            }
+        }
+    }
+
+    public PropertyViewer(Composite parent) {
+        super(parent, SWT.NONE);
+        setLayout(new FillLayout());
+        mTreeViewer = new TreeViewer(this, SWT.NONE);
+
+        mTree = mTreeViewer.getTree();
+        mTree.setLinesVisible(true);
+        mTree.setHeaderVisible(true);
+
+        TreeColumn propertyColumn = new TreeColumn(mTree, SWT.NONE);
+        propertyColumn.setText("Property");
+        TreeColumn valueColumn = new TreeColumn(mTree, SWT.NONE);
+        valueColumn.setText("Value");
+
+        mValueColumn = new TreeViewerColumn(mTreeViewer, valueColumn);
+        mPropertyValueEditingSupport = new PropertyValueEditingSupport(mTreeViewer);
+        mValueColumn.setEditingSupport(mPropertyValueEditingSupport);
+
+        mModel = TreeViewModel.getModel();
+        ContentProvider contentProvider = new ContentProvider();
+        mTreeViewer.setContentProvider(contentProvider);
+        mTreeViewer.setLabelProvider(contentProvider);
+        mTreeViewer.setInput(mModel);
+        mModel.addTreeChangeListener(this);
+
+        addDisposeListener(mDisposeListener);
+
+        @SuppressWarnings("unused")
+        TreeColumnResizer resizer = new TreeColumnResizer(this, propertyColumn, valueColumn);
+
+        addControlListener(mControlListener);
+
+        ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class);
+        mImage = imageLoader.loadImage("picker.png", Display.getDefault()); //$NON-NLS-1$
+
+        treeChanged();
+    }
+
+    private DisposeListener mDisposeListener = new DisposeListener() {
+        @Override
+        public void widgetDisposed(DisposeEvent e) {
+            mModel.removeTreeChangeListener(PropertyViewer.this);
+        }
+    };
+
+    // If the window gets too small, hide the data, otherwise SWT throws an
+    // ERROR.
+
+    private ControlListener mControlListener = new ControlAdapter() {
+        private boolean noInput = false;
+
+        private boolean noHeader = false;
+
+        @Override
+        public void controlResized(ControlEvent e) {
+            if (getBounds().height <= 20) {
+                mTree.setHeaderVisible(false);
+                noHeader = true;
+            } else if (noHeader) {
+                mTree.setHeaderVisible(true);
+                noHeader = false;
+            }
+            if (getBounds().height <= 38) {
+                mTreeViewer.setInput(null);
+                noInput = true;
+            } else if (noInput) {
+                mTreeViewer.setInput(mModel);
+                noInput = false;
+            }
+        }
+    };
+
+    @Override
+    public void selectionChanged() {
+        synchronized (this) {
+            mSelectedNode = mModel.getSelection();
+        }
+        doRefresh();
+    }
+
+    @Override
+    public void treeChanged() {
+        synchronized (this) {
+            mSelectedNode = mModel.getSelection();
+        }
+        doRefresh();
+    }
+
+    @Override
+    public void viewportChanged() {
+        // pass
+    }
+
+    @Override
+    public void zoomChanged() {
+        // pass
+    }
+
+    private void doRefresh() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                mTreeViewer.refresh();
+            }
+        });
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeView.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeView.java
new file mode 100644
index 0000000..5617239
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeView.java
@@ -0,0 +1,1086 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.ui;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+import com.android.hierarchyviewerlib.models.TreeViewModel;
+import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener;
+import com.android.hierarchyviewerlib.models.ViewNode.ProfileRating;
+import com.android.hierarchyviewerlib.ui.util.DrawableViewNode;
+import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Point;
+import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Rectangle;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.events.MouseMoveListener;
+import org.eclipse.swt.events.MouseWheelListener;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Path;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.graphics.Transform;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+
+import java.text.DecimalFormat;
+
+public class TreeView extends Canvas implements ITreeChangeListener {
+
+    private TreeViewModel mModel;
+
+    private DrawableViewNode mTree;
+
+    private DrawableViewNode mSelectedNode;
+
+    private Rectangle mViewport;
+
+    private Transform mTransform;
+
+    private Transform mInverse;
+
+    private double mZoom;
+
+    private Point mLastPoint;
+
+    private boolean mAlreadySelectedOnMouseDown;
+
+    private boolean mDoubleClicked;
+
+    private boolean mNodeMoved;
+
+    private DrawableViewNode mDraggedNode;
+
+    public static final int LINE_PADDING = 10;
+
+    public static final float BEZIER_FRACTION = 0.35f;
+
+    private static Image sRedImage;
+
+    private static Image sYellowImage;
+
+    private static Image sGreenImage;
+
+    private static Image sNotSelectedImage;
+
+    private static Image sSelectedImage;
+
+    private static Image sFilteredImage;
+
+    private static Image sFilteredSelectedImage;
+
+    private static Font sSystemFont;
+
+    private Color mBoxColor;
+
+    private Color mTextBackgroundColor;
+
+    private Rectangle mSelectedRectangleLocation;
+
+    private Point mButtonCenter;
+
+    private static final int BUTTON_SIZE = 13;
+
+    private Image mScaledSelectedImage;
+
+    private boolean mButtonClicked;
+
+    private DrawableViewNode mLastDrawnSelectedViewNode;
+
+    // The profile-image box needs to be moved to,
+    // so add some dragging leeway.
+    private static final int DRAG_LEEWAY = 220;
+
+    // Profile-image box constants
+    private static final int RECT_WIDTH = 190;
+
+    private static final int RECT_HEIGHT = 224;
+
+    private static final int BUTTON_RIGHT_OFFSET = 5;
+
+    private static final int BUTTON_TOP_OFFSET = 5;
+
+    private static final int IMAGE_WIDTH = 125;
+
+    private static final int IMAGE_HEIGHT = 120;
+
+    private static final int IMAGE_OFFSET = 6;
+
+    private static final int IMAGE_ROUNDING = 8;
+
+    private static final int RECTANGLE_SIZE = 5;
+
+    private static final int TEXT_SIDE_OFFSET = 8;
+
+    private static final int TEXT_TOP_OFFSET = 4;
+
+    private static final int TEXT_SPACING = 2;
+
+    private static final int TEXT_ROUNDING = 20;
+
+    public TreeView(Composite parent) {
+        super(parent, SWT.NONE);
+
+        mModel = TreeViewModel.getModel();
+        mModel.addTreeChangeListener(this);
+
+        addPaintListener(mPaintListener);
+        addMouseListener(mMouseListener);
+        addMouseMoveListener(mMouseMoveListener);
+        addMouseWheelListener(mMouseWheelListener);
+        addListener(SWT.Resize, mResizeListener);
+        addDisposeListener(mDisposeListener);
+        addKeyListener(mKeyListener);
+
+        loadResources();
+
+        mTransform = new Transform(Display.getDefault());
+        mInverse = new Transform(Display.getDefault());
+
+        loadAllData();
+    }
+
+    private void loadResources() {
+        ImageLoader loader = ImageLoader.getLoader(this.getClass());
+        sRedImage = loader.loadImage("red.png", Display.getDefault()); //$NON-NLS-1$
+        sYellowImage = loader.loadImage("yellow.png", Display.getDefault()); //$NON-NLS-1$
+        sGreenImage = loader.loadImage("green.png", Display.getDefault()); //$NON-NLS-1$
+        sNotSelectedImage = loader.loadImage("not-selected.png", Display.getDefault()); //$NON-NLS-1$
+        sSelectedImage = loader.loadImage("selected.png", Display.getDefault()); //$NON-NLS-1$
+        sFilteredImage = loader.loadImage("filtered.png", Display.getDefault()); //$NON-NLS-1$
+        sFilteredSelectedImage = loader.loadImage("selected-filtered.png", Display.getDefault()); //$NON-NLS-1$
+        mBoxColor = new Color(Display.getDefault(), new RGB(225, 225, 225));
+        mTextBackgroundColor = new Color(Display.getDefault(), new RGB(82, 82, 82));
+        if (mScaledSelectedImage != null) {
+            mScaledSelectedImage.dispose();
+        }
+        sSystemFont = Display.getDefault().getSystemFont();
+    }
+
+    private DisposeListener mDisposeListener = new DisposeListener() {
+        @Override
+        public void widgetDisposed(DisposeEvent e) {
+            mModel.removeTreeChangeListener(TreeView.this);
+            mTransform.dispose();
+            mInverse.dispose();
+            mBoxColor.dispose();
+            mTextBackgroundColor.dispose();
+            if (mTree != null) {
+                mModel.setViewport(null);
+            }
+        }
+    };
+
+    private Listener mResizeListener = new Listener() {
+        @Override
+        public void handleEvent(Event e) {
+            synchronized (TreeView.this) {
+                if (mTree != null && mViewport != null) {
+
+                    // Keep the center in the same place.
+                    Point viewCenter =
+                            new Point(mViewport.x + mViewport.width / 2, mViewport.y + mViewport.height
+                                    / 2);
+                    mViewport.width = getBounds().width / mZoom;
+                    mViewport.height = getBounds().height / mZoom;
+                    mViewport.x = viewCenter.x - mViewport.width / 2;
+                    mViewport.y = viewCenter.y - mViewport.height / 2;
+                }
+            }
+            if (mViewport != null) {
+                mModel.setViewport(mViewport);
+            }
+        }
+    };
+
+    private KeyListener mKeyListener = new KeyListener() {
+
+        @Override
+        public void keyPressed(KeyEvent e) {
+            boolean selectionChanged = false;
+            DrawableViewNode clickedNode = null;
+            synchronized (TreeView.this) {
+                if (mTree != null && mViewport != null && mSelectedNode != null) {
+                    switch (e.keyCode) {
+                        case SWT.ARROW_LEFT:
+                            if (mSelectedNode.parent != null) {
+                                mSelectedNode = mSelectedNode.parent;
+                                selectionChanged = true;
+                            }
+                            break;
+                        case SWT.ARROW_UP:
+
+                            // On up and down, it is cool to go up and down only
+                            // the leaf nodes.
+                            // It goes well with the layout viewer
+                            DrawableViewNode currentNode = mSelectedNode;
+                            while (currentNode.parent != null && currentNode.viewNode.index == 0) {
+                                currentNode = currentNode.parent;
+                            }
+                            if (currentNode.parent != null) {
+                                selectionChanged = true;
+                                currentNode =
+                                        currentNode.parent.children
+                                                .get(currentNode.viewNode.index - 1);
+                                while (currentNode.children.size() != 0) {
+                                    currentNode =
+                                            currentNode.children
+                                                    .get(currentNode.children.size() - 1);
+                                }
+                            }
+                            if (selectionChanged) {
+                                mSelectedNode = currentNode;
+                            }
+                            break;
+                        case SWT.ARROW_DOWN:
+                            currentNode = mSelectedNode;
+                            while (currentNode.parent != null
+                                    && currentNode.viewNode.index + 1 == currentNode.parent.children
+                                            .size()) {
+                                currentNode = currentNode.parent;
+                            }
+                            if (currentNode.parent != null) {
+                                selectionChanged = true;
+                                currentNode =
+                                        currentNode.parent.children
+                                                .get(currentNode.viewNode.index + 1);
+                                while (currentNode.children.size() != 0) {
+                                    currentNode = currentNode.children.get(0);
+                                }
+                            }
+                            if (selectionChanged) {
+                                mSelectedNode = currentNode;
+                            }
+                            break;
+                        case SWT.ARROW_RIGHT:
+                            DrawableViewNode rightNode = null;
+                            double mostOverlap = 0;
+                            final int N = mSelectedNode.children.size();
+
+                            // We consider all the children and pick the one
+                            // who's tree overlaps the most.
+                            for (int i = 0; i < N; i++) {
+                                DrawableViewNode child = mSelectedNode.children.get(i);
+                                DrawableViewNode topMostChild = child;
+                                while (topMostChild.children.size() != 0) {
+                                    topMostChild = topMostChild.children.get(0);
+                                }
+                                double overlap =
+                                        Math.min(DrawableViewNode.NODE_HEIGHT, Math.min(
+                                                mSelectedNode.top + DrawableViewNode.NODE_HEIGHT
+                                                        - topMostChild.top, topMostChild.top
+                                                        + child.treeHeight - mSelectedNode.top));
+                                if (overlap > mostOverlap) {
+                                    mostOverlap = overlap;
+                                    rightNode = child;
+                                }
+                            }
+                            if (rightNode != null) {
+                                mSelectedNode = rightNode;
+                                selectionChanged = true;
+                            }
+                            break;
+                        case SWT.CR:
+                            clickedNode = mSelectedNode;
+                            break;
+                    }
+                }
+            }
+            if (selectionChanged) {
+                mModel.setSelection(mSelectedNode);
+            }
+            if (clickedNode != null) {
+                HierarchyViewerDirector.getDirector().showCapture(getShell(), clickedNode.viewNode);
+            }
+        }
+
+        @Override
+        public void keyReleased(KeyEvent e) {
+        }
+    };
+
+    private MouseListener mMouseListener = new MouseListener() {
+
+        @Override
+        public void mouseDoubleClick(MouseEvent e) {
+            DrawableViewNode clickedNode = null;
+            synchronized (TreeView.this) {
+                if (mTree != null && mViewport != null) {
+                    Point pt = transformPoint(e.x, e.y);
+                    clickedNode = mTree.getSelected(pt.x, pt.y);
+                }
+            }
+            if (clickedNode != null) {
+                HierarchyViewerDirector.getDirector().showCapture(getShell(), clickedNode.viewNode);
+                mDoubleClicked = true;
+            }
+        }
+
+        @Override
+        public void mouseDown(MouseEvent e) {
+            boolean selectionChanged = false;
+            synchronized (TreeView.this) {
+                if (mTree != null && mViewport != null) {
+                    Point pt = transformPoint(e.x, e.y);
+
+                    // Ignore profiling rectangle, except for...
+                    if (mSelectedRectangleLocation != null
+                            && pt.x >= mSelectedRectangleLocation.x
+                            && pt.x < mSelectedRectangleLocation.x
+                                    + mSelectedRectangleLocation.width
+                            && pt.y >= mSelectedRectangleLocation.y
+                            && pt.y < mSelectedRectangleLocation.y
+                                    + mSelectedRectangleLocation.height) {
+
+                        // the small button!
+                        if ((pt.x - mButtonCenter.x) * (pt.x - mButtonCenter.x)
+                                + (pt.y - mButtonCenter.y) * (pt.y - mButtonCenter.y) <= (BUTTON_SIZE * BUTTON_SIZE) / 4) {
+                            mButtonClicked = true;
+                            doRedraw();
+                        }
+                        return;
+                    }
+                    mDraggedNode = mTree.getSelected(pt.x, pt.y);
+
+                    // Update the selection.
+                    if (mDraggedNode != null && mDraggedNode != mSelectedNode) {
+                        mSelectedNode = mDraggedNode;
+                        selectionChanged = true;
+                        mAlreadySelectedOnMouseDown = false;
+                    } else if (mDraggedNode != null) {
+                        mAlreadySelectedOnMouseDown = true;
+                    }
+
+                    // Can't drag the root.
+                    if (mDraggedNode == mTree) {
+                        mDraggedNode = null;
+                    }
+
+                    if (mDraggedNode != null) {
+                        mLastPoint = pt;
+                    } else {
+                        mLastPoint = new Point(e.x, e.y);
+                    }
+                    mNodeMoved = false;
+                    mDoubleClicked = false;
+                }
+            }
+            if (selectionChanged) {
+                mModel.setSelection(mSelectedNode);
+            }
+        }
+
+        @Override
+        public void mouseUp(MouseEvent e) {
+            boolean redraw = false;
+            boolean redrawButton = false;
+            boolean viewportChanged = false;
+            boolean selectionChanged = false;
+            synchronized (TreeView.this) {
+                if (mTree != null && mViewport != null && mLastPoint != null) {
+                    if (mDraggedNode == null) {
+                        // The viewport moves.
+                        handleMouseDrag(new Point(e.x, e.y));
+                        viewportChanged = true;
+                    } else {
+                        // The nodes move.
+                        handleMouseDrag(transformPoint(e.x, e.y));
+                    }
+
+                    // Deselect on the second click...
+                    // This is in the mouse up, because mouse up happens after a
+                    // double click event.
+                    // During a double click, we don't want to deselect.
+                    Point pt = transformPoint(e.x, e.y);
+                    DrawableViewNode mouseUpOn = mTree.getSelected(pt.x, pt.y);
+                    if (mouseUpOn != null && mouseUpOn == mSelectedNode
+                            && mAlreadySelectedOnMouseDown && !mNodeMoved && !mDoubleClicked) {
+                        mSelectedNode = null;
+                        selectionChanged = true;
+                    }
+                    mLastPoint = null;
+                    mDraggedNode = null;
+                    redraw = true;
+                }
+
+                // Just clicked the button here.
+                if (mButtonClicked) {
+                    HierarchyViewerDirector.getDirector().showCapture(getShell(),
+                            mSelectedNode.viewNode);
+                    mButtonClicked = false;
+                    redrawButton = true;
+                }
+            }
+
+            // Complicated.
+            if (viewportChanged) {
+                mModel.setViewport(mViewport);
+            } else if (redraw) {
+                mModel.removeTreeChangeListener(TreeView.this);
+                mModel.notifyViewportChanged();
+                if (selectionChanged) {
+                    mModel.setSelection(mSelectedNode);
+                }
+                mModel.addTreeChangeListener(TreeView.this);
+                doRedraw();
+            } else if (redrawButton) {
+                doRedraw();
+            }
+        }
+
+    };
+
+    private MouseMoveListener mMouseMoveListener = new MouseMoveListener() {
+        @Override
+        public void mouseMove(MouseEvent e) {
+            boolean redraw = false;
+            boolean viewportChanged = false;
+            synchronized (TreeView.this) {
+                if (mTree != null && mViewport != null && mLastPoint != null) {
+                    if (mDraggedNode == null) {
+                        handleMouseDrag(new Point(e.x, e.y));
+                        viewportChanged = true;
+                    } else {
+                        handleMouseDrag(transformPoint(e.x, e.y));
+                    }
+                    redraw = true;
+                }
+            }
+            if (viewportChanged) {
+                mModel.setViewport(mViewport);
+            } else if (redraw) {
+                mModel.removeTreeChangeListener(TreeView.this);
+                mModel.notifyViewportChanged();
+                mModel.addTreeChangeListener(TreeView.this);
+                doRedraw();
+            }
+        }
+    };
+
+    private void handleMouseDrag(Point pt) {
+
+        // Case 1: a node is dragged. DrawableViewNode knows how to handle this.
+        if (mDraggedNode != null) {
+            if (mLastPoint.y - pt.y != 0) {
+                mNodeMoved = true;
+            }
+            mDraggedNode.move(mLastPoint.y - pt.y);
+            mLastPoint = pt;
+            return;
+        }
+
+        // Case 2: the viewport is dragged. We have to make sure we respect the
+        // bounds - don't let the user drag way out... + some leeway for the
+        // profiling box.
+        double xDif = (mLastPoint.x - pt.x) / mZoom;
+        double yDif = (mLastPoint.y - pt.y) / mZoom;
+
+        double treeX = mTree.bounds.x - DRAG_LEEWAY;
+        double treeY = mTree.bounds.y - DRAG_LEEWAY;
+        double treeWidth = mTree.bounds.width + 2 * DRAG_LEEWAY;
+        double treeHeight = mTree.bounds.height + 2 * DRAG_LEEWAY;
+
+        if (mViewport.width > treeWidth) {
+            if (xDif < 0 && mViewport.x + mViewport.width > treeX + treeWidth) {
+                mViewport.x = Math.max(mViewport.x + xDif, treeX + treeWidth - mViewport.width);
+            } else if (xDif > 0 && mViewport.x < treeX) {
+                mViewport.x = Math.min(mViewport.x + xDif, treeX);
+            }
+        } else {
+            if (xDif < 0 && mViewport.x > treeX) {
+                mViewport.x = Math.max(mViewport.x + xDif, treeX);
+            } else if (xDif > 0 && mViewport.x + mViewport.width < treeX + treeWidth) {
+                mViewport.x = Math.min(mViewport.x + xDif, treeX + treeWidth - mViewport.width);
+            }
+        }
+        if (mViewport.height > treeHeight) {
+            if (yDif < 0 && mViewport.y + mViewport.height > treeY + treeHeight) {
+                mViewport.y = Math.max(mViewport.y + yDif, treeY + treeHeight - mViewport.height);
+            } else if (yDif > 0 && mViewport.y < treeY) {
+                mViewport.y = Math.min(mViewport.y + yDif, treeY);
+            }
+        } else {
+            if (yDif < 0 && mViewport.y > treeY) {
+                mViewport.y = Math.max(mViewport.y + yDif, treeY);
+            } else if (yDif > 0 && mViewport.y + mViewport.height < treeY + treeHeight) {
+                mViewport.y = Math.min(mViewport.y + yDif, treeY + treeHeight - mViewport.height);
+            }
+        }
+        mLastPoint = pt;
+    }
+
+    private Point transformPoint(double x, double y) {
+        float[] pt = {
+                (float) x, (float) y
+        };
+        mInverse.transform(pt);
+        return new Point(pt[0], pt[1]);
+    }
+
+    private MouseWheelListener mMouseWheelListener = new MouseWheelListener() {
+        @Override
+        public void mouseScrolled(MouseEvent e) {
+            Point zoomPoint = null;
+            synchronized (TreeView.this) {
+                if (mTree != null && mViewport != null) {
+                    mZoom += Math.ceil(e.count / 3.0) * 0.1;
+                    zoomPoint = transformPoint(e.x, e.y);
+                }
+            }
+            if (zoomPoint != null) {
+                mModel.zoomOnPoint(mZoom, zoomPoint);
+            }
+        }
+    };
+
+    private PaintListener mPaintListener = new PaintListener() {
+        @Override
+        public void paintControl(PaintEvent e) {
+            synchronized (TreeView.this) {
+                e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
+                e.gc.fillRectangle(0, 0, getBounds().width, getBounds().height);
+                if (mTree != null && mViewport != null) {
+
+                    // Easy stuff!
+                    e.gc.setTransform(mTransform);
+                    e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
+                    Path connectionPath = new Path(Display.getDefault());
+                    paintRecursive(e.gc, mTransform, mTree, mSelectedNode, connectionPath);
+                    e.gc.drawPath(connectionPath);
+                    connectionPath.dispose();
+
+                    // Draw the profiling box.
+                    if (mSelectedNode != null) {
+
+                        e.gc.setAlpha(200);
+
+                        // Draw the little triangle
+                        int x = mSelectedNode.left + DrawableViewNode.NODE_WIDTH / 2;
+                        int y = (int) mSelectedNode.top + 4;
+                        e.gc.setBackground(mBoxColor);
+                        e.gc.fillPolygon(new int[] {
+                                x, y, x - 11, y - 11, x + 11, y - 11
+                        });
+
+                        // Draw the rectangle and update the location.
+                        y -= 10 + RECT_HEIGHT;
+                        e.gc.fillRoundRectangle(x - RECT_WIDTH / 2, y, RECT_WIDTH, RECT_HEIGHT, 30,
+                                30);
+                        mSelectedRectangleLocation =
+                                new Rectangle(x - RECT_WIDTH / 2, y, RECT_WIDTH, RECT_HEIGHT);
+
+                        e.gc.setAlpha(255);
+
+                        // Draw the button
+                        mButtonCenter =
+                                new Point(x - BUTTON_RIGHT_OFFSET + (RECT_WIDTH - BUTTON_SIZE) / 2,
+                                        y + BUTTON_TOP_OFFSET + BUTTON_SIZE / 2);
+
+                        if (mButtonClicked) {
+                            e.gc
+                                    .setBackground(Display.getDefault().getSystemColor(
+                                            SWT.COLOR_BLACK));
+                        } else {
+                            e.gc.setBackground(mTextBackgroundColor);
+
+                        }
+                        e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
+
+                        e.gc.fillOval(x + RECT_WIDTH / 2 - BUTTON_RIGHT_OFFSET - BUTTON_SIZE, y
+                                + BUTTON_TOP_OFFSET, BUTTON_SIZE, BUTTON_SIZE);
+
+                        e.gc.drawRectangle(x - BUTTON_RIGHT_OFFSET
+                                + (RECT_WIDTH - BUTTON_SIZE - RECTANGLE_SIZE) / 2 - 1, y
+                                + BUTTON_TOP_OFFSET + (BUTTON_SIZE - RECTANGLE_SIZE) / 2,
+                                RECTANGLE_SIZE + 1, RECTANGLE_SIZE);
+
+                        y += 15;
+
+                        // If there is an image, draw it.
+                        if (mSelectedNode.viewNode.image != null
+                                && mSelectedNode.viewNode.image.getBounds().height != 1
+                                && mSelectedNode.viewNode.image.getBounds().width != 1) {
+
+                            // Scaling the image to the right size takes lots of
+                            // time, so we want to do it only once.
+
+                            // If the selection changed, get rid of the old
+                            // image.
+                            if (mLastDrawnSelectedViewNode != mSelectedNode) {
+                                if (mScaledSelectedImage != null) {
+                                    mScaledSelectedImage.dispose();
+                                    mScaledSelectedImage = null;
+                                }
+                                mLastDrawnSelectedViewNode = mSelectedNode;
+                            }
+
+                            if (mScaledSelectedImage == null) {
+                                double ratio =
+                                        1.0 * mSelectedNode.viewNode.image.getBounds().width
+                                                / mSelectedNode.viewNode.image.getBounds().height;
+                                int newWidth, newHeight;
+                                if (ratio > 1.0 * IMAGE_WIDTH / IMAGE_HEIGHT) {
+                                    newWidth =
+                                            Math.min(IMAGE_WIDTH, mSelectedNode.viewNode.image
+                                                    .getBounds().width);
+                                    newHeight = (int) (newWidth / ratio);
+                                } else {
+                                    newHeight =
+                                            Math.min(IMAGE_HEIGHT, mSelectedNode.viewNode.image
+                                                    .getBounds().height);
+                                    newWidth = (int) (newHeight * ratio);
+                                }
+
+                                // Interesting note... We make the image twice
+                                // the needed size so that there is better
+                                // resolution under zoom.
+                                newWidth = Math.max(newWidth * 2, 1);
+                                newHeight = Math.max(newHeight * 2, 1);
+                                mScaledSelectedImage =
+                                        new Image(Display.getDefault(), newWidth, newHeight);
+                                GC gc = new GC(mScaledSelectedImage);
+                                gc.setBackground(mTextBackgroundColor);
+                                gc.fillRectangle(0, 0, newWidth, newHeight);
+                                gc.drawImage(mSelectedNode.viewNode.image, 0, 0,
+                                        mSelectedNode.viewNode.image.getBounds().width,
+                                        mSelectedNode.viewNode.image.getBounds().height, 0, 0,
+                                        newWidth, newHeight);
+                                gc.dispose();
+                            }
+
+                            // Draw the background rectangle
+                            e.gc.setBackground(mTextBackgroundColor);
+                            e.gc.fillRoundRectangle(x - mScaledSelectedImage.getBounds().width / 4
+                                    - IMAGE_OFFSET, y
+                                    + (IMAGE_HEIGHT - mScaledSelectedImage.getBounds().height / 2)
+                                    / 2 - IMAGE_OFFSET, mScaledSelectedImage.getBounds().width / 2
+                                    + 2 * IMAGE_OFFSET, mScaledSelectedImage.getBounds().height / 2
+                                    + 2 * IMAGE_OFFSET, IMAGE_ROUNDING, IMAGE_ROUNDING);
+
+                            // Under max zoom, we want the image to be
+                            // untransformed. So, get back to the identity
+                            // transform.
+                            int imageX = x - mScaledSelectedImage.getBounds().width / 4;
+                            int imageY =
+                                    y
+                                            + (IMAGE_HEIGHT - mScaledSelectedImage.getBounds().height / 2)
+                                            / 2;
+
+                            Transform untransformedTransform = new Transform(Display.getDefault());
+                            e.gc.setTransform(untransformedTransform);
+                            float[] pt = new float[] {
+                                    imageX, imageY
+                            };
+                            mTransform.transform(pt);
+                            e.gc.drawImage(mScaledSelectedImage, 0, 0, mScaledSelectedImage
+                                    .getBounds().width, mScaledSelectedImage.getBounds().height,
+                                    (int) pt[0], (int) pt[1], (int) (mScaledSelectedImage
+                                            .getBounds().width
+                                            * mZoom / 2),
+                                    (int) (mScaledSelectedImage.getBounds().height * mZoom / 2));
+                            untransformedTransform.dispose();
+                            e.gc.setTransform(mTransform);
+                        }
+
+                        // Text stuff
+
+                        y += IMAGE_HEIGHT;
+                        y += 10;
+                        Font font = getFont(8, false);
+                        e.gc.setFont(font);
+
+                        String text =
+                                mSelectedNode.viewNode.viewCount + " view"
+                                        + (mSelectedNode.viewNode.viewCount != 1 ? "s" : "");
+                        DecimalFormat formatter = new DecimalFormat("0.000");
+
+                        String measureText =
+                                "Measure: "
+                                        + (mSelectedNode.viewNode.measureTime != -1 ? formatter
+                                                .format(mSelectedNode.viewNode.measureTime)
+                                                + " ms" : "n/a");
+                        String layoutText =
+                                "Layout: "
+                                        + (mSelectedNode.viewNode.layoutTime != -1 ? formatter
+                                                .format(mSelectedNode.viewNode.layoutTime)
+                                                + " ms" : "n/a");
+                        String drawText =
+                                "Draw: "
+                                        + (mSelectedNode.viewNode.drawTime != -1 ? formatter
+                                                .format(mSelectedNode.viewNode.drawTime)
+                                                + " ms" : "n/a");
+
+                        org.eclipse.swt.graphics.Point titleExtent = e.gc.stringExtent(text);
+                        org.eclipse.swt.graphics.Point measureExtent =
+                                e.gc.stringExtent(measureText);
+                        org.eclipse.swt.graphics.Point layoutExtent = e.gc.stringExtent(layoutText);
+                        org.eclipse.swt.graphics.Point drawExtent = e.gc.stringExtent(drawText);
+                        int boxWidth =
+                                Math.max(titleExtent.x, Math.max(measureExtent.x, Math.max(
+                                        layoutExtent.x, drawExtent.x)))
+                                        + 2 * TEXT_SIDE_OFFSET;
+                        int boxHeight =
+                                titleExtent.y + TEXT_SPACING + measureExtent.y + TEXT_SPACING
+                                        + layoutExtent.y + TEXT_SPACING + drawExtent.y + 2
+                                        * TEXT_TOP_OFFSET;
+
+                        e.gc.setBackground(mTextBackgroundColor);
+                        e.gc.fillRoundRectangle(x - boxWidth / 2, y, boxWidth, boxHeight,
+                                TEXT_ROUNDING, TEXT_ROUNDING);
+
+                        e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
+
+                        y += TEXT_TOP_OFFSET;
+
+                        e.gc.drawText(text, x - titleExtent.x / 2, y, true);
+
+                        x -= boxWidth / 2;
+                        x += TEXT_SIDE_OFFSET;
+
+                        y += titleExtent.y + TEXT_SPACING;
+
+                        e.gc.drawText(measureText, x, y, true);
+
+                        y += measureExtent.y + TEXT_SPACING;
+
+                        e.gc.drawText(layoutText, x, y, true);
+
+                        y += layoutExtent.y + TEXT_SPACING;
+
+                        e.gc.drawText(drawText, x, y, true);
+
+                        font.dispose();
+                    } else {
+                        mSelectedRectangleLocation = null;
+                        mButtonCenter = null;
+                    }
+                }
+            }
+        }
+    };
+
+    private static void paintRecursive(GC gc, Transform transform, DrawableViewNode node,
+            DrawableViewNode selectedNode, Path connectionPath) {
+        if (selectedNode == node && node.viewNode.filtered) {
+            gc.drawImage(sFilteredSelectedImage, node.left, (int) Math.round(node.top));
+        } else if (selectedNode == node) {
+            gc.drawImage(sSelectedImage, node.left, (int) Math.round(node.top));
+        } else if (node.viewNode.filtered) {
+            gc.drawImage(sFilteredImage, node.left, (int) Math.round(node.top));
+        } else {
+            gc.drawImage(sNotSelectedImage, node.left, (int) Math.round(node.top));
+        }
+
+        int fontHeight = gc.getFontMetrics().getHeight();
+
+        // Draw the text...
+        int contentWidth =
+                DrawableViewNode.NODE_WIDTH - 2 * DrawableViewNode.CONTENT_LEFT_RIGHT_PADDING;
+        String name = node.viewNode.name;
+        int dotIndex = name.lastIndexOf('.');
+        if (dotIndex != -1) {
+            name = name.substring(dotIndex + 1);
+        }
+        double x = node.left + DrawableViewNode.CONTENT_LEFT_RIGHT_PADDING;
+        double y = node.top + DrawableViewNode.CONTENT_TOP_BOTTOM_PADDING;
+        drawTextInArea(gc, transform, name, x, y, contentWidth, fontHeight, 10, true);
+
+        y += fontHeight + DrawableViewNode.CONTENT_INTER_PADDING;
+
+        drawTextInArea(gc, transform, "@" + node.viewNode.hashCode, x, y, contentWidth, fontHeight,
+                8, false);
+
+        y += fontHeight + DrawableViewNode.CONTENT_INTER_PADDING;
+        if (!node.viewNode.id.equals("NO_ID")) {
+            drawTextInArea(gc, transform, node.viewNode.id, x, y, contentWidth, fontHeight, 8,
+                    false);
+        }
+
+        if (node.viewNode.measureRating != ProfileRating.NONE) {
+            y =
+                    node.top + DrawableViewNode.NODE_HEIGHT
+                            - DrawableViewNode.CONTENT_TOP_BOTTOM_PADDING
+                            - sRedImage.getBounds().height;
+            x +=
+                    (contentWidth - (sRedImage.getBounds().width * 3 + 2 * DrawableViewNode.CONTENT_INTER_PADDING)) / 2;
+            switch (node.viewNode.measureRating) {
+                case GREEN:
+                    gc.drawImage(sGreenImage, (int) x, (int) y);
+                    break;
+                case YELLOW:
+                    gc.drawImage(sYellowImage, (int) x, (int) y);
+                    break;
+                case RED:
+                    gc.drawImage(sRedImage, (int) x, (int) y);
+                    break;
+            }
+
+            x += sRedImage.getBounds().width + DrawableViewNode.CONTENT_INTER_PADDING;
+            switch (node.viewNode.layoutRating) {
+                case GREEN:
+                    gc.drawImage(sGreenImage, (int) x, (int) y);
+                    break;
+                case YELLOW:
+                    gc.drawImage(sYellowImage, (int) x, (int) y);
+                    break;
+                case RED:
+                    gc.drawImage(sRedImage, (int) x, (int) y);
+                    break;
+            }
+
+            x += sRedImage.getBounds().width + DrawableViewNode.CONTENT_INTER_PADDING;
+            switch (node.viewNode.drawRating) {
+                case GREEN:
+                    gc.drawImage(sGreenImage, (int) x, (int) y);
+                    break;
+                case YELLOW:
+                    gc.drawImage(sYellowImage, (int) x, (int) y);
+                    break;
+                case RED:
+                    gc.drawImage(sRedImage, (int) x, (int) y);
+                    break;
+            }
+        }
+
+        org.eclipse.swt.graphics.Point indexExtent =
+                gc.stringExtent(Integer.toString(node.viewNode.index));
+        x =
+                node.left + DrawableViewNode.NODE_WIDTH - DrawableViewNode.INDEX_PADDING
+                        - indexExtent.x;
+        y =
+                node.top + DrawableViewNode.NODE_HEIGHT - DrawableViewNode.INDEX_PADDING
+                        - indexExtent.y;
+        gc.drawText(Integer.toString(node.viewNode.index), (int) x, (int) y, SWT.DRAW_TRANSPARENT);
+
+        int N = node.children.size();
+        if (N == 0) {
+            return;
+        }
+        float childSpacing = (1.0f * (DrawableViewNode.NODE_HEIGHT - 2 * LINE_PADDING)) / N;
+        for (int i = 0; i < N; i++) {
+            DrawableViewNode child = node.children.get(i);
+            paintRecursive(gc, transform, child, selectedNode, connectionPath);
+            float x1 = node.left + DrawableViewNode.NODE_WIDTH;
+            float y1 = (float) node.top + LINE_PADDING + childSpacing * i + childSpacing / 2;
+            float x2 = child.left;
+            float y2 = (float) child.top + DrawableViewNode.NODE_HEIGHT / 2.0f;
+            float cx1 = x1 + BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING;
+            float cy1 = y1;
+            float cx2 = x2 - BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING;
+            float cy2 = y2;
+            connectionPath.moveTo(x1, y1);
+            connectionPath.cubicTo(cx1, cy1, cx2, cy2, x2, y2);
+        }
+    }
+
+    private static void drawTextInArea(GC gc, Transform transform, String text, double x, double y,
+            double width, double height, int fontSize, boolean bold) {
+
+        Font oldFont = gc.getFont();
+
+        Font newFont = getFont(fontSize, bold);
+        gc.setFont(newFont);
+
+        org.eclipse.swt.graphics.Point extent = gc.stringExtent(text);
+
+        if (extent.x > width) {
+            // Oh no... we need to scale it.
+            double scale = width / extent.x;
+            float[] transformElements = new float[6];
+            transform.getElements(transformElements);
+            transform.scale((float) scale, (float) scale);
+            gc.setTransform(transform);
+
+            x /= scale;
+            y /= scale;
+            y += (extent.y / scale - extent.y) / 2;
+
+            gc.drawText(text, (int) x, (int) y, SWT.DRAW_TRANSPARENT);
+
+            transform.setElements(transformElements[0], transformElements[1], transformElements[2],
+                    transformElements[3], transformElements[4], transformElements[5]);
+            gc.setTransform(transform);
+        } else {
+            gc.drawText(text, (int) (x + (width - extent.x) / 2),
+                    (int) (y + (height - extent.y) / 2), SWT.DRAW_TRANSPARENT);
+        }
+        gc.setFont(oldFont);
+        newFont.dispose();
+
+    }
+
+    public static Image paintToImage(DrawableViewNode tree) {
+        Image image =
+                new Image(Display.getDefault(), (int) Math.ceil(tree.bounds.width), (int) Math
+                        .ceil(tree.bounds.height));
+
+        Transform transform = new Transform(Display.getDefault());
+        transform.identity();
+        transform.translate((float) -tree.bounds.x, (float) -tree.bounds.y);
+        Path connectionPath = new Path(Display.getDefault());
+        GC gc = new GC(image);
+
+        // Can't use Display.getDefault().getSystemColor in a non-UI thread.
+        Color white = new Color(Display.getDefault(), 255, 255, 255);
+        Color black = new Color(Display.getDefault(), 0, 0, 0);
+        gc.setForeground(white);
+        gc.setBackground(black);
+        gc.fillRectangle(0, 0, image.getBounds().width, image.getBounds().height);
+        gc.setTransform(transform);
+        paintRecursive(gc, transform, tree, null, connectionPath);
+        gc.drawPath(connectionPath);
+        gc.dispose();
+        connectionPath.dispose();
+        white.dispose();
+        black.dispose();
+        return image;
+    }
+
+    private static Font getFont(int size, boolean bold) {
+        FontData[] fontData = sSystemFont.getFontData();
+        for (int i = 0; i < fontData.length; i++) {
+            fontData[i].setHeight(size);
+            if (bold) {
+                fontData[i].setStyle(SWT.BOLD);
+            }
+        }
+        return new Font(Display.getDefault(), fontData);
+    }
+
+    private void doRedraw() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                redraw();
+            }
+        });
+    }
+
+    public void loadAllData() {
+        boolean newViewport = mViewport == null;
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                synchronized (this) {
+                    mTree = mModel.getTree();
+                    mSelectedNode = mModel.getSelection();
+                    mViewport = mModel.getViewport();
+                    mZoom = mModel.getZoom();
+                    if (mTree != null && mViewport == null) {
+                        mViewport =
+                                new Rectangle(0, mTree.top + DrawableViewNode.NODE_HEIGHT / 2
+                                        - getBounds().height / 2, getBounds().width,
+                                        getBounds().height);
+                    } else {
+                        setTransform();
+                    }
+                }
+            }
+        });
+        if (newViewport) {
+            mModel.setViewport(mViewport);
+        }
+    }
+
+    // Fickle behaviour... When a new tree is loaded, the model doesn't know
+    // about the viewport until it passes through here.
+    @Override
+    public void treeChanged() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                synchronized (this) {
+                    mTree = mModel.getTree();
+                    mSelectedNode = mModel.getSelection();
+                    if (mTree == null) {
+                        mViewport = null;
+                    } else {
+                        mViewport =
+                                new Rectangle(0, mTree.top + DrawableViewNode.NODE_HEIGHT / 2
+                                        - getBounds().height / 2, getBounds().width,
+                                        getBounds().height);
+                    }
+                }
+            }
+        });
+        if (mViewport != null) {
+            mModel.setViewport(mViewport);
+        } else {
+            doRedraw();
+        }
+    }
+
+    private void setTransform() {
+        if (mViewport != null && mTree != null) {
+            // Set the transform.
+            mTransform.identity();
+            mInverse.identity();
+
+            mTransform.scale((float) mZoom, (float) mZoom);
+            mInverse.scale((float) mZoom, (float) mZoom);
+            mTransform.translate((float) -mViewport.x, (float) -mViewport.y);
+            mInverse.translate((float) -mViewport.x, (float) -mViewport.y);
+            mInverse.invert();
+        }
+    }
+
+    // Note the syncExec and then synchronized... It avoids deadlock
+    @Override
+    public void viewportChanged() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                synchronized (this) {
+                    mViewport = mModel.getViewport();
+                    mZoom = mModel.getZoom();
+                    setTransform();
+                }
+            }
+        });
+        doRedraw();
+    }
+
+    @Override
+    public void zoomChanged() {
+        viewportChanged();
+    }
+
+    @Override
+    public void selectionChanged() {
+        synchronized (this) {
+            mSelectedNode = mModel.getSelection();
+            if (mSelectedNode != null && mSelectedNode.viewNode.image == null) {
+                HierarchyViewerDirector.getDirector()
+                        .loadCaptureInBackground(mSelectedNode.viewNode);
+            }
+        }
+        doRedraw();
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewControls.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewControls.java
new file mode 100644
index 0000000..fc03f13
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewControls.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.ui;
+
+import com.android.hierarchyviewerlib.HierarchyViewerDirector;
+import com.android.hierarchyviewerlib.models.TreeViewModel;
+import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Slider;
+import org.eclipse.swt.widgets.Text;
+
+public class TreeViewControls extends Composite implements ITreeChangeListener {
+
+    private Text mFilterText;
+
+    private Slider mZoomSlider;
+
+    public TreeViewControls(Composite parent) {
+        super(parent, SWT.NONE);
+        GridLayout layout = new GridLayout(5, false);
+        layout.marginWidth = layout.marginHeight = 2;
+        layout.verticalSpacing = layout.horizontalSpacing = 4;
+        setLayout(layout);
+
+        Label filterLabel = new Label(this, SWT.NONE);
+        filterLabel.setText("Filter by class or id:");
+        filterLabel.setLayoutData(new GridData(GridData.BEGINNING, GridData.CENTER, false, true));
+
+        mFilterText = new Text(this, SWT.LEFT | SWT.SINGLE);
+        mFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mFilterText.addModifyListener(mFilterTextModifyListener);
+        mFilterText.setText(HierarchyViewerDirector.getDirector().getFilterText());
+
+        Label smallZoomLabel = new Label(this, SWT.NONE);
+        smallZoomLabel.setText(" 20%");
+        smallZoomLabel
+                .setLayoutData(new GridData(GridData.BEGINNING, GridData.CENTER, false, true));
+
+        mZoomSlider = new Slider(this, SWT.HORIZONTAL);
+        GridData zoomSliderGridData = new GridData(GridData.CENTER, GridData.CENTER, false, false);
+        zoomSliderGridData.widthHint = 190;
+        mZoomSlider.setLayoutData(zoomSliderGridData);
+        mZoomSlider.setMinimum((int) (TreeViewModel.MIN_ZOOM * 10));
+        mZoomSlider.setMaximum((int) (TreeViewModel.MAX_ZOOM * 10 + 1));
+        mZoomSlider.setThumb(1);
+        mZoomSlider.setSelection((int) Math.round(TreeViewModel.getModel().getZoom() * 10));
+
+        mZoomSlider.addSelectionListener(mZoomSliderSelectionListener);
+
+        Label largeZoomLabel = new Label(this, SWT.NONE);
+        largeZoomLabel
+                .setLayoutData(new GridData(GridData.BEGINNING, GridData.CENTER, false, true));
+        largeZoomLabel.setText("200%");
+
+        addDisposeListener(mDisposeListener);
+
+        TreeViewModel.getModel().addTreeChangeListener(this);
+    }
+
+    private DisposeListener mDisposeListener = new DisposeListener() {
+        @Override
+        public void widgetDisposed(DisposeEvent e) {
+            TreeViewModel.getModel().removeTreeChangeListener(TreeViewControls.this);
+        }
+    };
+
+    private SelectionListener mZoomSliderSelectionListener = new SelectionListener() {
+        private int oldValue;
+
+        @Override
+        public void widgetDefaultSelected(SelectionEvent e) {
+            // pass
+        }
+
+        @Override
+        public void widgetSelected(SelectionEvent e) {
+            int newValue = mZoomSlider.getSelection();
+            if (oldValue != newValue) {
+                TreeViewModel.getModel().removeTreeChangeListener(TreeViewControls.this);
+                TreeViewModel.getModel().setZoom(newValue / 10.0);
+                TreeViewModel.getModel().addTreeChangeListener(TreeViewControls.this);
+                oldValue = newValue;
+            }
+        }
+    };
+
+    private ModifyListener mFilterTextModifyListener = new ModifyListener() {
+        @Override
+        public void modifyText(ModifyEvent e) {
+            HierarchyViewerDirector.getDirector().filterNodes(mFilterText.getText());
+        }
+    };
+
+    @Override
+    public void selectionChanged() {
+        // pass
+    }
+
+    @Override
+    public void treeChanged() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                if (TreeViewModel.getModel().getTree() != null) {
+                    mZoomSlider.setSelection((int) Math
+                            .round(TreeViewModel.getModel().getZoom() * 10));
+                }
+                mFilterText.setText(""); //$NON-NLS-1$
+            }
+        });
+    }
+
+    @Override
+    public void viewportChanged() {
+        // pass
+    }
+
+    @Override
+    public void zoomChanged() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                mZoomSlider.setSelection((int) Math.round(TreeViewModel.getModel().getZoom() * 10));
+            }
+        });
+    };
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewOverview.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewOverview.java
new file mode 100644
index 0000000..3352df0
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewOverview.java
@@ -0,0 +1,396 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.ui;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewerlib.models.TreeViewModel;
+import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener;
+import com.android.hierarchyviewerlib.ui.util.DrawableViewNode;
+import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Point;
+import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Rectangle;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.events.MouseMoveListener;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Path;
+import org.eclipse.swt.graphics.Transform;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+
+public class TreeViewOverview extends Canvas implements ITreeChangeListener {
+
+    private TreeViewModel mModel;
+
+    private DrawableViewNode mTree;
+
+    private Rectangle mViewport;
+
+    private Transform mTransform;
+
+    private Transform mInverse;
+
+    private Rectangle mBounds = new Rectangle();
+
+    private double mScale;
+
+    private boolean mDragging = false;
+
+    private DrawableViewNode mSelectedNode;
+
+    private static Image sNotSelectedImage;
+
+    private static Image sSelectedImage;
+
+    private static Image sFilteredImage;
+
+    private static Image sFilteredSelectedImage;
+
+    public TreeViewOverview(Composite parent) {
+        super(parent, SWT.NONE);
+
+        mModel = TreeViewModel.getModel();
+        mModel.addTreeChangeListener(this);
+
+        loadResources();
+
+        addPaintListener(mPaintListener);
+        addMouseListener(mMouseListener);
+        addMouseMoveListener(mMouseMoveListener);
+        addListener(SWT.Resize, mResizeListener);
+        addDisposeListener(mDisposeListener);
+
+        mTransform = new Transform(Display.getDefault());
+        mInverse = new Transform(Display.getDefault());
+
+        loadAllData();
+    }
+
+    private void loadResources() {
+        ImageLoader loader = ImageLoader.getLoader(this.getClass());
+        sNotSelectedImage = loader.loadImage("not-selected.png", Display.getDefault()); //$NON-NLS-1$
+        sSelectedImage = loader.loadImage("selected-small.png", Display.getDefault()); //$NON-NLS-1$
+        sFilteredImage = loader.loadImage("filtered.png", Display.getDefault()); //$NON-NLS-1$
+        sFilteredSelectedImage =
+                loader.loadImage("selected-filtered-small.png", Display.getDefault()); //$NON-NLS-1$
+    }
+
+    private DisposeListener mDisposeListener = new DisposeListener() {
+        @Override
+        public void widgetDisposed(DisposeEvent e) {
+            mModel.removeTreeChangeListener(TreeViewOverview.this);
+            mTransform.dispose();
+            mInverse.dispose();
+        }
+    };
+
+    private MouseListener mMouseListener = new MouseListener() {
+
+        @Override
+        public void mouseDoubleClick(MouseEvent e) {
+            // pass
+        }
+
+        @Override
+        public void mouseDown(MouseEvent e) {
+            boolean redraw = false;
+            synchronized (TreeViewOverview.this) {
+                if (mTree != null && mViewport != null) {
+                    mDragging = true;
+                    redraw = true;
+                    handleMouseEvent(transformPoint(e.x, e.y));
+                }
+            }
+            if (redraw) {
+                mModel.removeTreeChangeListener(TreeViewOverview.this);
+                mModel.setViewport(mViewport);
+                mModel.addTreeChangeListener(TreeViewOverview.this);
+                doRedraw();
+            }
+        }
+
+        @Override
+        public void mouseUp(MouseEvent e) {
+            boolean redraw = false;
+            synchronized (TreeViewOverview.this) {
+                if (mTree != null && mViewport != null) {
+                    mDragging = false;
+                    redraw = true;
+                    handleMouseEvent(transformPoint(e.x, e.y));
+
+                    // Update bounds and transform only on mouse up. That way,
+                    // you don't get confusing behaviour during mouse drag and
+                    // it snaps neatly at the end
+                    setBounds();
+                    setTransform();
+                }
+            }
+            if (redraw) {
+                mModel.removeTreeChangeListener(TreeViewOverview.this);
+                mModel.setViewport(mViewport);
+                mModel.addTreeChangeListener(TreeViewOverview.this);
+                doRedraw();
+            }
+        }
+
+    };
+
+    private MouseMoveListener mMouseMoveListener = new MouseMoveListener() {
+        @Override
+        public void mouseMove(MouseEvent e) {
+            boolean moved = false;
+            synchronized (TreeViewOverview.this) {
+                if (mDragging) {
+                    moved = true;
+                    handleMouseEvent(transformPoint(e.x, e.y));
+                }
+            }
+            if (moved) {
+                mModel.removeTreeChangeListener(TreeViewOverview.this);
+                mModel.setViewport(mViewport);
+                mModel.addTreeChangeListener(TreeViewOverview.this);
+                doRedraw();
+            }
+        }
+    };
+
+    private void handleMouseEvent(Point pt) {
+        mViewport.x = pt.x - mViewport.width / 2;
+        mViewport.y = pt.y - mViewport.height / 2;
+        if (mViewport.x < mBounds.x) {
+            mViewport.x = mBounds.x;
+        }
+        if (mViewport.y < mBounds.y) {
+            mViewport.y = mBounds.y;
+        }
+        if (mViewport.x + mViewport.width > mBounds.x + mBounds.width) {
+            mViewport.x = mBounds.x + mBounds.width - mViewport.width;
+        }
+        if (mViewport.y + mViewport.height > mBounds.y + mBounds.height) {
+            mViewport.y = mBounds.y + mBounds.height - mViewport.height;
+        }
+    }
+
+    private Point transformPoint(double x, double y) {
+        float[] pt = {
+                (float) x, (float) y
+        };
+        mInverse.transform(pt);
+        return new Point(pt[0], pt[1]);
+    }
+
+    private Listener mResizeListener = new Listener() {
+        @Override
+        public void handleEvent(Event arg0) {
+            synchronized (TreeViewOverview.this) {
+                setTransform();
+            }
+            doRedraw();
+        }
+    };
+
+    private PaintListener mPaintListener = new PaintListener() {
+        @Override
+        public void paintControl(PaintEvent e) {
+            synchronized (TreeViewOverview.this) {
+                if (mTree != null) {
+                    e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
+                    e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
+                    e.gc.fillRectangle(0, 0, getBounds().width, getBounds().height);
+                    e.gc.setTransform(mTransform);
+                    e.gc.setLineWidth((int) Math.ceil(0.7 / mScale));
+                    Path connectionPath = new Path(Display.getDefault());
+                    paintRecursive(e.gc, mTree, connectionPath);
+                    e.gc.drawPath(connectionPath);
+                    connectionPath.dispose();
+
+                    if (mViewport != null) {
+                        e.gc.setAlpha(50);
+                        e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
+                        e.gc.fillRectangle((int) mViewport.x, (int) mViewport.y, (int) Math
+                                .ceil(mViewport.width), (int) Math.ceil(mViewport.height));
+
+                        e.gc.setAlpha(255);
+                        e.gc.setForeground(Display.getDefault().getSystemColor(
+                                        SWT.COLOR_DARK_GRAY));
+                        e.gc.setLineWidth((int) Math.ceil(2 / mScale));
+                        e.gc.drawRectangle((int) mViewport.x, (int) mViewport.y, (int) Math
+                                .ceil(mViewport.width), (int) Math.ceil(mViewport.height));
+                    }
+                }
+            }
+        }
+    };
+
+    private void paintRecursive(GC gc, DrawableViewNode node, Path connectionPath) {
+        if (mSelectedNode == node && node.viewNode.filtered) {
+            gc.drawImage(sFilteredSelectedImage, node.left, (int) Math.round(node.top));
+        } else if (mSelectedNode == node) {
+            gc.drawImage(sSelectedImage, node.left, (int) Math.round(node.top));
+        } else if (node.viewNode.filtered) {
+            gc.drawImage(sFilteredImage, node.left, (int) Math.round(node.top));
+        } else {
+            gc.drawImage(sNotSelectedImage, node.left, (int) Math.round(node.top));
+        }
+        int N = node.children.size();
+        if (N == 0) {
+            return;
+        }
+        float childSpacing =
+                (1.0f * (DrawableViewNode.NODE_HEIGHT - 2 * TreeView.LINE_PADDING)) / N;
+        for (int i = 0; i < N; i++) {
+            DrawableViewNode child = node.children.get(i);
+            paintRecursive(gc, child, connectionPath);
+            float x1 = node.left + DrawableViewNode.NODE_WIDTH;
+            float y1 =
+                    (float) node.top + TreeView.LINE_PADDING + childSpacing * i + childSpacing / 2;
+            float x2 = child.left;
+            float y2 = (float) child.top + DrawableViewNode.NODE_HEIGHT / 2.0f;
+            float cx1 = x1 + TreeView.BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING;
+            float cy1 = y1;
+            float cx2 = x2 - TreeView.BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING;
+            float cy2 = y2;
+            connectionPath.moveTo(x1, y1);
+            connectionPath.cubicTo(cx1, cy1, cx2, cy2, x2, y2);
+        }
+    }
+
+    private void doRedraw() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                redraw();
+            }
+        });
+    }
+
+    public void loadAllData() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                synchronized (this) {
+                    mTree = mModel.getTree();
+                    mSelectedNode = mModel.getSelection();
+                    mViewport = mModel.getViewport();
+                    setBounds();
+                    setTransform();
+                }
+            }
+        });
+    }
+
+    // Note the syncExec and then synchronized... It avoids deadlock
+    @Override
+    public void treeChanged() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                synchronized (this) {
+                    mTree = mModel.getTree();
+                    mSelectedNode = mModel.getSelection();
+                    mViewport = mModel.getViewport();
+                    setBounds();
+                    setTransform();
+                }
+            }
+        });
+        doRedraw();
+    }
+
+    private void setBounds() {
+        if (mViewport != null && mTree != null) {
+            mBounds.x = Math.min(mViewport.x, mTree.bounds.x);
+            mBounds.y = Math.min(mViewport.y, mTree.bounds.y);
+            mBounds.width =
+                    Math.max(mViewport.x + mViewport.width, mTree.bounds.x + mTree.bounds.width)
+                            - mBounds.x;
+            mBounds.height =
+                    Math.max(mViewport.y + mViewport.height, mTree.bounds.y + mTree.bounds.height)
+                            - mBounds.y;
+        } else if (mTree != null) {
+            mBounds.x = mTree.bounds.x;
+            mBounds.y = mTree.bounds.y;
+            mBounds.width = mTree.bounds.x + mTree.bounds.width - mBounds.x;
+            mBounds.height = mTree.bounds.y + mTree.bounds.height - mBounds.y;
+        }
+    }
+
+    private void setTransform() {
+        if (mTree != null) {
+
+            mTransform.identity();
+            mInverse.identity();
+            final Point size = new Point();
+            size.x = getBounds().width;
+            size.y = getBounds().height;
+            if (mBounds.width == 0 || mBounds.height == 0 || size.x == 0 || size.y == 0) {
+                mScale = 1;
+            } else {
+                mScale = Math.min(size.x / mBounds.width, size.y / mBounds.height);
+            }
+            mTransform.scale((float) mScale, (float) mScale);
+            mInverse.scale((float) mScale, (float) mScale);
+            mTransform.translate((float) -mBounds.x, (float) -mBounds.y);
+            mInverse.translate((float) -mBounds.x, (float) -mBounds.y);
+            if (size.x / mBounds.width < size.y / mBounds.height) {
+                mTransform.translate(0, (float) (size.y / mScale - mBounds.height) / 2);
+                mInverse.translate(0, (float) (size.y / mScale - mBounds.height) / 2);
+            } else {
+                mTransform.translate((float) (size.x / mScale - mBounds.width) / 2, 0);
+                mInverse.translate((float) (size.x / mScale - mBounds.width) / 2, 0);
+            }
+            mInverse.invert();
+        }
+    }
+
+    @Override
+    public void viewportChanged() {
+        Display.getDefault().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                synchronized (this) {
+                    mViewport = mModel.getViewport();
+                    setBounds();
+                    setTransform();
+                }
+            }
+        });
+        doRedraw();
+    }
+
+    @Override
+    public void zoomChanged() {
+        viewportChanged();
+    }
+
+    @Override
+    public void selectionChanged() {
+        synchronized (this) {
+            mSelectedNode = mModel.getSelection();
+        }
+        doRedraw();
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/DrawableViewNode.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/DrawableViewNode.java
new file mode 100644
index 0000000..3c3b718
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/DrawableViewNode.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.ui.util;
+
+import com.android.hierarchyviewerlib.models.ViewNode;
+
+import java.util.ArrayList;
+
+public class DrawableViewNode {
+    public ViewNode viewNode;
+
+    public final ArrayList<DrawableViewNode> children = new ArrayList<DrawableViewNode>();
+
+    public final static int NODE_HEIGHT = 100;
+
+    public final static int NODE_WIDTH = 180;
+
+    public final static int CONTENT_LEFT_RIGHT_PADDING = 9;
+
+    public final static int CONTENT_TOP_BOTTOM_PADDING = 8;
+
+    public final static int CONTENT_INTER_PADDING = 3;
+
+    public final static int INDEX_PADDING = 7;
+
+    public final static int LEAF_NODE_SPACING = 9;
+
+    public final static int NON_LEAF_NODE_SPACING = 15;
+
+    public final static int PARENT_CHILD_SPACING = 50;
+
+    public final static int PADDING = 30;
+
+    public int treeHeight;
+
+    public int treeWidth;
+
+    public boolean leaf;
+
+    public DrawableViewNode parent;
+
+    public int left;
+
+    public double top;
+
+    public int topSpacing;
+
+    public int bottomSpacing;
+
+    public boolean treeDrawn;
+
+    public static class Rectangle {
+        public double x, y, width, height;
+
+        public Rectangle() {
+
+        }
+
+        public Rectangle(Rectangle other) {
+            this.x = other.x;
+            this.y = other.y;
+            this.width = other.width;
+            this.height = other.height;
+        }
+
+        public Rectangle(double x, double y, double width, double height) {
+            this.x = x;
+            this.y = y;
+            this.width = width;
+            this.height = height;
+        }
+
+        @Override
+        public String toString() {
+            return "{" + x + ", " + y + ", " + width + ", " + height + "}"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
+        }
+
+    }
+
+    public static class Point {
+        public double x, y;
+
+        public Point() {
+        }
+
+        public Point(double x, double y) {
+            this.x = x;
+            this.y = y;
+        }
+
+        @Override
+        public String toString() {
+            return "(" + x + ", " + y + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+        }
+    }
+
+    public Rectangle bounds = new Rectangle();
+
+    public DrawableViewNode(ViewNode viewNode) {
+        this.viewNode = viewNode;
+        treeDrawn = !viewNode.willNotDraw;
+        if (viewNode.children.size() == 0) {
+            treeHeight = NODE_HEIGHT;
+            treeWidth = NODE_WIDTH;
+            leaf = true;
+        } else {
+            leaf = false;
+            int N = viewNode.children.size();
+            treeHeight = 0;
+            treeWidth = 0;
+            for (int i = 0; i < N; i++) {
+                DrawableViewNode child = new DrawableViewNode(viewNode.children.get(i));
+                children.add(child);
+                child.parent = this;
+                treeHeight += child.treeHeight;
+                treeWidth = Math.max(treeWidth, child.treeWidth);
+                if (i != 0) {
+                    DrawableViewNode prevChild = children.get(i - 1);
+                    if (prevChild.leaf && child.leaf) {
+                        treeHeight += LEAF_NODE_SPACING;
+                        prevChild.bottomSpacing = LEAF_NODE_SPACING;
+                        child.topSpacing = LEAF_NODE_SPACING;
+                    } else {
+                        treeHeight += NON_LEAF_NODE_SPACING;
+                        prevChild.bottomSpacing = NON_LEAF_NODE_SPACING;
+                        child.topSpacing = NON_LEAF_NODE_SPACING;
+                    }
+                }
+                treeDrawn |= child.treeDrawn;
+            }
+            treeWidth += NODE_WIDTH + PARENT_CHILD_SPACING;
+        }
+    }
+
+    public void setLeft() {
+        if (parent == null) {
+            left = PADDING;
+            bounds.x = 0;
+            bounds.width = treeWidth + 2 * PADDING;
+        } else {
+            left = parent.left + NODE_WIDTH + PARENT_CHILD_SPACING;
+        }
+        int N = children.size();
+        for (int i = 0; i < N; i++) {
+            children.get(i).setLeft();
+        }
+    }
+
+    public void placeRoot() {
+        top = PADDING + (treeHeight - NODE_HEIGHT) / 2.0;
+        double currentTop = PADDING;
+        int N = children.size();
+        for (int i = 0; i < N; i++) {
+            DrawableViewNode child = children.get(i);
+            child.place(currentTop, top - currentTop);
+            currentTop += child.treeHeight + child.bottomSpacing;
+        }
+        bounds.y = 0;
+        bounds.height = treeHeight + 2 * PADDING;
+    }
+
+    private void place(double treeTop, double rootDistance) {
+        if (treeHeight <= rootDistance) {
+            top = treeTop + treeHeight - NODE_HEIGHT;
+        } else if (rootDistance <= -NODE_HEIGHT) {
+            top = treeTop;
+        } else {
+            if (children.size() == 0) {
+                top = treeTop;
+            } else {
+                top =
+                        rootDistance + treeTop - NODE_HEIGHT + (2.0 * NODE_HEIGHT)
+                                / (treeHeight + NODE_HEIGHT) * (treeHeight - rootDistance);
+            }
+        }
+        int N = children.size();
+        double currentTop = treeTop;
+        for (int i = 0; i < N; i++) {
+            DrawableViewNode child = children.get(i);
+            child.place(currentTop, rootDistance);
+            currentTop += child.treeHeight + child.bottomSpacing;
+            rootDistance -= child.treeHeight + child.bottomSpacing;
+        }
+    }
+
+    public DrawableViewNode getSelected(double x, double y) {
+        if (x >= left && x < left + NODE_WIDTH && y >= top && y <= top + NODE_HEIGHT) {
+            return this;
+        }
+        int N = children.size();
+        for (int i = 0; i < N; i++) {
+            DrawableViewNode selected = children.get(i).getSelected(x, y);
+            if (selected != null) {
+                return selected;
+            }
+        }
+        return null;
+    }
+
+    /*
+     * Moves the node the specified distance up.
+     */
+    public void move(double distance) {
+        top -= distance;
+
+        // Get the root
+        DrawableViewNode root = this;
+        while (root.parent != null) {
+            root = root.parent;
+        }
+
+        // Figure out the new tree top.
+        double treeTop;
+        if (top + NODE_HEIGHT <= root.top) {
+            treeTop = top + NODE_HEIGHT - treeHeight;
+        } else if (top >= root.top + NODE_HEIGHT) {
+            treeTop = top;
+        } else {
+            if (leaf) {
+                treeTop = top;
+            } else {
+                double distanceRatio = 1 - (root.top + NODE_HEIGHT - top) / (2.0 * NODE_HEIGHT);
+                treeTop = root.top - treeHeight + distanceRatio * (treeHeight + NODE_HEIGHT);
+            }
+        }
+        // Go up the tree and figure out the tree top.
+        DrawableViewNode node = this;
+        while (node.parent != null) {
+            int index = node.viewNode.index;
+            for (int i = 0; i < index; i++) {
+                DrawableViewNode sibling = node.parent.children.get(i);
+                treeTop -= sibling.treeHeight + sibling.bottomSpacing;
+            }
+            node = node.parent;
+        }
+
+        // Update the bounds.
+        root.bounds.y = Math.min(root.top - PADDING, treeTop - PADDING);
+        root.bounds.height =
+                Math.max(treeTop + root.treeHeight + PADDING, root.top + NODE_HEIGHT + PADDING)
+                        - root.bounds.y;
+        // Place all the children of the root
+        double currentTop = treeTop;
+        int N = root.children.size();
+        for (int i = 0; i < N; i++) {
+            DrawableViewNode child = root.children.get(i);
+            child.place(currentTop, root.top - currentTop);
+            currentTop += child.treeHeight + child.bottomSpacing;
+        }
+
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/PsdFile.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/PsdFile.java
new file mode 100644
index 0000000..2c1154b
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/PsdFile.java
@@ -0,0 +1,508 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.ui.util;
+
+import java.awt.Graphics2D;
+import java.awt.Point;
+import java.awt.image.BufferedImage;
+import java.io.BufferedOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Writes PSD file. Supports only 8 bits, RGB images with 4 channels.
+ */
+public class PsdFile {
+    private final Header mHeader;
+
+    private final ColorMode mColorMode;
+
+    private final ImageResources mImageResources;
+
+    private final LayersMasksInfo mLayersMasksInfo;
+
+    private final LayersInfo mLayersInfo;
+
+    private final BufferedImage mMergedImage;
+
+    private final Graphics2D mGraphics;
+
+    public PsdFile(int width, int height) {
+        mHeader = new Header(width, height);
+        mColorMode = new ColorMode();
+        mImageResources = new ImageResources();
+        mLayersMasksInfo = new LayersMasksInfo();
+        mLayersInfo = new LayersInfo();
+
+        mMergedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+        mGraphics = mMergedImage.createGraphics();
+    }
+
+    public void addLayer(String name, BufferedImage image, Point offset) {
+        addLayer(name, image, offset, true);
+    }
+
+    public void addLayer(String name, BufferedImage image, Point offset, boolean visible) {
+        mLayersInfo.addLayer(name, image, offset, visible);
+        if (visible)
+            mGraphics.drawImage(image, null, offset.x, offset.y);
+    }
+
+    public void write(OutputStream stream) {
+        mLayersMasksInfo.setLayersInfo(mLayersInfo);
+
+        DataOutputStream out = new DataOutputStream(new BufferedOutputStream(stream));
+        try {
+            mHeader.write(out);
+            out.flush();
+
+            mColorMode.write(out);
+            mImageResources.write(out);
+            mLayersMasksInfo.write(out);
+            mLayersInfo.write(out);
+            out.flush();
+
+            mLayersInfo.writeImageData(out);
+            out.flush();
+
+            writeImage(mMergedImage, out, false);
+            out.flush();
+        } catch (IOException e) {
+            e.printStackTrace();
+        } finally {
+            try {
+                out.close();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    private static void writeImage(BufferedImage image, DataOutputStream out, boolean split)
+            throws IOException {
+
+        if (!split)
+            out.writeShort(0);
+
+        int width = image.getWidth();
+        int height = image.getHeight();
+
+        final int length = width * height;
+        int[] pixels = new int[length];
+
+        image.getData().getDataElements(0, 0, width, height, pixels);
+
+        byte[] a = new byte[length];
+        byte[] r = new byte[length];
+        byte[] g = new byte[length];
+        byte[] b = new byte[length];
+
+        for (int i = 0; i < length; i++) {
+            final int pixel = pixels[i];
+            a[i] = (byte) ((pixel >> 24) & 0xFF);
+            r[i] = (byte) ((pixel >> 16) & 0xFF);
+            g[i] = (byte) ((pixel >> 8) & 0xFF);
+            b[i] = (byte) (pixel & 0xFF);
+        }
+
+        if (split)
+            out.writeShort(0);
+        if (split)
+            out.write(a);
+        if (split)
+            out.writeShort(0);
+        out.write(r);
+        if (split)
+            out.writeShort(0);
+        out.write(g);
+        if (split)
+            out.writeShort(0);
+        out.write(b);
+        if (!split)
+            out.write(a);
+    }
+
+    @SuppressWarnings( {
+        "UnusedDeclaration"
+    })
+    static class Header {
+        static final short MODE_BITMAP = 0;
+
+        static final short MODE_GRAYSCALE = 1;
+
+        static final short MODE_INDEXED = 2;
+
+        static final short MODE_RGB = 3;
+
+        static final short MODE_CMYK = 4;
+
+        static final short MODE_MULTI_CHANNEL = 7;
+
+        static final short MODE_DUOTONE = 8;
+
+        static final short MODE_LAB = 9;
+
+        final byte[] mSignature = "8BPS".getBytes(); //$NON-NLS-1$
+
+        final short mVersion = 1;
+
+        final byte[] mReserved = new byte[6];
+
+        final short mChannelCount = 4;
+
+        final int mHeight;
+
+        final int mWidth;
+
+        final short mDepth = 8;
+
+        final short mMode = MODE_RGB;
+
+        Header(int width, int height) {
+            mWidth = width;
+            mHeight = height;
+        }
+
+        void write(DataOutputStream out) throws IOException {
+            out.write(mSignature);
+            out.writeShort(mVersion);
+            out.write(mReserved);
+            out.writeShort(mChannelCount);
+            out.writeInt(mHeight);
+            out.writeInt(mWidth);
+            out.writeShort(mDepth);
+            out.writeShort(mMode);
+        }
+    }
+
+    // Unused at the moment
+    @SuppressWarnings( {
+        "UnusedDeclaration"
+    })
+    static class ColorMode {
+        final int mLength = 0;
+
+        void write(DataOutputStream out) throws IOException {
+            out.writeInt(mLength);
+        }
+    }
+
+    // Unused at the moment
+    @SuppressWarnings( {
+        "UnusedDeclaration"
+    })
+    static class ImageResources {
+        static final short RESOURCE_RESOLUTION_INFO = 0x03ED;
+
+        int mLength = 0;
+
+        final byte[] mSignature = "8BIM".getBytes(); //$NON-NLS-1$
+
+        final short mResourceId = RESOURCE_RESOLUTION_INFO;
+
+        final short mPad = 0;
+
+        final int mDataLength = 16;
+
+        final short mHorizontalDisplayUnit = 0x48; // 72 dpi
+
+        final int mHorizontalResolution = 1;
+
+        final short mWidthDisplayUnit = 1;
+
+        final short mVerticalDisplayUnit = 0x48; // 72 dpi
+
+        final int mVerticalResolution = 1;
+
+        final short mHeightDisplayUnit = 1;
+
+        ImageResources() {
+            mLength = mSignature.length;
+            mLength += 2;
+            mLength += 2;
+            mLength += 4;
+            mLength += 8;
+            mLength += 8;
+        }
+
+        void write(DataOutputStream out) throws IOException {
+            out.writeInt(mLength);
+            out.write(mSignature);
+            out.writeShort(mResourceId);
+            out.writeShort(mPad);
+            out.writeInt(mDataLength);
+            out.writeShort(mHorizontalDisplayUnit);
+            out.writeInt(mHorizontalResolution);
+            out.writeShort(mWidthDisplayUnit);
+            out.writeShort(mVerticalDisplayUnit);
+            out.writeInt(mVerticalResolution);
+            out.writeShort(mHeightDisplayUnit);
+        }
+    }
+
+    @SuppressWarnings( {
+        "UnusedDeclaration"
+    })
+    static class LayersMasksInfo {
+        int mMiscLength;
+
+        int mLayerInfoLength;
+
+        void setLayersInfo(LayersInfo layersInfo) {
+            mLayerInfoLength = layersInfo.getLength();
+            // Round to the next multiple of 2
+            if ((mLayerInfoLength & 0x1) == 0x1)
+                mLayerInfoLength++;
+            mMiscLength = mLayerInfoLength + 8;
+        }
+
+        void write(DataOutputStream out) throws IOException {
+            out.writeInt(mMiscLength);
+            out.writeInt(mLayerInfoLength);
+        }
+    }
+
+    @SuppressWarnings( {
+        "UnusedDeclaration"
+    })
+    static class LayersInfo {
+        final List<Layer> mLayers = new ArrayList<Layer>();
+
+        void addLayer(String name, BufferedImage image, Point offset, boolean visible) {
+            mLayers.add(new Layer(name, image, offset, visible));
+        }
+
+        int getLength() {
+            int length = 2;
+            for (Layer layer : mLayers) {
+                length += layer.getLength();
+            }
+            return length;
+        }
+
+        void write(DataOutputStream out) throws IOException {
+            out.writeShort((short) -mLayers.size());
+            for (Layer layer : mLayers) {
+                layer.write(out);
+            }
+        }
+
+        void writeImageData(DataOutputStream out) throws IOException {
+            for (Layer layer : mLayers) {
+                layer.writeImageData(out);
+            }
+            // Global layer mask info length
+            out.writeInt(0);
+        }
+    }
+
+    @SuppressWarnings( {
+        "UnusedDeclaration"
+    })
+    static class Layer {
+        static final byte OPACITY_TRANSPARENT = 0x0;
+
+        static final byte OPACITY_OPAQUE = (byte) 0xFF;
+
+        static final byte CLIPPING_BASE = 0x0;
+
+        static final byte CLIPPING_NON_BASE = 0x1;
+
+        static final byte FLAG_TRANSPARENCY_PROTECTED = 0x1;
+
+        static final byte FLAG_INVISIBLE = 0x2;
+
+        final int mTop;
+
+        final int mLeft;
+
+        final int mBottom;
+
+        final int mRight;
+
+        final short mChannelCount = 4;
+
+        final Channel[] mChannelInfo = new Channel[mChannelCount];
+
+        final byte[] mBlendSignature = "8BIM".getBytes(); //$NON-NLS-1$
+
+        final byte[] mBlendMode = "norm".getBytes(); //$NON-NLS-1$
+
+        final byte mOpacity = OPACITY_OPAQUE;
+
+        final byte mClipping = CLIPPING_BASE;
+
+        byte mFlags = 0x0;
+
+        final byte mFiller = 0x0;
+
+        int mExtraSize = 4 + 4;
+
+        final int mMaskDataLength = 0;
+
+        final int mBlendRangeDataLength = 0;
+
+        final byte[] mName;
+
+        final byte[] mLayerExtraSignature = "8BIM".getBytes(); //$NON-NLS-1$
+
+        final byte[] mLayerExtraKey = "luni".getBytes(); //$NON-NLS-1$
+
+        int mLayerExtraLength;
+
+        final String mOriginalName;
+
+        private BufferedImage mImage;
+
+        Layer(String name, BufferedImage image, Point offset, boolean visible) {
+            final int height = image.getHeight();
+            final int width = image.getWidth();
+            final int length = width * height;
+
+            mChannelInfo[0] = new Channel(Channel.ID_ALPHA, length);
+            mChannelInfo[1] = new Channel(Channel.ID_RED, length);
+            mChannelInfo[2] = new Channel(Channel.ID_GREEN, length);
+            mChannelInfo[3] = new Channel(Channel.ID_BLUE, length);
+
+            mTop = offset.y;
+            mLeft = offset.x;
+            mBottom = offset.y + height;
+            mRight = offset.x + width;
+
+            mOriginalName = name;
+            byte[] data = name.getBytes();
+
+            try {
+                mLayerExtraLength = 4 + mOriginalName.getBytes("UTF-16").length; //$NON-NLS-1$
+            } catch (UnsupportedEncodingException e) {
+                e.printStackTrace();
+            }
+
+            final byte[] nameData = new byte[data.length + 1];
+            nameData[0] = (byte) (data.length & 0xFF);
+            System.arraycopy(data, 0, nameData, 1, data.length);
+
+            // This could be done in the same pass as above
+            if (nameData.length % 4 != 0) {
+                data = new byte[nameData.length + 4 - (nameData.length % 4)];
+                System.arraycopy(nameData, 0, data, 0, nameData.length);
+                mName = data;
+            } else {
+                mName = nameData;
+            }
+            mExtraSize += mName.length;
+            mExtraSize +=
+                    mLayerExtraLength + 4 + mLayerExtraKey.length + mLayerExtraSignature.length;
+
+            mImage = image;
+
+            if (!visible) {
+                mFlags |= FLAG_INVISIBLE;
+            }
+        }
+
+        int getLength() {
+            int length = 4 * 4 + 2;
+
+            for (Channel channel : mChannelInfo) {
+                length += channel.getLength();
+            }
+
+            length += mBlendSignature.length;
+            length += mBlendMode.length;
+            length += 4;
+            length += 4;
+            length += mExtraSize;
+
+            return length;
+        }
+
+        void write(DataOutputStream out) throws IOException {
+            out.writeInt(mTop);
+            out.writeInt(mLeft);
+            out.writeInt(mBottom);
+            out.writeInt(mRight);
+
+            out.writeShort(mChannelCount);
+            for (Channel channel : mChannelInfo) {
+                channel.write(out);
+            }
+
+            out.write(mBlendSignature);
+            out.write(mBlendMode);
+
+            out.write(mOpacity);
+            out.write(mClipping);
+            out.write(mFlags);
+            out.write(mFiller);
+
+            out.writeInt(mExtraSize);
+            out.writeInt(mMaskDataLength);
+
+            out.writeInt(mBlendRangeDataLength);
+
+            out.write(mName);
+
+            out.write(mLayerExtraSignature);
+            out.write(mLayerExtraKey);
+            out.writeInt(mLayerExtraLength);
+            out.writeInt(mOriginalName.length() + 1);
+            out.write(mOriginalName.getBytes("UTF-16")); //$NON-NLS-1$
+        }
+
+        void writeImageData(DataOutputStream out) throws IOException {
+            writeImage(mImage, out, true);
+        }
+    }
+
+    @SuppressWarnings( {
+        "UnusedDeclaration"
+    })
+    static class Channel {
+        static final short ID_RED = 0;
+
+        static final short ID_GREEN = 1;
+
+        static final short ID_BLUE = 2;
+
+        static final short ID_ALPHA = -1;
+
+        static final short ID_LAYER_MASK = -2;
+
+        final short mId;
+
+        final int mDataLength;
+
+        Channel(short id, int dataLength) {
+            mId = id;
+            mDataLength = dataLength + 2;
+        }
+
+        int getLength() {
+            return 2 + 4 + mDataLength;
+        }
+
+        void write(DataOutputStream out) throws IOException {
+            out.writeShort(mId);
+            out.writeInt(mDataLength);
+        }
+    }
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/TreeColumnResizer.java b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/TreeColumnResizer.java
new file mode 100644
index 0000000..1213620
--- /dev/null
+++ b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/TreeColumnResizer.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2010 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.hierarchyviewerlib.ui.util;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.TreeColumn;
+
+public class TreeColumnResizer {
+
+    private TreeColumn mColumn1;
+
+    private TreeColumn mColumn2;
+
+    private Composite mControl;
+
+    private int mColumn1Width;
+
+    private int mColumn2Width;
+
+    private final static int MIN_COLUMN1_WIDTH = 18;
+
+    private final static int MIN_COLUMN2_WIDTH = 3;
+
+    public TreeColumnResizer(Composite control, TreeColumn column1, TreeColumn column2) {
+        this.mControl = control;
+        this.mColumn1 = column1;
+        this.mColumn2 = column2;
+        control.addListener(SWT.Resize, resizeListener);
+        column1.addListener(SWT.Resize, column1ResizeListener);
+        column2.setResizable(false);
+    }
+
+    private Listener resizeListener = new Listener() {
+        @Override
+        public void handleEvent(Event e) {
+            if (mColumn1Width == 0 && mColumn2Width == 0) {
+                mColumn1Width = (mControl.getBounds().width - 18) / 2;
+                mColumn2Width = (mControl.getBounds().width - 18) / 2;
+            } else {
+                int dif = mControl.getBounds().width - 18 - (mColumn1Width + mColumn2Width);
+                int columnDif = Math.abs(mColumn1Width - mColumn2Width);
+                int mainColumnChange = Math.min(Math.abs(dif), columnDif);
+                int left = Math.max(0, Math.abs(dif) - columnDif);
+                if (dif < 0) {
+                    if (mColumn1Width > mColumn2Width) {
+                        mColumn1Width -= mainColumnChange;
+                    } else {
+                        mColumn2Width -= mainColumnChange;
+                    }
+                    mColumn1Width -= left / 2;
+                    mColumn2Width -= left - left / 2;
+                } else {
+                    if (mColumn1Width > mColumn2Width) {
+                        mColumn2Width += mainColumnChange;
+                    } else {
+                        mColumn1Width += mainColumnChange;
+                    }
+                    mColumn1Width += left / 2;
+                    mColumn2Width += left - left / 2;
+                }
+            }
+            mColumn1.removeListener(SWT.Resize, column1ResizeListener);
+            mColumn1.setWidth(mColumn1Width);
+            mColumn2.setWidth(mColumn2Width);
+            mColumn1.addListener(SWT.Resize, column1ResizeListener);
+        }
+    };
+
+    private Listener column1ResizeListener = new Listener() {
+        @Override
+        public void handleEvent(Event e) {
+            int widthDif = mColumn1Width - mColumn1.getWidth();
+            mColumn1Width -= widthDif;
+            mColumn2Width += widthDif;
+            boolean column1Changed = false;
+
+            // Strange, but these constants make the columns look the same.
+
+            if (mColumn1Width < MIN_COLUMN1_WIDTH) {
+                mColumn2Width -= MIN_COLUMN1_WIDTH - mColumn1Width;
+                mColumn1Width += MIN_COLUMN1_WIDTH - mColumn1Width;
+                column1Changed = true;
+            }
+            if (mColumn2Width < MIN_COLUMN2_WIDTH) {
+                mColumn1Width += mColumn2Width - MIN_COLUMN2_WIDTH;
+                mColumn2Width = MIN_COLUMN2_WIDTH;
+                column1Changed = true;
+            }
+            if (column1Changed) {
+                mColumn1.removeListener(SWT.Resize, this);
+                mColumn1.setWidth(mColumn1Width);
+                mColumn1.addListener(SWT.Resize, this);
+            }
+            mColumn2.setWidth(mColumn2Width);
+        }
+    };
+}
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/auto-refresh.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/auto-refresh.png
new file mode 100644
index 0000000..240862f
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/auto-refresh.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/capture-psd.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/capture-psd.png
new file mode 100644
index 0000000..0f25426
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/capture-psd.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/device-view-selected.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/device-view-selected.png
new file mode 100644
index 0000000..fd107ed
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/device-view-selected.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/device-view.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/device-view.png
new file mode 100644
index 0000000..9a7eed4
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/device-view.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/display.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/display.png
new file mode 100644
index 0000000..a9de0ec
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/display.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/filtered.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/filtered.png
new file mode 100644
index 0000000..4fcab3f
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/filtered.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/green.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/green.png
new file mode 100644
index 0000000..800000d
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/green.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/inspect-screenshot.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/inspect-screenshot.png
new file mode 100644
index 0000000..6e51701
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/inspect-screenshot.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/invalidate.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/invalidate.png
new file mode 100644
index 0000000..ee75f69
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/invalidate.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/load-all-views.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/load-all-views.png
new file mode 100644
index 0000000..3329ec9
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/load-all-views.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/load-overlay.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/load-overlay.png
new file mode 100644
index 0000000..4817252
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/load-overlay.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/load-view-hierarchy.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/load-view-hierarchy.png
new file mode 100644
index 0000000..8f01dda
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/load-view-hierarchy.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/not-selected.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/not-selected.png
new file mode 100644
index 0000000..db6f13b
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/not-selected.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/on-black.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/on-black.png
new file mode 100644
index 0000000..cd88803
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/on-black.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/on-white.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/on-white.png
new file mode 100644
index 0000000..5f05662
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/on-white.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/picker.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/picker.png
new file mode 100644
index 0000000..8ea2bed
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/picker.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/pixel-perfect-view-selected.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/pixel-perfect-view-selected.png
new file mode 100644
index 0000000..1e44000
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/pixel-perfect-view-selected.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/pixel-perfect-view.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/pixel-perfect-view.png
new file mode 100644
index 0000000..ec51cec
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/pixel-perfect-view.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/profile.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/profile.png
new file mode 100644
index 0000000..1e9fb5a
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/profile.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/red.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/red.png
new file mode 100644
index 0000000..a2ab855
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/red.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/refresh-windows.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/refresh-windows.png
new file mode 100644
index 0000000..8fddcae
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/refresh-windows.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/request-layout.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/request-layout.png
new file mode 100644
index 0000000..92a78c8
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/request-layout.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/save.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/save.png
new file mode 100644
index 0000000..2c0bab1
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/save.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/sdk-hierarchyviewer-128.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/sdk-hierarchyviewer-128.png
new file mode 100644
index 0000000..4535f22
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/sdk-hierarchyviewer-128.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/sdk-hierarchyviewer-16.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/sdk-hierarchyviewer-16.png
new file mode 100755
index 0000000..8c3c23d
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/sdk-hierarchyviewer-16.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/selected-filtered-small.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/selected-filtered-small.png
new file mode 100644
index 0000000..9ef6b34
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/selected-filtered-small.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/selected-filtered.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/selected-filtered.png
new file mode 100644
index 0000000..1f59685
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/selected-filtered.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/selected-small.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/selected-small.png
new file mode 100644
index 0000000..538e385
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/selected-small.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/selected.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/selected.png
new file mode 100644
index 0000000..5cd5c3f
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/selected.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/show-extras.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/show-extras.png
new file mode 100644
index 0000000..ba9c305
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/show-extras.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/show-overlay.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/show-overlay.png
new file mode 100644
index 0000000..e39e90a
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/show-overlay.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/tree-view-selected.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/tree-view-selected.png
new file mode 100644
index 0000000..175ad1f
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/tree-view-selected.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/tree-view.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/tree-view.png
new file mode 100644
index 0000000..23aa424
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/tree-view.png differ
diff --git a/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/yellow.png b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/yellow.png
new file mode 100644
index 0000000..e9b5781
Binary files /dev/null and b/hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/yellow.png differ
diff --git a/sdklib/.classpath b/sdklib/.classpath
new file mode 100644
index 0000000..a441584
--- /dev/null
+++ b/sdklib/.classpath
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src/main/java"/>
+	<classpathentry kind="src" path="src/test/java"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/3"/>
+	<classpathentry combineaccessrules="false" exported="true" kind="src" path="/common"/>
+	<classpathentry exported="true" kind="var" path="ANDROID_SRC/prebuilts/tools/common/m2/repository/org/apache/commons/commons-compress/1.0/commons-compress-1.0.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/m2/repository/org/apache/commons/commons-compress/1.0/commons-compress-1.0-sources.jar"/>
+	<classpathentry exported="true" kind="var" path="ANDROID_SRC/prebuilts/tools/common/m2/repository/commons-codec/commons-codec/1.4/commons-codec-1.4.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/m2/repository/commons-codec/commons-codec/1.4/commons-codec-1.4-sources.jar"/>
+	<classpathentry exported="true" kind="var" path="ANDROID_SRC/prebuilts/tools/common/m2/repository/commons-logging/commons-logging/1.1.1/commons-logging-1.1.1.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/m2/repository/commons-logging/commons-logging/1.1.1/commons-logging-1.1.1-sources.jar"/>
+	<classpathentry exported="true" kind="var" path="ANDROID_SRC/prebuilts/tools/common/m2/repository/org/apache/httpcomponents/httpclient/4.1.1/httpclient-4.1.1.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/m2/repository/org/apache/httpcomponents/httpclient/4.1.1/httpclient-4.1.1-sources.jar"/>
+	<classpathentry exported="true" kind="var" path="ANDROID_SRC/prebuilts/tools/common/m2/repository/org/apache/httpcomponents/httpcore/4.1/httpcore-4.1.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/m2/repository/org/apache/httpcomponents/httpcore/4.1/httpcore-4.1-sources.jar"/>
+	<classpathentry exported="true" kind="var" path="ANDROID_SRC/prebuilts/tools/common/m2/repository/org/apache/httpcomponents/httpmime/4.1/httpmime-4.1.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/m2/repository/org/apache/httpcomponents/httpmime/4.1/httpmime-4.1-sources.jar"/>
+	<classpathentry combineaccessrules="false" exported="true" kind="src" path="/dvlib"/>
+	<classpathentry combineaccessrules="false" exported="true" kind="src" path="/layoutlib-api"/>
+	<classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/m2/repository/org/bouncycastle/bcpkix-jdk15on/1.48/bcpkix-jdk15on-1.48.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/m2/repository/org/bouncycastle/bcpkix-jdk15on/1.48/bcpkix-jdk15on-1.48-sources.jar"/>
+	<classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/m2/repository/org/bouncycastle/bcprov-jdk15on/1.48/bcprov-jdk15on-1.48.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/m2/repository/org/bouncycastle/bcprov-jdk15on/1.48/bcprov-jdk15on-1.48-sources.jar"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/sdklib/.gitignore b/sdklib/.gitignore
new file mode 100644
index 0000000..81631c6
--- /dev/null
+++ b/sdklib/.gitignore
@@ -0,0 +1,2 @@
+/bin
+/build
diff --git a/sdklib/.project b/sdklib/.project
new file mode 100644
index 0000000..5a73da4
--- /dev/null
+++ b/sdklib/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>sdklib</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>
diff --git a/sdklib/.settings/org.eclipse.core.resources.prefs b/sdklib/.settings/org.eclipse.core.resources.prefs
new file mode 100755
index 0000000..3d65728
--- /dev/null
+++ b/sdklib/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,4 @@
+#Mon Aug 29 11:46:20 PDT 2011
+eclipse.preferences.version=1
+encoding//src/test/java/com/android/sdklib/testdata/addon_sample_1.xml=UTF-8
+encoding//src/test/java/com/android/sdklib/io/MockFileOpTest.java=UTF-8
diff --git a/sdklib/.settings/org.eclipse.jdt.core.prefs b/sdklib/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..9dbff07
--- /dev/null
+++ b/sdklib/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,98 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
+org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault
+org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled
+org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
+org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
+org.eclipse.jdt.core.compiler.problem.deadCode=warning
+org.eclipse.jdt.core.compiler.problem.deprecation=warning
+org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
+org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
+org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
+org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning
+org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled
+org.eclipse.jdt.core.compiler.problem.fieldHiding=warning
+org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
+org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=error
+org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
+org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled
+org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
+org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning
+org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning
+org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
+org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
+org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
+org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
+org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
+org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
+org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
+org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
+org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
+org.eclipse.jdt.core.compiler.problem.nullReference=error
+org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning
+org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore
+org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
+org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning
+org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
+org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
+org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
+org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled
+org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
+org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
+org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
+org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled
+org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning
+org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error
+org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning
+org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.unusedImport=warning
+org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
+org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
+org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning
+org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled
+org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
+org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
+org.eclipse.jdt.core.compiler.source=1.6
diff --git a/sdklib/.settings/org.eclipse.jdt.ui.prefs b/sdklib/.settings/org.eclipse.jdt.ui.prefs
new file mode 100755
index 0000000..4712267
--- /dev/null
+++ b/sdklib/.settings/org.eclipse.jdt.ui.prefs
@@ -0,0 +1,55 @@
+#Tue Aug 07 12:32:32 PDT 2012
+eclipse.preferences.version=1
+editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
+sp_cleanup.add_default_serial_version_id=true
+sp_cleanup.add_generated_serial_version_id=false
+sp_cleanup.add_missing_annotations=false
+sp_cleanup.add_missing_deprecated_annotations=true
+sp_cleanup.add_missing_methods=false
+sp_cleanup.add_missing_nls_tags=false
+sp_cleanup.add_missing_override_annotations=true
+sp_cleanup.add_missing_override_annotations_interface_methods=false
+sp_cleanup.add_serial_version_id=false
+sp_cleanup.always_use_blocks=true
+sp_cleanup.always_use_parentheses_in_expressions=false
+sp_cleanup.always_use_this_for_non_static_field_access=false
+sp_cleanup.always_use_this_for_non_static_method_access=false
+sp_cleanup.convert_to_enhanced_for_loop=false
+sp_cleanup.correct_indentation=false
+sp_cleanup.format_source_code=false
+sp_cleanup.format_source_code_changes_only=false
+sp_cleanup.make_local_variable_final=false
+sp_cleanup.make_parameters_final=false
+sp_cleanup.make_private_fields_final=true
+sp_cleanup.make_type_abstract_if_missing_method=false
+sp_cleanup.make_variable_declarations_final=false
+sp_cleanup.never_use_blocks=false
+sp_cleanup.never_use_parentheses_in_expressions=true
+sp_cleanup.on_save_use_additional_actions=true
+sp_cleanup.organize_imports=false
+sp_cleanup.qualify_static_field_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
+sp_cleanup.remove_private_constructors=true
+sp_cleanup.remove_trailing_whitespaces=true
+sp_cleanup.remove_trailing_whitespaces_all=false
+sp_cleanup.remove_trailing_whitespaces_ignore_empty=true
+sp_cleanup.remove_unnecessary_casts=false
+sp_cleanup.remove_unnecessary_nls_tags=false
+sp_cleanup.remove_unused_imports=false
+sp_cleanup.remove_unused_local_variables=false
+sp_cleanup.remove_unused_private_fields=true
+sp_cleanup.remove_unused_private_members=false
+sp_cleanup.remove_unused_private_methods=true
+sp_cleanup.remove_unused_private_types=true
+sp_cleanup.sort_members=false
+sp_cleanup.sort_members_all=false
+sp_cleanup.use_blocks=false
+sp_cleanup.use_blocks_only_for_return_and_throw=false
+sp_cleanup.use_parentheses_in_expressions=false
+sp_cleanup.use_this_for_non_static_field_access=false
+sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true
+sp_cleanup.use_this_for_non_static_method_access=false
+sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true
diff --git a/sdklib/MODULE_LICENSE_APACHE2 b/sdklib/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
diff --git a/sdklib/NOTICE b/sdklib/NOTICE
new file mode 100644
index 0000000..c5b1efa
--- /dev/null
+++ b/sdklib/NOTICE
@@ -0,0 +1,190 @@
+
+   Copyright (c) 2005-2008, 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.
+
+   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.
+
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
diff --git a/sdklib/sdklib.iml b/sdklib/sdklib.iml
new file mode 100644
index 0000000..da285ac
--- /dev/null
+++ b/sdklib/sdklib.iml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4">
+  <component name="NewModuleRootManager" inherit-compiler-output="true">
+    <exclude-output />
+    <content url="file://$MODULE_DIR$">
+      <sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
+      <excludeFolder url="file://$MODULE_DIR$/.settings" />
+      <excludeFolder url="file://$MODULE_DIR$/build" />
+    </content>
+    <orderEntry type="sourceFolder" forTests="false" />
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="module" module-name="common" exported="" />
+    <orderEntry type="module" module-name="dvlib" exported="" />
+    <orderEntry type="module" module-name="layoutlib-api" exported="" />
+    <orderEntry type="library" exported="" name="http-client" level="project" />
+    <orderEntry type="library" exported="" name="commons-compress" level="project" />
+    <orderEntry type="library" scope="TEST" name="JUnit3" level="project" />
+    <orderEntry type="library" exported="" name="bouncy-castle" level="project" />
+  </component>
+</module>
+
diff --git a/sdklib/src/main/java/com/android/sdklib/util/ArrayUtils.java b/sdklib/src/main/java/com/android/sdklib/util/ArrayUtils.java
new file mode 100644
index 0000000..787940b
--- /dev/null
+++ b/sdklib/src/main/java/com/android/sdklib/util/ArrayUtils.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdklib.util;
+
+import java.lang.reflect.Array;
+
+// XXX these should be changed to reflect the actual memory allocator we use.
+// it looks like right now objects want to be powers of 2 minus 8
+// and the array size eats another 4 bytes
+
+/**
+ * ArrayUtils contains some methods that you can call to find out
+ * the most efficient increments by which to grow arrays.
+ */
+/* package */ class ArrayUtils
+{
+    private static final Object[] EMPTY = new Object[0];
+    private static final int CACHE_SIZE = 73;
+    private static Object[] sCache = new Object[CACHE_SIZE];
+
+    private ArrayUtils() { /* cannot be instantiated */ }
+
+    public static int idealByteArraySize(int need) {
+        for (int i = 4; i < 32; i++)
+            if (need <= (1 << i) - 12)
+                return (1 << i) - 12;
+
+        return need;
+    }
+
+    public static int idealBooleanArraySize(int need) {
+        return idealByteArraySize(need);
+    }
+
+    public static int idealShortArraySize(int need) {
+        return idealByteArraySize(need * 2) / 2;
+    }
+
+    public static int idealCharArraySize(int need) {
+        return idealByteArraySize(need * 2) / 2;
+    }
+
+    public static int idealIntArraySize(int need) {
+        return idealByteArraySize(need * 4) / 4;
+    }
+
+    public static int idealFloatArraySize(int need) {
+        return idealByteArraySize(need * 4) / 4;
+    }
+
+    public static int idealObjectArraySize(int need) {
+        return idealByteArraySize(need * 4) / 4;
+    }
+
+    public static int idealLongArraySize(int need) {
+        return idealByteArraySize(need * 8) / 8;
+    }
+
+    /**
+     * Checks if the beginnings of two byte arrays are equal.
+     *
+     * @param array1 the first byte array
+     * @param array2 the second byte array
+     * @param length the number of bytes to check
+     * @return true if they're equal, false otherwise
+     */
+    public static boolean equals(byte[] array1, byte[] array2, int length) {
+        if (array1 == array2) {
+            return true;
+        }
+        if (array1 == null || array2 == null || array1.length < length || array2.length < length) {
+            return false;
+        }
+        for (int i = 0; i < length; i++) {
+            if (array1[i] != array2[i]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns an empty array of the specified type.  The intent is that
+     * it will return the same empty array every time to avoid reallocation,
+     * although this is not guaranteed.
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> T[] emptyArray(Class<T> kind) {
+        if (kind == Object.class) {
+            return (T[]) EMPTY;
+        }
+
+        int bucket = ((System.identityHashCode(kind) / 8) & 0x7FFFFFFF) % CACHE_SIZE;
+        Object cache = sCache[bucket];
+
+        if (cache == null || cache.getClass().getComponentType() != kind) {
+            cache = Array.newInstance(kind, 0);
+            sCache[bucket] = cache;
+
+            // Log.e("cache", "new empty " + kind.getName() + " at " + bucket);
+        }
+
+        return (T[]) cache;
+    }
+
+    /**
+     * Checks that value is present as at least one of the elements of the array.
+     * @param array the array to check in
+     * @param value the value to check for
+     * @return true if the value is present in the array
+     */
+    public static <T> boolean contains(T[] array, T value) {
+        for (T element : array) {
+            if (element == null) {
+                if (value == null) return true;
+            } else {
+                if (value != null && element.equals(value)) return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/sdklib/src/main/java/com/android/sdklib/util/CommandLineParser.java b/sdklib/src/main/java/com/android/sdklib/util/CommandLineParser.java
new file mode 100644
index 0000000..5c19052
--- /dev/null
+++ b/sdklib/src/main/java/com/android/sdklib/util/CommandLineParser.java
@@ -0,0 +1,968 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdklib.util;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.utils.ILogger;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map.Entry;
+
+/**
+ * Parses the command-line and stores flags needed or requested.
+ * <p/>
+ * This is a base class. To be useful you want to:
+ * <ul>
+ * <li>override it.
+ * <li>pass an action array to the constructor.
+ * <li>define flags for your actions.
+ * </ul>
+ * <p/>
+ * To use, call {@link #parseArgs(String[])} and then
+ * call {@link #getValue(String, String, String)}.
+ */
+public class CommandLineParser {
+
+    /*
+     * Steps needed to add a new action:
+     * - Each action is defined as a "verb object" followed by parameters.
+     * - Either reuse a VERB_ constant or define a new one.
+     * - Either reuse an OBJECT_ constant or define a new one.
+     * - Add a new entry to mAction with a one-line help summary.
+     * - In the constructor, add a define() call for each parameter (either mandatory
+     *   or optional) for the given action.
+     */
+
+    /** Internal verb name for internally hidden flags. */
+    public static final String GLOBAL_FLAG_VERB = "@@internal@@";   //$NON-NLS-1$
+
+    /** String to use when the verb doesn't need any object. */
+    public static final String NO_VERB_OBJECT = "";                 //$NON-NLS-1$
+
+    /** The global help flag. */
+    public static final String KEY_HELP = "help";
+    /** The global verbose flag. */
+    public static final String KEY_VERBOSE = "verbose";
+    /** The global silent flag. */
+    public static final String KEY_SILENT = "silent";
+
+    /** Verb requested by the user. Null if none specified, which will be an error. */
+    private String mVerbRequested;
+    /** Direct object requested by the user. Can be null. */
+    private String mDirectObjectRequested;
+
+    /**
+     * Action definitions.
+     * <p/>
+     * This list serves two purposes: first it is used to know which verb/object
+     * actions are acceptable on the command-line; second it provides a summary
+     * for each action that is printed in the help.
+     * <p/>
+     * Each entry is a string array with:
+     * <ul>
+     * <li> the verb.
+     * <li> a direct object (use {@link #NO_VERB_OBJECT} if there's no object).
+     * <li> a description.
+     * <li> an alternate form for the object (e.g. plural).
+     * </ul>
+     */
+    private final String[][] mActions;
+
+    private static final int ACTION_VERB_INDEX = 0;
+    private static final int ACTION_OBJECT_INDEX = 1;
+    private static final int ACTION_DESC_INDEX = 2;
+    private static final int ACTION_ALT_OBJECT_INDEX = 3;
+
+    /**
+     * The map of all defined arguments.
+     * <p/>
+     * The key is a string "verb/directObject/longName".
+     */
+    private final HashMap<String, Arg> mArguments = new HashMap<String, Arg>();
+    /** Logger */
+    private final ILogger mLog;
+
+    /**
+     * Constructs a new command-line processor.
+     *
+     * @param logger An SDK logger object. Must not be null.
+     * @param actions The list of actions recognized on the command-line.
+     *                See the javadoc of {@link #mActions} for more details.
+     *
+     * @see #mActions
+     */
+    public CommandLineParser(ILogger logger, String[][] actions) {
+        mLog = logger;
+        mActions = actions;
+
+        /*
+         * usage should fit in 80 columns, including the space to print the options:
+         * "  -v --verbose  7890123456789012345678901234567890123456789012345678901234567890"
+         */
+
+        define(Mode.BOOLEAN, false, GLOBAL_FLAG_VERB, NO_VERB_OBJECT, "v", KEY_VERBOSE,
+                           "Verbose mode, shows errors, warnings and all messages.",
+                           false);
+        define(Mode.BOOLEAN, false, GLOBAL_FLAG_VERB, NO_VERB_OBJECT, "s", KEY_SILENT,
+                           "Silent mode, shows errors only.",
+                           false);
+        define(Mode.BOOLEAN, false, GLOBAL_FLAG_VERB, NO_VERB_OBJECT, "h", KEY_HELP,
+                           "Help on a specific command.",
+                           false);
+    }
+
+    /**
+     * Indicates if this command-line can work when no verb is specified.
+     * The default is false, which generates an error when no verb/object is specified.
+     * Derived implementations can set this to true if they can deal with a lack
+     * of verb/action.
+     */
+    public boolean acceptLackOfVerb() {
+        return false;
+    }
+
+
+    //------------------
+    // Helpers to get flags values
+
+    /** Helper that returns true if --verbose was requested. */
+    public boolean isVerbose() {
+        return ((Boolean) getValue(GLOBAL_FLAG_VERB, NO_VERB_OBJECT, KEY_VERBOSE)).booleanValue();
+    }
+
+    /** Helper that returns true if --silent was requested. */
+    public boolean isSilent() {
+        return ((Boolean) getValue(GLOBAL_FLAG_VERB, NO_VERB_OBJECT, KEY_SILENT)).booleanValue();
+    }
+
+    /** Helper that returns true if --help was requested. */
+    public boolean isHelpRequested() {
+        return ((Boolean) getValue(GLOBAL_FLAG_VERB, NO_VERB_OBJECT, KEY_HELP)).booleanValue();
+    }
+
+    /** Returns the verb name from the command-line. Can be null. */
+    public String getVerb() {
+        return mVerbRequested;
+    }
+
+    /** Returns the direct object name from the command-line. Can be null. */
+    public String getDirectObject() {
+        return mDirectObjectRequested;
+    }
+
+    //------------------
+
+    /**
+     * Raw access to parsed parameter values.
+     * <p/>
+     * The default is to scan all parameters. Parameters that have been explicitly set on the
+     * command line are returned first. Otherwise one with a non-null value is returned.
+     * <p/>
+     * Both a verb and a direct object filter can be specified. When they are non-null they limit
+     * the scope of the search.
+     * <p/>
+     * If nothing has been found, return the last default value seen matching the filter.
+     *
+     * @param verb The verb name, including {@link #GLOBAL_FLAG_VERB}. If null, all possible
+     *             verbs that match the direct object condition will be examined and the first
+     *             value set will be used.
+     * @param directObject The direct object name, including {@link #NO_VERB_OBJECT}. If null,
+     *             all possible direct objects that match the verb condition will be examined and
+     *             the first value set will be used.
+     * @param longFlagName The long flag name for the given action. Mandatory. Cannot be null.
+     * @return The current value object stored in the parameter, which depends on the argument mode.
+     */
+    public Object getValue(String verb, String directObject, String longFlagName) {
+
+        if (verb != null && directObject != null) {
+            String key = verb + '/' + directObject + '/' + longFlagName;
+            Arg arg = mArguments.get(key);
+            return arg.getCurrentValue();
+        }
+
+        Object lastDefault = null;
+        for (Arg arg : mArguments.values()) {
+            if (arg.getLongArg().equals(longFlagName)) {
+                if (verb == null || arg.getVerb().equals(verb)) {
+                    if (directObject == null || arg.getDirectObject().equals(directObject)) {
+                        if (arg.isInCommandLine()) {
+                            return arg.getCurrentValue();
+                        }
+                        if (arg.getCurrentValue() != null) {
+                            lastDefault = arg.getCurrentValue();
+                        }
+                    }
+                }
+            }
+        }
+
+        return lastDefault;
+    }
+
+    /**
+     * Internal setter for raw parameter value.
+     * @param verb The verb name, including {@link #GLOBAL_FLAG_VERB}.
+     * @param directObject The direct object name, including {@link #NO_VERB_OBJECT}.
+     * @param longFlagName The long flag name for the given action.
+     * @param value The new current value object stored in the parameter, which depends on the
+     *              argument mode.
+     */
+    protected void setValue(String verb, String directObject, String longFlagName, Object value) {
+        String key = verb + '/' + directObject + '/' + longFlagName;
+        Arg arg = mArguments.get(key);
+        arg.setCurrentValue(value);
+    }
+
+    /**
+     * Parses the command-line arguments.
+     * <p/>
+     * This method will exit and not return if a parsing error arise.
+     *
+     * @param args The arguments typically received by a main method.
+     */
+    public void parseArgs(String[] args) {
+        String errorMsg = null;
+        String verb = null;
+        String directObject = null;
+
+        try {
+            int n = args.length;
+            for (int i = 0; i < n; i++) {
+                Arg arg = null;
+                String a = args[i];
+                if (a.startsWith("--")) {                                       //$NON-NLS-1$
+                    arg = findLongArg(verb, directObject, a.substring(2));
+                } else if (a.startsWith("-")) {                                 //$NON-NLS-1$
+                    arg = findShortArg(verb, directObject, a.substring(1));
+                }
+
+                // No matching argument name found
+                if (arg == null) {
+                    // Does it looks like a dashed parameter?
+                    if (a.startsWith("-")) {                                    //$NON-NLS-1$
+                        if (verb == null || directObject == null) {
+                            // It looks like a dashed parameter and we don't have a a verb/object
+                            // set yet, the parameter was just given too early.
+
+                            errorMsg = String.format(
+                                "Flag '%1$s' is not a valid global flag. Did you mean to specify it after the verb/object name?",
+                                a);
+                            return;
+                        } else {
+                            // It looks like a dashed parameter but it is unknown by this
+                            // verb-object combination
+
+                            errorMsg = String.format(
+                                    "Flag '%1$s' is not valid for '%2$s %3$s'.",
+                                    a, verb, directObject);
+                            return;
+                        }
+                    }
+
+                    if (verb == null) {
+                        // Fill verb first. Find it.
+                        for (String[] actionDesc : mActions) {
+                            if (actionDesc[ACTION_VERB_INDEX].equals(a)) {
+                                verb = a;
+                                break;
+                            }
+                        }
+
+                        // Error if it was not a valid verb
+                        if (verb == null) {
+                            errorMsg = String.format(
+                                "Expected verb after global parameters but found '%1$s' instead.",
+                                a);
+                            return;
+                        }
+
+                    } else if (directObject == null) {
+                        // Then fill the direct object. Find it.
+                        for (String[] actionDesc : mActions) {
+                            if (actionDesc[ACTION_VERB_INDEX].equals(verb)) {
+                                if (actionDesc[ACTION_OBJECT_INDEX].equals(a)) {
+                                    directObject = a;
+                                    break;
+                                } else if (actionDesc.length > ACTION_ALT_OBJECT_INDEX &&
+                                        actionDesc[ACTION_ALT_OBJECT_INDEX].equals(a)) {
+                                    // if the alternate form exist and is used, we internally
+                                    // only memorize the default direct object form.
+                                    directObject = actionDesc[ACTION_OBJECT_INDEX];
+                                    break;
+                                }
+                            }
+                        }
+
+                        // Error if it was not a valid object for that verb
+                        if (directObject == null) {
+                            errorMsg = String.format(
+                                "Expected verb after global parameters but found '%1$s' instead.",
+                                a);
+                            return;
+
+                        }
+                    } else {
+                        // The argument is not a dashed parameter and we already
+                        // have a verb/object. Must be some extra unknown argument.
+                        errorMsg = String.format(
+                                "Argument '%1$s' is not recognized.",
+                                a);
+                    }
+                } else if (arg != null) {
+                    // This argument was present on the command line
+                    arg.setInCommandLine(true);
+
+                    // Process keyword
+                    Object error = null;
+                    if (arg.getMode().needsExtra()) {
+                        if (i+1 >= n) {
+                            errorMsg = String.format("Missing argument for flag %1$s.", a);
+                            return;
+                        }
+
+                        while (i+1 < n) {
+                            String b = args[i+1];
+
+                            if (arg.getMode() != Mode.STRING_ARRAY) {
+                                // We never accept something that looks like a valid argument
+                                // unless we see -- first
+                                Arg dummyArg = null;
+                                if (b.startsWith("--")) {                              //$NON-NLS-1$
+                                    dummyArg = findLongArg(verb, directObject, b.substring(2));
+                                } else if (b.startsWith("-")) {                        //$NON-NLS-1$
+                                    dummyArg = findShortArg(verb, directObject, b.substring(1));
+                                }
+                                if (dummyArg != null) {
+                                    errorMsg = String.format(
+                                            "Oops, it looks like you didn't provide an argument for '%1$s'.\n'%2$s' was found instead.",
+                                            a, b);
+                                    return;
+                                }
+                            }
+
+                            error = arg.getMode().process(arg, b);
+                            if (error == Accept.CONTINUE) {
+                                i++;
+                            } else if (error == Accept.ACCEPT_AND_STOP) {
+                                i++;
+                                break;
+                            } else if (error == Accept.REJECT_AND_STOP) {
+                                break;
+                            } else if (error instanceof String) {
+                                // We stop because of an error
+                                break;
+                            }
+                        }
+                    } else {
+                        error = arg.getMode().process(arg, null);
+
+                        if (isHelpRequested()) {
+                            // The --help flag was requested. We'll continue the usual processing
+                            // so that we can find the optional verb/object words. Those will be
+                            // used to print specific help.
+                            // Setting a non-null error message triggers printing the help, however
+                            // there is no specific error to print.
+                            errorMsg = "";                                          //$NON-NLS-1$
+                        }
+                    }
+
+                    if (error instanceof String) {
+                        errorMsg = String.format("Invalid usage for flag %1$s: %2$s.", a, error);
+                        return;
+                    }
+                }
+            }
+
+            if (errorMsg == null) {
+                if (verb == null && !acceptLackOfVerb()) {
+                    errorMsg = "Missing verb name.";
+                } else if (verb != null) {
+                    if (directObject == null) {
+                        // Make sure this verb has an optional direct object
+                        for (String[] actionDesc : mActions) {
+                            if (actionDesc[ACTION_VERB_INDEX].equals(verb) &&
+                                    actionDesc[ACTION_OBJECT_INDEX].equals(NO_VERB_OBJECT)) {
+                                directObject = NO_VERB_OBJECT;
+                                break;
+                            }
+                        }
+
+                        if (directObject == null) {
+                            errorMsg = String.format("Missing object name for verb '%1$s'.", verb);
+                            return;
+                        }
+                    }
+
+                    // Validate that all mandatory arguments are non-null for this action
+                    String missing = null;
+                    boolean plural = false;
+                    for (Entry<String, Arg> entry : mArguments.entrySet()) {
+                        Arg arg = entry.getValue();
+                        if (arg.getVerb().equals(verb) &&
+                                arg.getDirectObject().equals(directObject)) {
+                            if (arg.isMandatory() && arg.getCurrentValue() == null) {
+                                if (missing == null) {
+                                    missing = "--" + arg.getLongArg();              //$NON-NLS-1$
+                                } else {
+                                    missing += ", --" + arg.getLongArg();           //$NON-NLS-1$
+                                    plural = true;
+                                }
+                            }
+                        }
+                    }
+
+                    if (missing != null) {
+                        errorMsg  = String.format(
+                                "The %1$s %2$s must be defined for action '%3$s %4$s'",
+                                plural ? "parameters" : "parameter",
+                                missing,
+                                verb,
+                                directObject);
+                    }
+
+                    mVerbRequested = verb;
+                    mDirectObjectRequested = directObject;
+                }
+            }
+        } finally {
+            if (errorMsg != null) {
+                printHelpAndExitForAction(verb, directObject, errorMsg);
+            }
+        }
+    }
+
+    /**
+     * Finds an {@link Arg} given an action name and a long flag name.
+     * @return The {@link Arg} found or null.
+     */
+    protected Arg findLongArg(String verb, String directObject, String longName) {
+        if (verb == null) {
+            verb = GLOBAL_FLAG_VERB;
+        }
+        if (directObject == null) {
+            directObject = NO_VERB_OBJECT;
+        }
+        String key = verb + '/' + directObject + '/' + longName;                    //$NON-NLS-1$
+        return mArguments.get(key);
+    }
+
+    /**
+     * Finds an {@link Arg} given an action name and a short flag name.
+     * @return The {@link Arg} found or null.
+     */
+    protected Arg findShortArg(String verb, String directObject, String shortName) {
+        if (verb == null) {
+            verb = GLOBAL_FLAG_VERB;
+        }
+        if (directObject == null) {
+            directObject = NO_VERB_OBJECT;
+        }
+
+        for (Entry<String, Arg> entry : mArguments.entrySet()) {
+            Arg arg = entry.getValue();
+            if (arg.getVerb().equals(verb) && arg.getDirectObject().equals(directObject)) {
+                if (shortName.equals(arg.getShortArg())) {
+                    return arg;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Prints the help/usage and exits.
+     *
+     * @param errorFormat Optional error message to print prior to usage using String.format
+     * @param args Arguments for String.format
+     */
+    public void printHelpAndExit(String errorFormat, Object... args) {
+        printHelpAndExitForAction(null /*verb*/, null /*directObject*/, errorFormat, args);
+    }
+
+    /**
+     * Prints the help/usage and exits.
+     *
+     * @param verb If null, displays help for all verbs. If not null, display help only
+     *          for that specific verb. In all cases also displays general usage and action list.
+     * @param directObject If null, displays help for all verb objects.
+     *          If not null, displays help only for that specific action
+     *          In all cases also display general usage and action list.
+     * @param errorFormat Optional error message to print prior to usage using String.format
+     * @param args Arguments for String.format
+     */
+    public void printHelpAndExitForAction(String verb, String directObject,
+            String errorFormat, Object... args) {
+        if (errorFormat != null && errorFormat.length() > 0) {
+            stderr(errorFormat, args);
+        }
+
+        /*
+         * usage should fit in 80 columns
+         *   12345678901234567890123456789012345678901234567890123456789012345678901234567890
+         */
+        stdout("\n" +
+            "Usage:\n" +
+            "  android [global options] %s [action options]\n" +
+            "\n" +
+            "Global options:",
+            verb == null ? "action" :
+                verb + (directObject == null ? "" : " " + directObject));           //$NON-NLS-1$
+        listOptions(GLOBAL_FLAG_VERB, NO_VERB_OBJECT);
+
+        if (verb == null || directObject == null) {
+            stdout("\nValid actions are composed of a verb and an optional direct object:");
+            for (String[] action : mActions) {
+                if (verb == null || verb.equals(action[ACTION_VERB_INDEX])) {
+                    stdout("- %1$6s %2$-13s: %3$s",
+                            action[ACTION_VERB_INDEX],
+                            action[ACTION_OBJECT_INDEX],
+                            action[ACTION_DESC_INDEX]);
+                }
+            }
+        }
+
+        // Only print details if a verb/object is requested
+        if (verb != null) {
+            for (String[] action : mActions) {
+                if (verb == null || verb.equals(action[ACTION_VERB_INDEX])) {
+                    if (directObject == null || directObject.equals(action[ACTION_OBJECT_INDEX])) {
+                        stdout("\nAction \"%1$s %2$s\":",
+                                action[ACTION_VERB_INDEX],
+                                action[ACTION_OBJECT_INDEX]);
+                        stdout("  %1$s", action[ACTION_DESC_INDEX]);
+                        stdout("Options:");
+                        listOptions(action[ACTION_VERB_INDEX], action[ACTION_OBJECT_INDEX]);
+                    }
+                }
+            }
+        }
+
+        exit();
+    }
+
+    /**
+     * Internal helper to print all the option flags for a given action name.
+     */
+    protected void listOptions(String verb, String directObject) {
+        int numOptions = 0;
+        int longArgLen = 8;
+
+        for (Entry<String, Arg> entry : mArguments.entrySet()) {
+            Arg arg = entry.getValue();
+            if (arg.getVerb().equals(verb) && arg.getDirectObject().equals(directObject)) {
+                int n = arg.getLongArg().length();
+                if (n > longArgLen) {
+                    longArgLen = n;
+                }
+            }
+        }
+
+        for (Entry<String, Arg> entry : mArguments.entrySet()) {
+            Arg arg = entry.getValue();
+            if (arg.getVerb().equals(verb) && arg.getDirectObject().equals(directObject)) {
+
+                String value = "";                                              //$NON-NLS-1$
+                String required = "";                                           //$NON-NLS-1$
+                if (arg.isMandatory()) {
+                    required = " [required]";
+
+                } else {
+                    if (arg.getDefaultValue() instanceof String[]) {
+                        for (String v : (String[]) arg.getDefaultValue()) {
+                            if (value.length() > 0) {
+                                value += ", ";
+                            }
+                            value += v;
+                        }
+                    } else if (arg.getDefaultValue() != null) {
+                        Object v = arg.getDefaultValue();
+                        if (arg.getMode() != Mode.BOOLEAN || v.equals(Boolean.TRUE)) {
+                            value = v.toString();
+                        }
+                    }
+                    if (value.length() > 0) {
+                        value = " [Default: " + value + "]";
+                    }
+                }
+
+                // Java doesn't support * for printf variable width, so we'll insert the long arg
+                // width "manually" in the printf format string.
+                String longArgWidth = Integer.toString(longArgLen + 2);
+
+                // Print a line in the form " -1_letter_arg --long_arg description"
+                // where either the 1-letter arg or the long arg are optional.
+                String output = String.format(
+                        "  %1$-2s %2$-" + longArgWidth + "s: %3$s%4$s%5$s", //$NON-NLS-1$ //$NON-NLS-2$
+                        arg.getShortArg().length() > 0 ?
+                                "-" + arg.getShortArg() :                              //$NON-NLS-1$
+                                "",                                                    //$NON-NLS-1$
+                        arg.getLongArg().length() > 0 ?
+                                "--" + arg.getLongArg() :                              //$NON-NLS-1$
+                                "",                                                    //$NON-NLS-1$
+                        arg.getDescription(),
+                        value,
+                        required);
+                stdout(output);
+                numOptions++;
+            }
+        }
+
+        if (numOptions == 0) {
+            stdout("  No options");
+        }
+    }
+
+    //----
+
+    private static enum Accept {
+        CONTINUE,
+        ACCEPT_AND_STOP,
+        REJECT_AND_STOP,
+    }
+
+    /**
+     * The mode of an argument specifies the type of variable it represents,
+     * whether an extra parameter is required after the flag and how to parse it.
+     */
+    public static enum Mode {
+        /** Argument value is a Boolean. Default value is a Boolean. */
+        BOOLEAN {
+            @Override
+            public boolean needsExtra() {
+                return false;
+            }
+            @Override
+            public Object process(Arg arg, String extra) {
+                // Toggle the current value
+                arg.setCurrentValue(! ((Boolean) arg.getCurrentValue()).booleanValue());
+                return Accept.ACCEPT_AND_STOP;
+            }
+        },
+
+        /** Argument value is an Integer. Default value is an Integer. */
+        INTEGER {
+            @Override
+            public boolean needsExtra() {
+                return true;
+            }
+            @Override
+            public Object process(Arg arg, String extra) {
+                try {
+                    arg.setCurrentValue(Integer.parseInt(extra));
+                    return null;
+                } catch (NumberFormatException e) {
+                    return String.format("Failed to parse '%1$s' as an integer: %2$s", extra,
+                            e.getMessage());
+                }
+            }
+        },
+
+        /** Argument value is a String. Default value is a String[]. */
+        ENUM {
+            @Override
+            public boolean needsExtra() {
+                return true;
+            }
+            @Override
+            public Object process(Arg arg, String extra) {
+                StringBuilder desc = new StringBuilder();
+                String[] values = (String[]) arg.getDefaultValue();
+                for (String value : values) {
+                    if (value.equals(extra)) {
+                        arg.setCurrentValue(extra);
+                        return Accept.ACCEPT_AND_STOP;
+                    }
+
+                    if (desc.length() != 0) {
+                        desc.append(", ");
+                    }
+                    desc.append(value);
+                }
+
+                return String.format("'%1$s' is not one of %2$s", extra, desc.toString());
+            }
+        },
+
+        /** Argument value is a String. Default value is a null. */
+        STRING {
+            @Override
+            public boolean needsExtra() {
+                return true;
+            }
+            @Override
+            public Object process(Arg arg, String extra) {
+                arg.setCurrentValue(extra);
+                return Accept.ACCEPT_AND_STOP;
+            }
+        },
+
+        /** Argument value is a {@link List}<String>. Default value is an empty list. */
+        STRING_ARRAY {
+            @Override
+            public boolean needsExtra() {
+                return true;
+            }
+            @Override
+            public Object process(Arg arg, String extra) {
+                // For simplification, a string array doesn't accept something that
+                // starts with a dash unless a pure -- was seen before.
+                if (extra != null) {
+                    Object v = arg.getCurrentValue();
+                    if (v == null) {
+                        ArrayList<String> a = new ArrayList<String>();
+                        arg.setCurrentValue(a);
+                        v = a;
+                    }
+                    if (v instanceof List<?>) {
+                        @SuppressWarnings("unchecked") List<String> a = (List<String>) v;
+
+                        if (extra.equals("--") ||
+                                !extra.startsWith("-") ||
+                                (extra.startsWith("-") && a.contains("--"))) {
+                            a.add(extra);
+                            return Accept.CONTINUE;
+                        } else if (a.isEmpty()) {
+                            return "No values provided";
+                        }
+                    }
+                }
+                return Accept.REJECT_AND_STOP;
+            }
+        };
+
+        /**
+         * Returns true if this mode requires an extra parameter.
+         */
+        public abstract boolean needsExtra();
+
+        /**
+         * Processes the flag for this argument.
+         *
+         * @param arg The argument being processed.
+         * @param extra The extra parameter. Null if {@link #needsExtra()} returned false.
+         * @return {@link Accept#CONTINUE} if this argument can use multiple values and
+         *   wishes to receive more.
+         *   Or {@link Accept#ACCEPT_AND_STOP} if this was the last value accepted by the argument.
+         *   Or {@link Accept#REJECT_AND_STOP} if this was value was reject and the argument
+         *   stops accepting new values with no error.
+         *   Or a string in case of error.
+         *   Never returns null.
+         */
+        public abstract Object process(Arg arg, String extra);
+    }
+
+    /**
+     * An argument accepted by the command-line, also called "a flag".
+     * Arguments must have a short version (one letter), a long version name and a description.
+     * They can have a default value, or it can be null.
+     * Depending on the {@link Mode}, the default value can be a Boolean, an Integer, a String
+     * or a String array (in which case the first item is the current by default.)
+     */
+    static class Arg {
+        /** Verb for that argument. Never null. */
+        private final String mVerb;
+        /** Direct Object for that argument. Never null, but can be empty string. */
+        private final String mDirectObject;
+        /** The 1-letter short name of the argument, e.g. -v. */
+        private final String mShortName;
+        /** The long name of the argument, e.g. --verbose. */
+        private final String mLongName;
+        /** A description. Never null. */
+        private final String mDescription;
+        /** A default value. Can be null. */
+        private final Object mDefaultValue;
+        /** The argument mode (type + process method). Never null. */
+        private final Mode mMode;
+        /** True if this argument is mandatory for this verb/directobject. */
+        private final boolean mMandatory;
+        /** Current value. Initially set to the default value. */
+        private Object mCurrentValue;
+        /** True if the argument has been used on the command line. */
+        private boolean mInCommandLine;
+
+        /**
+         * Creates a new argument flag description.
+         *
+         * @param mode The {@link Mode} for the argument.
+         * @param mandatory True if this argument is mandatory for this action.
+         * @param verb The verb name. Never null. Can be {@link CommandLineParser#GLOBAL_FLAG_VERB}.
+         * @param directObject The action name. Can be {@link CommandLineParser#NO_VERB_OBJECT}.
+         * @param shortName The one-letter short argument name. Can be empty but not null.
+         * @param longName The long argument name. Can be empty but not null.
+         * @param description The description. Cannot be null.
+         * @param defaultValue The default value (or values), which depends on the selected
+         *          {@link Mode}. Can be null.
+         */
+        public Arg(Mode mode,
+                   boolean mandatory,
+                   @NonNull String verb,
+                   @NonNull String directObject,
+                   @NonNull String shortName,
+                   @NonNull String longName,
+                   @NonNull String description,
+                   @Nullable Object defaultValue) {
+            mMode = mode;
+            mMandatory = mandatory;
+            mVerb = verb;
+            mDirectObject = directObject;
+            mShortName = shortName;
+            mLongName = longName;
+            mDescription = description;
+            mDefaultValue = defaultValue;
+            mInCommandLine = false;
+            if (defaultValue instanceof String[]) {
+                mCurrentValue = ((String[])defaultValue)[0];
+            } else {
+                mCurrentValue = mDefaultValue;
+            }
+        }
+
+        /** Return true if this argument is mandatory for this verb/directobject. */
+        public boolean isMandatory() {
+            return mMandatory;
+        }
+
+        /** Returns the 1-letter short name of the argument, e.g. -v. */
+        public String getShortArg() {
+            return mShortName;
+        }
+
+        /** Returns the long name of the argument, e.g. --verbose. */
+        public String getLongArg() {
+            return mLongName;
+        }
+
+        /** Returns the description. Never null. */
+        public String getDescription() {
+            return mDescription;
+        }
+
+        /** Returns the verb for that argument. Never null. */
+        public String getVerb() {
+            return mVerb;
+        }
+
+        /** Returns the direct Object for that argument. Never null, but can be empty string. */
+        public String getDirectObject() {
+            return mDirectObject;
+        }
+
+        /** Returns the default value. Can be null. */
+        public Object getDefaultValue() {
+            return mDefaultValue;
+        }
+
+        /** Returns the current value. Initially set to the default value. Can be null. */
+        public Object getCurrentValue() {
+            return mCurrentValue;
+        }
+
+        /** Sets the current value. Can be null. */
+        public void setCurrentValue(Object currentValue) {
+            mCurrentValue = currentValue;
+        }
+
+        /** Returns the argument mode (type + process method). Never null. */
+        public Mode getMode() {
+            return mMode;
+        }
+
+        /** Returns true if the argument has been used on the command line. */
+        public boolean isInCommandLine() {
+            return mInCommandLine;
+        }
+
+        /** Sets if the argument has been used on the command line. */
+        public void setInCommandLine(boolean inCommandLine) {
+            mInCommandLine = inCommandLine;
+        }
+    }
+
+    /**
+     * Internal helper to define a new argument for a give action.
+     *
+     * @param mode The {@link Mode} for the argument.
+     * @param mandatory The argument is required (never if {@link Mode#BOOLEAN})
+     * @param verb The verb name. Never null. Can be {@link CommandLineParser#GLOBAL_FLAG_VERB}.
+     * @param directObject The action name. Can be {@link CommandLineParser#NO_VERB_OBJECT}.
+     * @param shortName The one-letter short argument name. Can be empty but not null.
+     * @param longName The long argument name. Can be empty but not null.
+     * @param description The description. Cannot be null.
+     * @param defaultValue The default value (or values), which depends on the selected
+     *          {@link Mode}.
+     */
+    protected void define(Mode mode,
+            boolean mandatory,
+            @NonNull String verb,
+            @NonNull String directObject,
+            @NonNull String shortName,
+            @NonNull String longName,
+            @NonNull String description,
+            @Nullable Object defaultValue) {
+        assert verb != null;
+        assert(!(mandatory && mode == Mode.BOOLEAN)); // a boolean mode cannot be mandatory
+
+        // We should always have at least a short or long name, ideally both but never none.
+        assert shortName != null;
+        assert longName != null;
+        assert shortName.length() > 0 || longName.length()  > 0;
+
+        if (directObject == null) {
+            directObject = NO_VERB_OBJECT;
+        }
+
+        String key = verb + '/' + directObject + '/' + longName;
+        mArguments.put(key, new Arg(mode, mandatory,
+                verb, directObject, shortName, longName, description, defaultValue));
+    }
+
+    /**
+     * Exits in case of error.
+     * This is protected so that it can be overridden in unit tests.
+     */
+    protected void exit() {
+        System.exit(1);
+    }
+
+    /**
+     * Prints a line to stdout.
+     * This is protected so that it can be overridden in unit tests.
+     *
+     * @param format The string to be formatted. Cannot be null.
+     * @param args Format arguments.
+     */
+    protected void stdout(String format, Object...args) {
+        String output = String.format(format, args);
+        output = LineUtil.reflowLine(output);
+        mLog.info("%s\n", output);    //$NON-NLS-1$
+    }
+
+    /**
+     * Prints a line to stderr.
+     * This is protected so that it can be overridden in unit tests.
+     *
+     * @param format The string to be formatted. Cannot be null.
+     * @param args Format arguments.
+     */
+    protected void stderr(String format, Object...args) {
+        mLog.error(null, format, args);
+    }
+}
diff --git a/sdklib/src/main/java/com/android/sdklib/util/FormatUtils.java b/sdklib/src/main/java/com/android/sdklib/util/FormatUtils.java
new file mode 100755
index 0000000..0ff5e69
--- /dev/null
+++ b/sdklib/src/main/java/com/android/sdklib/util/FormatUtils.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdklib.util;
+
+import com.android.annotations.NonNull;
+
+/**
+ * Helper methods to do some format conversions.
+ */
+public abstract class FormatUtils {
+
+    /**
+     * Converts a byte size to a human readable string,
+     * for example "3 MiB", "1020 Bytes" or "1.2 GiB".
+     *
+     * @param size The byte size to convert.
+     * @return A new non-null string, with the size expressed in either Bytes
+     *   or KiB or MiB or GiB.
+     */
+    @NonNull
+    public static String byteSizeToString(long size) {
+        String sizeStr;
+
+        if (size < 1024) {
+            sizeStr = String.format("%d Bytes", size);
+        } else if (size < 1024 * 1024) {
+            sizeStr = String.format("%d KiB", Math.round(size / 1024.0));
+        } else if (size < 1024 * 1024 * 1024) {
+            sizeStr = String.format("%.1f MiB",
+                    Math.round(10.0 * size / (1024 * 1024.0))/ 10.0);
+        } else {
+            sizeStr = String.format("%.1f GiB",
+                    Math.round(10.0 * size / (1024 * 1024 * 1024.0))/ 10.0);
+        }
+
+        return sizeStr;
+    }
+
+}
diff --git a/sdklib/src/main/java/com/android/sdklib/util/GrabProcessOutput.java b/sdklib/src/main/java/com/android/sdklib/util/GrabProcessOutput.java
new file mode 100755
index 0000000..3d3734c
--- /dev/null
+++ b/sdklib/src/main/java/com/android/sdklib/util/GrabProcessOutput.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdklib.util;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+public class GrabProcessOutput {
+
+    public enum Wait {
+        /**
+         * Doesn't wait for the exec to complete.
+         * This still monitors the output but does not wait for the process to finish.
+         * In this mode the process return code is unknown and always 0.
+         */
+        ASYNC,
+        /**
+         * This waits for the process to finish.
+         * In this mode, {@link GrabProcessOutput#grabProcessOutput} returns the
+         * error code from the process.
+         * In some rare cases and depending on the OS, the process might not have
+         * finished dumping data into stdout/stderr.
+         * <p/>
+         * Use this when you don't particularly care for the output but instead
+         * care for the return code of the executed process.
+         */
+        WAIT_FOR_PROCESS,
+        /**
+         * This waits for the process to finish <em>and</em> for the stdout/stderr
+         * threads to complete.
+         * In this mode, {@link GrabProcessOutput#grabProcessOutput} returns the
+         * error code from the process.
+         * <p/>
+         * Use this one when capturing all the output from the process is important.
+         */
+        WAIT_FOR_READERS,
+    }
+
+    public interface IProcessOutput {
+        /**
+         * Processes an stdout message line.
+         * @param line The stdout message line. Null when the reader reached the end of stdout.
+         */
+        public void out(@Nullable String line);
+        /**
+         * Processes an stderr message line.
+         * @param line The stderr message line. Null when the reader reached the end of stderr.
+         */
+        public void err(@Nullable String line);
+    }
+
+    /**
+     * Get the stderr/stdout outputs of a process and return when the process is done.
+     * Both <b>must</b> be read or the process will block on windows.
+     *
+     * @param process The process to get the output from.
+     * @param output Optional object to capture stdout/stderr.
+     *      Note that on Windows capturing the output is not optional. If output is null
+     *      the stdout/stderr will be captured and discarded.
+     * @param waitMode Whether to wait for the process and/or the readers to finish.
+     * @return the process return code.
+     * @throws InterruptedException if {@link Process#waitFor()} was interrupted.
+     */
+    public static int grabProcessOutput(
+            @NonNull final Process process,
+            Wait waitMode,
+            @Nullable final IProcessOutput output) throws InterruptedException {
+        // read the lines as they come. if null is returned, it's
+        // because the process finished
+        Thread threadErr = new Thread("stderr") {
+            @Override
+            public void run() {
+                // create a buffer to read the stderr output
+                InputStreamReader is = new InputStreamReader(process.getErrorStream());
+                BufferedReader errReader = new BufferedReader(is);
+
+                try {
+                    while (true) {
+                        String line = errReader.readLine();
+                        if (output != null) {
+                            output.err(line);
+                        }
+                        if (line == null) {
+                            break;
+                        }
+                    }
+                } catch (IOException e) {
+                    // do nothing.
+                }
+            }
+        };
+
+        Thread threadOut = new Thread("stdout") {
+            @Override
+            public void run() {
+                InputStreamReader is = new InputStreamReader(process.getInputStream());
+                BufferedReader outReader = new BufferedReader(is);
+
+                try {
+                    while (true) {
+                        String line = outReader.readLine();
+                        if (output != null) {
+                            output.out(line);
+                        }
+                        if (line == null) {
+                            break;
+                        }
+                    }
+                } catch (IOException e) {
+                    // do nothing.
+                }
+            }
+        };
+
+        threadErr.start();
+        threadOut.start();
+
+        if (waitMode == Wait.ASYNC) {
+            return 0;
+        }
+
+        // it looks like on windows process#waitFor() can return
+        // before the thread have filled the arrays, so we wait for both threads and the
+        // process itself.
+        if (waitMode == Wait.WAIT_FOR_READERS) {
+            try {
+                threadErr.join();
+            } catch (InterruptedException e) {
+            }
+            try {
+                threadOut.join();
+            } catch (InterruptedException e) {
+            }
+        }
+
+        // get the return code from the process
+        return process.waitFor();
+    }
+}
diff --git a/sdklib/src/main/java/com/android/sdklib/util/LineUtil.java b/sdklib/src/main/java/com/android/sdklib/util/LineUtil.java
new file mode 100755
index 0000000..c42bd0d
--- /dev/null
+++ b/sdklib/src/main/java/com/android/sdklib/util/LineUtil.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdklib.util;
+
+
+public abstract class LineUtil {
+
+    /**
+     * Reformats a line so that it fits in 78 characters max.
+     * <p/>
+     * When wrapping the second line and following, prefix the string with a number of
+     * spaces. This will use the first colon (:) to determine the prefix size
+     * or use 4 as a minimum if there are no colons in the string.
+     *
+     * @param line The line to reflow. Must be non-null.
+     * @return A new line to print as-is, that contains \n as needed.
+     */
+    public static String reflowLine(String line) {
+        final int maxLen = 78;
+
+        // Most of time the line will fit in the given length and this will be a no-op
+        int n = line.length();
+        int cr = line.indexOf('\n');
+        if (n <= maxLen && (cr == -1 || cr == n - 1)) {
+            return line;
+        }
+
+        int prefixSize = line.indexOf(':') + 1;
+        // If there' some spacing after the colon, use the same when wrapping
+        if (prefixSize > 0 && prefixSize < maxLen) {
+            while(prefixSize < n && line.charAt(prefixSize) == ' ') {
+                prefixSize++;
+            }
+        } else {
+            prefixSize = 4;
+        }
+        String prefix = String.format(
+                "%-" + Integer.toString(prefixSize) + "s",      //$NON-NLS-1$ //$NON-NLS-2$
+                " ");                                           //$NON-NLS-1$
+
+        StringBuilder output = new StringBuilder(n + prefixSize);
+
+        while (n > 0) {
+            cr = line.indexOf('\n');
+            if (n <= maxLen && (cr == -1 || cr == n - 1)) {
+                output.append(line);
+                break;
+            }
+
+            // Line is longer than the max length, find the first character before and after
+            // the whitespace where we want to break the line.
+            int posNext = maxLen;
+            if (cr != -1 && cr != n - 1 && cr <= posNext) {
+                posNext = cr + 1;
+                while (posNext < n && line.charAt(posNext) == '\n') {
+                    posNext++;
+                }
+            }
+            while (posNext < n && line.charAt(posNext) == ' ') {
+                posNext++;
+            }
+            while (posNext > 0) {
+                char c = line.charAt(posNext - 1);
+                if (c != ' ' && c != '\n') {
+                    posNext--;
+                } else {
+                    break;
+                }
+            }
+
+            if (posNext == 0 || (posNext >= n && maxLen < n)) {
+                // We found no whitespace separator. This should generally not occur.
+                posNext = maxLen;
+            }
+            int posPrev = posNext;
+            while (posPrev > 0) {
+                char c = line.charAt(posPrev - 1);
+                if (c == ' ' || c == '\n') {
+                    posPrev--;
+                } else {
+                    break;
+                }
+            }
+
+            output.append(line.substring(0, posPrev)).append('\n');
+            line = prefix + line.substring(posNext);
+            n = line.length();
+        }
+
+        return output.toString();
+    }
+
+    /**
+     * Formats the string using {@link String#format(String, Object...)}
+     * and then returns the result of {@link #reflowLine(String)}.
+     *
+     * @param format The string format.
+     * @param params The parameters for the string format.
+     * @return The result of {@link #reflowLine(String)} on the formatted string.
+     */
+    public static String reformatLine(String format, Object...params) {
+        return reflowLine(String.format(format, params));
+    }
+}
diff --git a/sdklib/src/main/java/com/android/sdklib/util/SparseArray.java b/sdklib/src/main/java/com/android/sdklib/util/SparseArray.java
new file mode 100644
index 0000000..f0693fe
--- /dev/null
+++ b/sdklib/src/main/java/com/android/sdklib/util/SparseArray.java
@@ -0,0 +1,401 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdklib.util;
+
+
+/**
+ * SparseArrays map integers to Objects.  Unlike a normal array of Objects,
+ * there can be gaps in the indices.  It is intended to be more efficient
+ * than using a HashMap to map Integers to Objects.
+ */
+public class SparseArray<E> {
+    private static final Object DELETED = new Object();
+    private boolean mGarbage = false;
+
+    /**
+     * Creates a new SparseArray containing no mappings.
+     */
+    public SparseArray() {
+        this(10);
+    }
+
+    /**
+     * Creates a new SparseArray containing no mappings that will not
+     * require any additional memory allocation to store the specified
+     * number of mappings.
+     */
+    public SparseArray(int initialCapacity) {
+        initialCapacity = ArrayUtils.idealIntArraySize(initialCapacity);
+
+        mKeys = new int[initialCapacity];
+        mValues = new Object[initialCapacity];
+        mSize = 0;
+    }
+
+    /**
+     * Gets the Object mapped from the specified key, or <code>null</code>
+     * if no such mapping has been made.
+     */
+    public E get(int key) {
+        return get(key, null);
+    }
+
+    /**
+     * Gets the Object mapped from the specified key, or the specified Object
+     * if no such mapping has been made.
+     */
+    @SuppressWarnings("unchecked")
+    public E get(int key, E valueIfKeyNotFound) {
+        int i = binarySearch(mKeys, 0, mSize, key);
+
+        if (i < 0 || mValues[i] == DELETED) {
+            return valueIfKeyNotFound;
+        } else {
+            return (E) mValues[i];
+        }
+    }
+
+    /**
+     * Removes the mapping from the specified key, if there was any.
+     */
+    public void delete(int key) {
+        int i = binarySearch(mKeys, 0, mSize, key);
+
+        if (i >= 0) {
+            if (mValues[i] != DELETED) {
+                mValues[i] = DELETED;
+                mGarbage = true;
+            }
+        }
+    }
+
+    /**
+     * Alias for {@link #delete(int)}.
+     */
+    public void remove(int key) {
+        delete(key);
+    }
+
+    private void gc() {
+        // Log.e("SparseArray", "gc start with " + mSize);
+
+        int n = mSize;
+        int o = 0;
+        int[] keys = mKeys;
+        Object[] values = mValues;
+
+        for (int i = 0; i < n; i++) {
+            Object val = values[i];
+
+            if (val != DELETED) {
+                if (i != o) {
+                    keys[o] = keys[i];
+                    values[o] = val;
+                }
+
+                o++;
+            }
+        }
+
+        mGarbage = false;
+        mSize = o;
+
+        // Log.e("SparseArray", "gc end with " + mSize);
+    }
+
+    /**
+     * Adds a mapping from the specified key to the specified value,
+     * replacing the previous mapping from the specified key if there
+     * was one.
+     */
+    public void put(int key, E value) {
+        int i = binarySearch(mKeys, 0, mSize, key);
+
+        if (i >= 0) {
+            mValues[i] = value;
+        } else {
+            i = ~i;
+
+            if (i < mSize && mValues[i] == DELETED) {
+                mKeys[i] = key;
+                mValues[i] = value;
+                return;
+            }
+
+            if (mGarbage && mSize >= mKeys.length) {
+                gc();
+
+                // Search again because indices may have changed.
+                i = ~binarySearch(mKeys, 0, mSize, key);
+            }
+
+            if (mSize >= mKeys.length) {
+                int n = ArrayUtils.idealIntArraySize(mSize + 1);
+
+                int[] nkeys = new int[n];
+                Object[] nvalues = new Object[n];
+
+                // Log.e("SparseArray", "grow " + mKeys.length + " to " + n);
+                System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
+                System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
+
+                mKeys = nkeys;
+                mValues = nvalues;
+            }
+
+            if (mSize - i != 0) {
+                // Log.e("SparseArray", "move " + (mSize - i));
+                System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
+                System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
+            }
+
+            mKeys[i] = key;
+            mValues[i] = value;
+            mSize++;
+        }
+    }
+
+    /**
+     * Returns the number of key-value mappings that this SparseArray
+     * currently stores.
+     */
+    public int size() {
+        if (mGarbage) {
+            gc();
+        }
+
+        return mSize;
+    }
+
+    /**
+     * Given an index in the range <code>0...size()-1</code>, returns
+     * the key from the <code>index</code>th key-value mapping that this
+     * SparseArray stores.
+     */
+    public int keyAt(int index) {
+        if (mGarbage) {
+            gc();
+        }
+
+        return mKeys[index];
+    }
+
+    /**
+     * Given an index in the range <code>0...size()-1</code>, returns
+     * the value from the <code>index</code>th key-value mapping that this
+     * SparseArray stores.
+     */
+    @SuppressWarnings("unchecked")
+    public E valueAt(int index) {
+        if (mGarbage) {
+            gc();
+        }
+
+        return (E) mValues[index];
+    }
+
+    /**
+     * Given an index in the range <code>0...size()-1</code>, sets a new
+     * value for the <code>index</code>th key-value mapping that this
+     * SparseArray stores.
+     */
+    public void setValueAt(int index, E value) {
+        if (mGarbage) {
+            gc();
+        }
+
+        mValues[index] = value;
+    }
+
+    /**
+     * Returns the index for which {@link #keyAt} would return the
+     * specified key, or a negative number if the specified
+     * key is not mapped.
+     */
+    public int indexOfKey(int key) {
+        if (mGarbage) {
+            gc();
+        }
+
+        return binarySearch(mKeys, 0, mSize, key);
+    }
+
+    /**
+     * Returns an index for which {@link #valueAt} would return the
+     * specified key, or a negative number if no keys map to the
+     * specified value.
+     * Beware that this is a linear search, unlike lookups by key,
+     * and that multiple keys can map to the same value and this will
+     * find only one of them.
+     */
+    public int indexOfValue(E value) {
+        if (mGarbage) {
+            gc();
+        }
+
+        for (int i = 0; i < mSize; i++)
+            if (mValues[i] == value)
+                return i;
+
+        return -1;
+    }
+
+    /**
+     * Removes all key-value mappings from this SparseArray.
+     */
+    public void clear() {
+        int n = mSize;
+        Object[] values = mValues;
+
+        for (int i = 0; i < n; i++) {
+            values[i] = null;
+        }
+
+        mSize = 0;
+        mGarbage = false;
+    }
+
+    /**
+     * Puts a key/value pair into the array, optimizing for the case where
+     * the key is greater than all existing keys in the array.
+     */
+    public void append(int key, E value) {
+        if (mSize != 0 && key <= mKeys[mSize - 1]) {
+            put(key, value);
+            return;
+        }
+
+        if (mGarbage && mSize >= mKeys.length) {
+            gc();
+        }
+
+        int pos = mSize;
+        if (pos >= mKeys.length) {
+            int n = ArrayUtils.idealIntArraySize(pos + 1);
+
+            int[] nkeys = new int[n];
+            Object[] nvalues = new Object[n];
+
+            // Log.e("SparseArray", "grow " + mKeys.length + " to " + n);
+            System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
+            System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
+
+            mKeys = nkeys;
+            mValues = nvalues;
+        }
+
+        mKeys[pos] = key;
+        mValues[pos] = value;
+        mSize = pos + 1;
+    }
+
+    public SparseArray<E> getUnmodifiable() {
+        final SparseArray<E> mStorage = this;
+        return new SparseArray<E>() {
+
+            @Override
+            public E get(int key) {
+                return mStorage.get(key);
+            }
+
+            @Override
+            public E get(int key, E valueIfKeyNotFound) {
+                return mStorage.get(key, valueIfKeyNotFound);
+            }
+
+            @Override
+            public void delete(int key) {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public void remove(int key) {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public void put(int key, E value) {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public int size() {
+                return mStorage.size();
+            }
+
+            @Override
+            public int keyAt(int index) {
+                return mStorage.keyAt(index);
+            }
+
+            @Override
+            public E valueAt(int index) {
+                return mStorage.valueAt(index);
+            }
+
+            @Override
+            public void setValueAt(int index, E value) {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public int indexOfKey(int key) {
+                return mStorage.indexOfKey(key);
+            }
+
+            @Override
+            public int indexOfValue(E value) {
+                return mStorage.indexOfValue(value);
+            }
+
+            @Override
+            public void clear() {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public void append(int key, E value) {
+                throw new UnsupportedOperationException();
+            }
+
+        };
+    }
+
+    private static int binarySearch(int[] a, int start, int len, int key) {
+        int high = start + len, low = start - 1, guess;
+
+        while (high - low > 1) {
+            guess = (high + low) / 2;
+
+            if (a[guess] < key)
+                low = guess;
+            else
+                high = guess;
+        }
+
+        if (high == start + len)
+            return ~(start + len);
+        else if (a[high] == key)
+            return high;
+        else
+            return ~high;
+    }
+
+    private int[] mKeys;
+    private Object[] mValues;
+    private int mSize;
+}
diff --git a/sdklib/src/main/java/com/android/sdklib/util/SparseIntArray.java b/sdklib/src/main/java/com/android/sdklib/util/SparseIntArray.java
new file mode 100644
index 0000000..9573566
--- /dev/null
+++ b/sdklib/src/main/java/com/android/sdklib/util/SparseIntArray.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdklib.util;
+
+
+/**
+ * SparseIntArrays map integers to integers.  Unlike a normal array of integers,
+ * there can be gaps in the indices.  It is intended to be more efficient
+ * than using a HashMap to map Integers to Integers.
+ */
+public class SparseIntArray {
+    /**
+     * Creates a new SparseIntArray containing no mappings.
+     */
+    public SparseIntArray() {
+        this(10);
+    }
+
+    /**
+     * Creates a new SparseIntArray containing no mappings that will not
+     * require any additional memory allocation to store the specified
+     * number of mappings.
+     */
+    public SparseIntArray(int initialCapacity) {
+        initialCapacity = ArrayUtils.idealIntArraySize(initialCapacity);
+
+        mKeys = new int[initialCapacity];
+        mValues = new int[initialCapacity];
+        mSize = 0;
+    }
+
+    /**
+     * Gets the int mapped from the specified key, or <code>0</code>
+     * if no such mapping has been made.
+     */
+    public int get(int key) {
+        return get(key, 0);
+    }
+
+    /**
+     * Gets the int mapped from the specified key, or the specified value
+     * if no such mapping has been made.
+     */
+    public int get(int key, int valueIfKeyNotFound) {
+        int i = binarySearch(mKeys, 0, mSize, key);
+
+        if (i < 0) {
+            return valueIfKeyNotFound;
+        } else {
+            return mValues[i];
+        }
+    }
+
+    /**
+     * Removes the mapping from the specified key, if there was any.
+     */
+    public void delete(int key) {
+        int i = binarySearch(mKeys, 0, mSize, key);
+
+        if (i >= 0) {
+            removeAt(i);
+        }
+    }
+
+    /**
+     * Removes the mapping at the given index.
+     */
+    public void removeAt(int index) {
+        System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1));
+        System.arraycopy(mValues, index + 1, mValues, index, mSize - (index + 1));
+        mSize--;
+    }
+
+    /**
+     * Adds a mapping from the specified key to the specified value,
+     * replacing the previous mapping from the specified key if there
+     * was one.
+     */
+    public void put(int key, int value) {
+        int i = binarySearch(mKeys, 0, mSize, key);
+
+        if (i >= 0) {
+            mValues[i] = value;
+        } else {
+            i = ~i;
+
+            if (mSize >= mKeys.length) {
+                int n = ArrayUtils.idealIntArraySize(mSize + 1);
+
+                int[] nkeys = new int[n];
+                int[] nvalues = new int[n];
+
+                // Log.e("SparseIntArray", "grow " + mKeys.length + " to " + n);
+                System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
+                System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
+
+                mKeys = nkeys;
+                mValues = nvalues;
+            }
+
+            if (mSize - i != 0) {
+                // Log.e("SparseIntArray", "move " + (mSize - i));
+                System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
+                System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
+            }
+
+            mKeys[i] = key;
+            mValues[i] = value;
+            mSize++;
+        }
+    }
+
+    /**
+     * Returns the number of key-value mappings that this SparseIntArray
+     * currently stores.
+     */
+    public int size() {
+        return mSize;
+    }
+
+    /**
+     * Given an index in the range <code>0...size()-1</code>, returns
+     * the key from the <code>index</code>th key-value mapping that this
+     * SparseIntArray stores.
+     */
+    public int keyAt(int index) {
+        return mKeys[index];
+    }
+
+    /**
+     * Given an index in the range <code>0...size()-1</code>, returns
+     * the value from the <code>index</code>th key-value mapping that this
+     * SparseIntArray stores.
+     */
+    public int valueAt(int index) {
+        return mValues[index];
+    }
+
+    /**
+     * Returns the index for which {@link #keyAt} would return the
+     * specified key, or a negative number if the specified
+     * key is not mapped.
+     */
+    public int indexOfKey(int key) {
+        return binarySearch(mKeys, 0, mSize, key);
+    }
+
+    /**
+     * Returns an index for which {@link #valueAt} would return the
+     * specified key, or a negative number if no keys map to the
+     * specified value.
+     * Beware that this is a linear search, unlike lookups by key,
+     * and that multiple keys can map to the same value and this will
+     * find only one of them.
+     */
+    public int indexOfValue(int value) {
+        for (int i = 0; i < mSize; i++)
+            if (mValues[i] == value)
+                return i;
+
+        return -1;
+    }
+
+    /**
+     * Removes all key-value mappings from this SparseIntArray.
+     */
+    public void clear() {
+        mSize = 0;
+    }
+
+    /**
+     * Puts a key/value pair into the array, optimizing for the case where
+     * the key is greater than all existing keys in the array.
+     */
+    public void append(int key, int value) {
+        if (mSize != 0 && key <= mKeys[mSize - 1]) {
+            put(key, value);
+            return;
+        }
+
+        int pos = mSize;
+        if (pos >= mKeys.length) {
+            int n = ArrayUtils.idealIntArraySize(pos + 1);
+
+            int[] nkeys = new int[n];
+            int[] nvalues = new int[n];
+
+            // Log.e("SparseIntArray", "grow " + mKeys.length + " to " + n);
+            System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
+            System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
+
+            mKeys = nkeys;
+            mValues = nvalues;
+        }
+
+        mKeys[pos] = key;
+        mValues[pos] = value;
+        mSize = pos + 1;
+    }
+
+    private static int binarySearch(int[] a, int start, int len, int key) {
+        int high = start + len, low = start - 1, guess;
+
+        while (high - low > 1) {
+            guess = (high + low) / 2;
+
+            if (a[guess] < key)
+                low = guess;
+            else
+                high = guess;
+        }
+
+        if (high == start + len)
+            return ~(start + len);
+        else if (a[high] == key)
+            return high;
+        else
+            return ~high;
+    }
+
+    private int[] mKeys;
+    private int[] mValues;
+    private int mSize;
+}
diff --git a/sdkmanager/MODULE_LICENSE_APACHE2 b/sdkmanager/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
diff --git a/sdkmanager/sdkuilib/.classpath b/sdkmanager/sdkuilib/.classpath
new file mode 100644
index 0000000..8d67591
--- /dev/null
+++ b/sdkmanager/sdkuilib/.classpath
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src/main/java"/>
+	<classpathentry kind="src" path="src/test/java"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+	<classpathentry combineaccessrules="false" exported="true" kind="src" path="/sdklib"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/3"/>
+	<classpathentry exported="true" kind="var" path="ANDROID_OUT_FRAMEWORK/swt.jar"/>
+	<classpathentry combineaccessrules="false" exported="true" kind="src" path="/layoutlib-api"/>
+	<classpathentry combineaccessrules="false" exported="true" kind="src" path="/common"/>
+	<classpathentry combineaccessrules="false" exported="true" kind="src" path="/swtmenubar"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/sdkmanager/sdkuilib/.project b/sdkmanager/sdkuilib/.project
new file mode 100644
index 0000000..254725c
--- /dev/null
+++ b/sdkmanager/sdkuilib/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>sdkuilib</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>
diff --git a/sdkmanager/sdkuilib/.settings/org.eclipse.jdt.core.prefs b/sdkmanager/sdkuilib/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..9dbff07
--- /dev/null
+++ b/sdkmanager/sdkuilib/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,98 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
+org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault
+org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled
+org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
+org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
+org.eclipse.jdt.core.compiler.problem.deadCode=warning
+org.eclipse.jdt.core.compiler.problem.deprecation=warning
+org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
+org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
+org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
+org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning
+org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled
+org.eclipse.jdt.core.compiler.problem.fieldHiding=warning
+org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
+org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=error
+org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
+org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled
+org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
+org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning
+org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning
+org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
+org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
+org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
+org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
+org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
+org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
+org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
+org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
+org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
+org.eclipse.jdt.core.compiler.problem.nullReference=error
+org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning
+org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore
+org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
+org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning
+org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
+org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
+org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
+org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled
+org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
+org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
+org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
+org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled
+org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning
+org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error
+org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning
+org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.unusedImport=warning
+org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
+org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
+org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning
+org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled
+org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
+org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
+org.eclipse.jdt.core.compiler.source=1.6
diff --git a/sdkmanager/sdkuilib/.settings/org.eclipse.jdt.ui.prefs b/sdkmanager/sdkuilib/.settings/org.eclipse.jdt.ui.prefs
new file mode 100755
index 0000000..cc5f0a2
--- /dev/null
+++ b/sdkmanager/sdkuilib/.settings/org.eclipse.jdt.ui.prefs
@@ -0,0 +1,55 @@
+#Tue Aug 07 12:32:25 PDT 2012
+eclipse.preferences.version=1
+editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
+sp_cleanup.add_default_serial_version_id=true
+sp_cleanup.add_generated_serial_version_id=false
+sp_cleanup.add_missing_annotations=false
+sp_cleanup.add_missing_deprecated_annotations=true
+sp_cleanup.add_missing_methods=false
+sp_cleanup.add_missing_nls_tags=false
+sp_cleanup.add_missing_override_annotations=true
+sp_cleanup.add_missing_override_annotations_interface_methods=false
+sp_cleanup.add_serial_version_id=false
+sp_cleanup.always_use_blocks=true
+sp_cleanup.always_use_parentheses_in_expressions=false
+sp_cleanup.always_use_this_for_non_static_field_access=false
+sp_cleanup.always_use_this_for_non_static_method_access=false
+sp_cleanup.convert_to_enhanced_for_loop=false
+sp_cleanup.correct_indentation=false
+sp_cleanup.format_source_code=false
+sp_cleanup.format_source_code_changes_only=false
+sp_cleanup.make_local_variable_final=false
+sp_cleanup.make_parameters_final=false
+sp_cleanup.make_private_fields_final=true
+sp_cleanup.make_type_abstract_if_missing_method=false
+sp_cleanup.make_variable_declarations_final=false
+sp_cleanup.never_use_blocks=false
+sp_cleanup.never_use_parentheses_in_expressions=true
+sp_cleanup.on_save_use_additional_actions=true
+sp_cleanup.organize_imports=true
+sp_cleanup.qualify_static_field_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
+sp_cleanup.remove_private_constructors=true
+sp_cleanup.remove_trailing_whitespaces=true
+sp_cleanup.remove_trailing_whitespaces_all=true
+sp_cleanup.remove_trailing_whitespaces_ignore_empty=false
+sp_cleanup.remove_unnecessary_casts=false
+sp_cleanup.remove_unnecessary_nls_tags=false
+sp_cleanup.remove_unused_imports=false
+sp_cleanup.remove_unused_local_variables=false
+sp_cleanup.remove_unused_private_fields=true
+sp_cleanup.remove_unused_private_members=false
+sp_cleanup.remove_unused_private_methods=true
+sp_cleanup.remove_unused_private_types=true
+sp_cleanup.sort_members=false
+sp_cleanup.sort_members_all=false
+sp_cleanup.use_blocks=false
+sp_cleanup.use_blocks_only_for_return_and_throw=false
+sp_cleanup.use_parentheses_in_expressions=false
+sp_cleanup.use_this_for_non_static_field_access=false
+sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true
+sp_cleanup.use_this_for_non_static_method_access=false
+sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true
diff --git a/sdkmanager/sdkuilib/MODULE_LICENSE_APACHE2 b/sdkmanager/sdkuilib/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
diff --git a/sdkmanager/sdkuilib/NOTICE b/sdkmanager/sdkuilib/NOTICE
new file mode 100644
index 0000000..c5b1efa
--- /dev/null
+++ b/sdkmanager/sdkuilib/NOTICE
@@ -0,0 +1,190 @@
+
+   Copyright (c) 2005-2008, 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.
+
+   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.
+
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
diff --git a/sdkmanager/sdkuilib/README b/sdkmanager/sdkuilib/README
new file mode 100644
index 0000000..dee4a24
--- /dev/null
+++ b/sdkmanager/sdkuilib/README
@@ -0,0 +1,45 @@
+Using the Eclipse project SdkUiLib
+----------------------------------
+
+1- sdkuilib requires SWT to compile.
+
+SWT is available in the tree under prebuild/<platform>/swt
+
+Because the build path cannot contain relative path that are not inside the project directory,
+the .classpath file references a user library called ANDROID_SWT.
+
+In order to compile the project:
+- Open Preferences > Java > Build Path > User Libraries
+- Create a new user library named ANDROID_SWT
+- Add the following 4 JAR files:
+
+  - prebuilt/<platform>/swt/swt.jar
+  - prebuilt/common/eclipse/org.eclipse.core.commands_3.*.jar
+  - prebuilt/common/eclipse/org.eclipse.equinox.common_3.*.jar
+  - prebuilt/common/eclipse/org.eclipse.jface_3.*.jar
+
+
+2- sdkuilib also requires the compiled swtmenubar library.
+
+Build the swtmenubar library:
+$ cd $TOP (top of Android tree)
+$ . build/envsetup.sh && lunch sdk-eng
+$ sdk/eclipse/scripts/create_sdkman_symlinks.sh
+
+Define a classpath variable in Eclipse:
+- Open Preferences > Java > Build Path > Classpath Variables
+- Create a new classpath variable named ANDROID_OUT_FRAMEWORK
+- Set its folder value to <Android tree>/out/host/<platform>/framework
+- Create a new classpath variable named ANDROID_SRC
+- Set its folder value to <Android tree>
+
+You might need to clean the SdkUiLib project (Project > Clean...) after
+you add the new classpath variable, otherwise previous errors might not
+go away automatically.
+
+The ANDROID_SRC part should be optional. It allows you to have access to
+the SwtMenuBar generic parts from the Java editor.
+
+
+--
+EOF
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/AboutDialog.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/AboutDialog.java
new file mode 100755
index 0000000..6b3a258
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/AboutDialog.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository;
+
+
+import com.android.SdkConstants;
+import com.android.sdklib.io.FileOp;
+import com.android.sdklib.repository.PkgProps;
+import com.android.sdklib.repository.SdkAddonConstants;
+import com.android.sdklib.repository.SdkRepoConstants;
+import com.android.sdkuilib.internal.repository.icons.ImageFactory;
+import com.android.sdkuilib.ui.GridDataBuilder;
+import com.android.sdkuilib.ui.GridLayoutBuilder;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Properties;
+
+public class AboutDialog extends UpdaterBaseDialog {
+
+    public AboutDialog(Shell parentShell, SwtUpdaterData swtUpdaterData) {
+        super(parentShell, swtUpdaterData, "About" /*title*/);
+        assert swtUpdaterData != null;
+    }
+
+    @Override
+    protected void createContents() {
+        super.createContents();
+        Shell shell = getShell();
+        shell.setMinimumSize(new Point(450, 150));
+        shell.setSize(450, 150);
+
+        GridLayoutBuilder.create(shell).columns(3);
+
+        Label logo = new Label(shell, SWT.NONE);
+        ImageFactory imgf = getSwtUpdaterData() == null ? null
+                                                        : getSwtUpdaterData().getImageFactory();
+        Image image = imgf == null ? null : imgf.getImageByName("sdkman_logo_128.png");
+        if (image != null) logo.setImage(image);
+
+        Label label = new Label(shell, SWT.NONE);
+        GridDataBuilder.create(label).hFill().hGrab().hSpan(2);;
+        label.setText(String.format(
+                "Android SDK Manager.\n" +
+                "Revision %1$s\n" +
+                "Add-on XML Schema #%2$d\n" +
+                "Repository XML Schema #%3$d\n" +
+                // TODO: update with new year date (search this to find other occurrences to update)
+                "Copyright (C) 2009-2012 The Android Open Source Project.",
+                getRevision(),
+                SdkAddonConstants.NS_LATEST_VERSION,
+                SdkRepoConstants.NS_LATEST_VERSION));
+
+        Label filler = new Label(shell, SWT.NONE);
+        GridDataBuilder.create(filler).fill().grab().hSpan(2);
+
+        createCloseButton();
+    }
+
+    @Override
+    protected void checkSubclass() {
+        // Disable the check that prevents subclassing of SWT components
+    }
+
+    // -- Start of internal part ----------
+    // Hide everything down-below from SWT designer
+    //$hide>>$
+
+    // End of hiding from SWT Designer
+    //$hide<<$
+
+    private String getRevision() {
+        Properties p = new Properties();
+        try{
+            File sourceProp = FileOp.append(getSwtUpdaterData().getOsSdkRoot(),
+                    SdkConstants.FD_TOOLS,
+                    SdkConstants.FN_SOURCE_PROP);
+            FileInputStream fis = null;
+            try {
+                fis = new FileInputStream(sourceProp);
+                p.load(fis);
+            } finally {
+                if (fis != null) {
+                    try {
+                        fis.close();
+                    } catch (IOException ignore) {
+                    }
+                }
+            }
+
+            String revision = p.getProperty(PkgProps.PKG_REVISION);
+            if (revision != null) {
+                return revision;
+            }
+        } catch (IOException e) {
+        }
+
+        return "?";
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ISdkUpdaterWindow.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ISdkUpdaterWindow.java
new file mode 100755
index 0000000..ead5a78
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ISdkUpdaterWindow.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository;
+
+import com.android.sdklib.repository.ISdkChangeListener;
+
+/**
+ * Interface for the actual implementation of the Update Window.
+ */
+public interface ISdkUpdaterWindow {
+
+    /**
+     * Adds a new listener to be notified when a change is made to the content of the SDK.
+     */
+    public abstract void addListener(ISdkChangeListener listener);
+
+    /**
+     * Removes a new listener to be notified anymore when a change is made to the content of
+     * the SDK.
+     */
+    public abstract void removeListener(ISdkChangeListener listener);
+
+    /**
+     * Opens the window.
+     */
+    public abstract void open();
+
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ISwtUpdaterData.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ISwtUpdaterData.java
new file mode 100755
index 0000000..32e279a
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ISwtUpdaterData.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository;
+
+import com.android.sdklib.internal.repository.updater.IUpdaterData;
+import com.android.sdklib.internal.repository.updater.UpdaterData;
+import com.android.sdkuilib.internal.repository.icons.ImageFactory;
+
+import org.eclipse.swt.widgets.Shell;
+
+
+/**
+ * Interface used to retrieve some parameters from an {@link UpdaterData} instance.
+ * Useful mostly for unit tests purposes.
+ */
+interface ISwtUpdaterData extends IUpdaterData {
+
+    public abstract ImageFactory getImageFactory();
+
+    public abstract Shell getWindowShell();
+
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/MenuBarWrapper.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/MenuBarWrapper.java
new file mode 100755
index 0000000..8d3eabd
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/MenuBarWrapper.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository;
+
+
+import com.android.menubar.IMenuBarCallback;
+import com.android.menubar.MenuBarEnhancer;
+import com.android.sdkuilib.internal.repository.ui.SdkUpdaterWindowImpl2;
+
+import org.eclipse.swt.widgets.Menu;
+
+/**
+ * A simple wrapper/delegate around the {@link MenuBarEnhancer}.
+ *
+ * The {@link MenuBarEnhancer} and {@link IMenuBarCallback} classes are only
+ * available when the SwtMenuBar library is available too. This wrapper helps
+ * {@link SdkUpdaterWindowImpl2} make the call conditional, otherwise the updater
+ * window class would fail to load when the SwtMenuBar library isn't found.
+ */
+public abstract class MenuBarWrapper {
+
+    public MenuBarWrapper(String appName, Menu menu) {
+        MenuBarEnhancer.setupMenu(appName, menu, new IMenuBarCallback() {
+            @Override
+            public void onPreferencesMenuSelected() {
+                MenuBarWrapper.this.onPreferencesMenuSelected();
+            }
+
+            @Override
+            public void onAboutMenuSelected() {
+                MenuBarWrapper.this.onAboutMenuSelected();
+            }
+
+            @Override
+            public void printError(String format, Object... args) {
+                MenuBarWrapper.this.printError(format, args);
+            }
+        });
+    }
+
+    abstract public void onPreferencesMenuSelected();
+
+    abstract public void onAboutMenuSelected();
+
+    abstract public void printError(String format, Object... args);
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SdkUpdaterChooserDialog.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SdkUpdaterChooserDialog.java
new file mode 100755
index 0000000..dc2edb4
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SdkUpdaterChooserDialog.java
@@ -0,0 +1,1130 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.sdklib.AndroidVersion;
+import com.android.sdklib.internal.repository.archives.Archive;
+import com.android.sdklib.internal.repository.packages.IAndroidVersionProvider;
+import com.android.sdklib.internal.repository.packages.Package;
+import com.android.sdklib.internal.repository.packages.Package.License;
+import com.android.sdklib.internal.repository.sources.SdkSource;
+import com.android.sdklib.internal.repository.updater.ArchiveInfo;
+import com.android.sdklib.internal.repository.updater.SdkUpdaterLogic;
+import com.android.sdklib.repository.FullRevision;
+import com.android.sdkuilib.internal.repository.icons.ImageFactory;
+import com.android.sdkuilib.ui.GridDialog;
+
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.custom.StyleRange;
+import org.eclipse.swt.custom.StyledText;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Link;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeColumn;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TreeMap;
+
+
+/**
+ * Implements an {@link SdkUpdaterChooserDialog}.
+ */
+final class SdkUpdaterChooserDialog extends GridDialog {
+
+    /** Last dialog size for this session. */
+    private static Point sLastSize;
+    /** Precomputed flag indicating whether the "accept license" radio is checked. */
+    private boolean mAcceptSameAllLicense;
+    private boolean mInternalLicenseRadioUpdate;
+
+    // UI fields
+    private SashForm mSashForm;
+    private Composite mPackageRootComposite;
+    private TreeViewer mTreeViewPackage;
+    private Tree mTreePackage;
+    private TreeColumn mTreeColum;
+    private StyledText mPackageText;
+    private Button mLicenseRadioAccept;
+    private Button mLicenseRadioReject;
+    private Button mLicenseRadioAcceptLicense;
+    private Group mPackageTextGroup;
+    private final SwtUpdaterData mSwtUpdaterData;
+    private Group mTableGroup;
+    private Label mErrorLabel;
+
+    /**
+     * List of all archives to be installed with dependency information.
+     * <p/>
+     * Note: in a lot of cases, we need to find the archive info for a given archive. This
+     * is currently done using a simple linear search, which is fine since we only have a very
+     * limited number of archives to deal with (e.g. < 10 now). We might want to revisit
+     * this later if it becomes an issue. Right now just do the simple thing.
+     * <p/>
+     * Typically we could add a map Archive=>ArchiveInfo later.
+     */
+    private final Collection<ArchiveInfo> mArchives;
+
+
+
+    /**
+     * Create the dialog.
+     *
+     * @param parentShell The shell to use, typically updaterData.getWindowShell()
+     * @param swtUpdaterData The updater data
+     * @param archives The archives to be installed
+     */
+    public SdkUpdaterChooserDialog(Shell parentShell,
+            SwtUpdaterData swtUpdaterData,
+            Collection<ArchiveInfo> archives) {
+        super(parentShell, 3, false/*makeColumnsEqual*/);
+        mSwtUpdaterData = swtUpdaterData;
+        mArchives = archives;
+    }
+
+    @Override
+    protected boolean isResizable() {
+        return true;
+    }
+
+    /**
+     * Returns the results, i.e. the list of selected new archives to install.
+     * This is similar to the {@link ArchiveInfo} list instance given to the constructor
+     * except only accepted archives are present.
+     * <p/>
+     * An empty list is returned if cancel was chosen.
+     */
+    public ArrayList<ArchiveInfo> getResult() {
+        ArrayList<ArchiveInfo> ais = new ArrayList<ArchiveInfo>();
+
+        if (getReturnCode() == Window.OK) {
+            for (ArchiveInfo ai : mArchives) {
+                if (ai.isAccepted()) {
+                    ais.add(ai);
+                }
+            }
+        }
+
+        return ais;
+    }
+
+    /**
+     * Create the main content of the dialog.
+     * See also {@link #createButtonBar(Composite)} below.
+     */
+    @Override
+    public void createDialogContent(Composite parent) {
+        // Sash form
+        mSashForm = new SashForm(parent, SWT.NONE);
+        mSashForm.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 3, 1));
+
+
+        // Left part of Sash Form
+
+        mTableGroup = new Group(mSashForm, SWT.NONE);
+        mTableGroup.setText("Packages");
+        mTableGroup.setLayout(new GridLayout(1, false/*makeColumnsEqual*/));
+
+        mTreeViewPackage = new TreeViewer(mTableGroup, SWT.BORDER | SWT.V_SCROLL | SWT.SINGLE);
+        mTreePackage = mTreeViewPackage.getTree();
+        mTreePackage.setHeaderVisible(false);
+        mTreePackage.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
+
+        mTreePackage.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                onPackageSelected();  //$hide$
+            }
+            @Override
+            public void widgetDefaultSelected(SelectionEvent e) {
+                onPackageDoubleClick();
+            }
+        });
+
+        mTreeColum = new TreeColumn(mTreePackage, SWT.NONE);
+        mTreeColum.setWidth(100);
+        mTreeColum.setText("Packages");
+
+        // Right part of Sash form
+
+        mPackageRootComposite = new Composite(mSashForm, SWT.NONE);
+        mPackageRootComposite.setLayout(new GridLayout(4, false/*makeColumnsEqual*/));
+        mPackageRootComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+
+        mPackageTextGroup = new Group(mPackageRootComposite, SWT.NONE);
+        mPackageTextGroup.setText("Package Description && License");
+        mPackageTextGroup.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 4, 1));
+        mPackageTextGroup.setLayout(new GridLayout(1, false/*makeColumnsEqual*/));
+
+        mPackageText = new StyledText(mPackageTextGroup,
+                        SWT.MULTI | SWT.READ_ONLY | SWT.WRAP | SWT.V_SCROLL);
+        mPackageText.setBackground(
+                getParentShell().getDisplay().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND));
+        mPackageText.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
+
+        mLicenseRadioAccept = new Button(mPackageRootComposite, SWT.RADIO);
+        mLicenseRadioAccept.setText("Accept");
+        mLicenseRadioAccept.setToolTipText("Accept this package.");
+        mLicenseRadioAccept.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                onLicenseRadioSelected();
+            }
+        });
+
+        mLicenseRadioReject = new Button(mPackageRootComposite, SWT.RADIO);
+        mLicenseRadioReject.setText("Reject");
+        mLicenseRadioReject.setToolTipText("Reject this package.");
+        mLicenseRadioReject.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                onLicenseRadioSelected();
+            }
+        });
+
+        Link link = new Link(mPackageRootComposite, SWT.NONE);
+        link.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false, 1, 1));
+        final String printAction = "Print"; // extracted for NLS, to compare with below.
+        link.setText(String.format("<a>Copy to clipboard</a> | <a>%1$s</a>", printAction));
+        link.setToolTipText("Copies all text and license to clipboard | Print using system defaults.");
+        link.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                super.widgetSelected(e);
+                if (printAction.equals(e.text)) {
+                    mPackageText.print();
+                } else {
+                    Point p = mPackageText.getSelection();
+                    mPackageText.selectAll();
+                    mPackageText.copy();
+                    mPackageText.setSelection(p);
+                }
+            }
+        });
+
+
+        mLicenseRadioAcceptLicense = new Button(mPackageRootComposite, SWT.RADIO);
+        mLicenseRadioAcceptLicense.setText("Accept License");
+        mLicenseRadioAcceptLicense.setToolTipText("Accept all packages that use the same license.");
+        mLicenseRadioAcceptLicense.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                onLicenseRadioSelected();
+            }
+        });
+
+        mSashForm.setWeights(new int[] {200, 300});
+    }
+
+    /**
+     * Creates and returns the contents of this dialog's button bar.
+     * <p/>
+     * This reimplements most of the code from the base class with a few exceptions:
+     * <ul>
+     * <li>Enforces 3 columns.
+     * <li>Inserts a full-width error label.
+     * <li>Inserts a help label on the left of the first button.
+     * <li>Renames the OK button into "Install"
+     * </ul>
+     */
+    @Override
+    protected Control createButtonBar(Composite parent) {
+        Composite composite = new Composite(parent, SWT.NONE);
+        GridLayout layout = new GridLayout();
+        layout.numColumns = 0; // this is incremented by createButton
+        layout.makeColumnsEqualWidth = false;
+        layout.marginWidth = convertHorizontalDLUsToPixels(IDialogConstants.HORIZONTAL_MARGIN);
+        layout.marginHeight = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_MARGIN);
+        layout.horizontalSpacing = convertHorizontalDLUsToPixels(IDialogConstants.HORIZONTAL_SPACING);
+        layout.verticalSpacing = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_SPACING);
+        composite.setLayout(layout);
+        GridData data = new GridData(SWT.FILL, SWT.CENTER, true, false, 3, 1);
+        composite.setLayoutData(data);
+        composite.setFont(parent.getFont());
+
+        // Error message area
+        mErrorLabel = new Label(composite, SWT.NONE);
+        mErrorLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 3, 1));
+
+        // Label at the left of the install/cancel buttons
+        Label label = new Label(composite, SWT.NONE);
+        label.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+        label.setText("[*] Something depends on this package");
+        label.setEnabled(false);
+        layout.numColumns++;
+
+        // Add the ok/cancel to the button bar.
+        createButtonsForButtonBar(composite);
+
+        // the ok button should be an "install" button
+        Button button = getButton(IDialogConstants.OK_ID);
+        button.setText("Install");
+
+        return composite;
+    }
+
+    // -- End of UI, Start of internal logic ----------
+    // Hide everything down-below from SWT designer
+    //$hide>>$
+
+    @Override
+    public void create() {
+        super.create();
+
+        // set window title
+        getShell().setText("Choose Packages to Install");
+
+        setWindowImage();
+
+        // Automatically accept those with an empty license or no license
+        for (ArchiveInfo ai : mArchives) {
+            Archive a = ai.getNewArchive();
+            if (a != null) {
+                License license = a.getParentPackage().getLicense();
+                boolean hasLicense = license != null &&
+                                     license.getLicense() != null &&
+                                     license.getLicense().length() > 0;
+                ai.setAccepted(!hasLicense);
+            }
+        }
+
+        // Fill the list with the replacement packages
+        mTreeViewPackage.setLabelProvider(new NewArchivesLabelProvider());
+        mTreeViewPackage.setContentProvider(new NewArchivesContentProvider());
+        mTreeViewPackage.setInput(createTreeInput(mArchives));
+        mTreeViewPackage.expandAll();
+
+        adjustColumnsWidth();
+
+        // select first item
+        onPackageSelected();
+    }
+
+    /**
+     * Creates the icon of the window shell.
+     */
+    private void setWindowImage() {
+        String imageName = "android_icon_16.png"; //$NON-NLS-1$
+        if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_DARWIN) {
+            imageName = "android_icon_128.png";   //$NON-NLS-1$
+        }
+
+        if (mSwtUpdaterData != null) {
+            ImageFactory imgFactory = mSwtUpdaterData.getImageFactory();
+            if (imgFactory != null) {
+                getShell().setImage(imgFactory.getImageByName(imageName));
+            }
+        }
+    }
+
+    /**
+     * Adds a listener to adjust the columns width when the parent is resized.
+     * <p/>
+     * If we need something more fancy, we might want to use this:
+     * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet77.java?view=co
+     */
+    private void adjustColumnsWidth() {
+        // Add a listener to resize the column to the full width of the table
+        ControlAdapter resizer = new ControlAdapter() {
+            @Override
+            public void controlResized(ControlEvent e) {
+                Rectangle r = mTreePackage.getClientArea();
+                mTreeColum.setWidth(r.width);
+            }
+        };
+        mTreePackage.addControlListener(resizer);
+        resizer.controlResized(null);
+    }
+
+    /**
+     * Captures the window size before closing this.
+     * @see #getInitialSize()
+     */
+    @Override
+    public boolean close() {
+        sLastSize = getShell().getSize();
+        return super.close();
+    }
+
+    /**
+     * Tries to reuse the last window size during this session.
+     * <p/>
+     * Note: the alternative would be to implement {@link #getDialogBoundsSettings()}
+     * since the default {@link #getDialogBoundsStrategy()} is to persist both location
+     * and size.
+     */
+    @Override
+    protected Point getInitialSize() {
+        if (sLastSize != null) {
+            return sLastSize;
+        } else {
+            // Arbitrary values that look good on my screen and fit on 800x600
+            return new Point(740, 470);
+        }
+    }
+
+    /**
+     * Callback invoked when a package item is selected in the list.
+     */
+    private void onPackageSelected() {
+        Object item = getSelectedItem();
+
+        // Update mAcceptSameAllLicense : true if all items under the same license are accepted.
+        ArchiveInfo ai = null;
+        List<ArchiveInfo> list = null;
+        if (item instanceof ArchiveInfo) {
+            ai = (ArchiveInfo) item;
+
+            Object p =
+                ((NewArchivesContentProvider) mTreeViewPackage.getContentProvider()).getParent(ai);
+            if (p instanceof LicenseEntry) {
+                list = ((LicenseEntry) p).getArchives();
+            }
+            displayPackageInformation(ai);
+
+        } else if (item instanceof LicenseEntry) {
+            LicenseEntry entry = (LicenseEntry) item;
+            list = entry.getArchives();
+            displayLicenseInformation(entry);
+
+        } else {
+            // Fallback, should not happen.
+            displayEmptyInformation();
+        }
+
+        // the "Accept License" radio is selected if there's a license with >= 0 items
+        // and they are all in "accepted" state.
+        mAcceptSameAllLicense = list != null && list.size() > 0;
+        if (mAcceptSameAllLicense) {
+            assert list != null;
+            License lic0 = getLicense(list.get(0));
+            for (ArchiveInfo ai2 : list) {
+                License lic2 = getLicense(ai2);
+                if (ai2.isAccepted() && (lic0 == lic2 || lic0.equals(lic2))) {
+                    continue;
+                } else {
+                    mAcceptSameAllLicense = false;
+                    break;
+                }
+            }
+        }
+
+        displayMissingDependency(ai);
+        updateLicenceRadios(ai);
+    }
+
+    /** Returns the currently selected tree item.
+     * @return Either {@link ArchiveInfo} or {@link LicenseEntry} or null. */
+    private Object getSelectedItem() {
+        ISelection sel = mTreeViewPackage.getSelection();
+        if (sel instanceof IStructuredSelection) {
+            Object elem = ((IStructuredSelection) sel).getFirstElement();
+            if (elem instanceof ArchiveInfo || elem instanceof LicenseEntry) {
+                return elem;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Information displayed when nothing valid is selected.
+     */
+    private void displayEmptyInformation() {
+        mPackageText.setText("Please select a package or a license.");
+    }
+
+    /**
+     * Updates the package description and license text depending on the selected package.
+     * <p/>
+     * Note that right now there is no logic to support more than one level of dependencies
+     * (e.g. A <- B <- C and A is disabled so C should be disabled; currently C's state depends
+     * solely on B's state). We currently don't need this. It would be straightforward to add
+     * if we had a need for it, though. This would require changes to {@link ArchiveInfo} and
+     * {@link SdkUpdaterLogic}.
+     */
+    private void displayPackageInformation(ArchiveInfo ai) {
+        Archive aNew = ai   == null ? null : ai.getNewArchive();
+        Package pNew = aNew == null ? null : aNew.getParentPackage();
+
+        if (pNew == null) {
+            displayEmptyInformation();
+            return;
+        }
+        assert ai   != null;                        // make Eclipse null detector happy
+        assert aNew != null;
+
+        mPackageText.setText("");                   //$NON-NLS-1$
+
+        addSectionTitle("Package Description\n");
+        addText(pNew.getLongDescription(), "\n\n"); //$NON-NLS-1$
+
+        Archive aOld = ai.getReplaced();
+        if (aOld != null) {
+            Package pOld = aOld.getParentPackage();
+
+            FullRevision rOld = pOld.getRevision();
+            FullRevision rNew = pNew.getRevision();
+
+            boolean showRev = true;
+
+            if (pNew instanceof IAndroidVersionProvider &&
+                    pOld instanceof IAndroidVersionProvider) {
+                AndroidVersion vOld = ((IAndroidVersionProvider) pOld).getAndroidVersion();
+                AndroidVersion vNew = ((IAndroidVersionProvider) pNew).getAndroidVersion();
+
+                if (!vOld.equals(vNew)) {
+                    // Versions are different, so indicate more than just the revision.
+                    addText(String.format("This update will replace API %1$s revision %2$s with API %3$s revision %4$s.\n\n",
+                            vOld.getApiString(), rOld.toShortString(),
+                            vNew.getApiString(), rNew.toShortString()));
+                    showRev = false;
+                }
+            }
+
+            if (showRev) {
+                addText(String.format("This update will replace revision %1$s with revision %2$s.\n\n",
+                        rOld.toShortString(),
+                        rNew.toShortString()));
+            }
+        }
+
+        ArchiveInfo[] aDeps = ai.getDependsOn();
+        if ((aDeps != null && aDeps.length > 0) || ai.isDependencyFor()) {
+            addSectionTitle("Dependencies\n");
+
+            if (aDeps != null && aDeps.length > 0) {
+                addText("Installing this package also requires installing:");
+                for (ArchiveInfo aDep : aDeps) {
+                    addText(String.format("\n- %1$s",
+                            aDep.getShortDescription()));
+                }
+                addText("\n\n");
+            }
+
+            if (ai.isDependencyFor()) {
+                addText("This package is a dependency for:");
+                for (ArchiveInfo ai2 : ai.getDependenciesFor()) {
+                    addText(String.format("\n- %1$s",
+                            ai2.getShortDescription()));
+                }
+                addText("\n\n");
+            }
+        }
+
+        addSectionTitle("Archive Description\n");
+        addText(aNew.getLongDescription(), "\n\n");                             //$NON-NLS-1$
+
+        License license = pNew.getLicense();
+        if (license != null) {
+            String text = license.getLicense();
+            if (text != null) {
+                addSectionTitle("License\n");
+                addText(text.trim(), "\n\n");                                   //$NON-NLS-1$
+            }
+        }
+
+        addSectionTitle("Site\n");
+        SdkSource source = pNew.getParentSource();
+        if (source != null) {
+            addText(source.getShortDescription());
+        }
+    }
+
+    /**
+     * Updates the description for a license entry.
+     */
+    private void displayLicenseInformation(LicenseEntry entry) {
+        List<ArchiveInfo> archives = entry == null ? null : entry.getArchives();
+        if (archives == null) {
+            // There should not be a license entry without any package in it.
+            displayEmptyInformation();
+            return;
+        }
+        assert entry != null;
+
+        mPackageText.setText("");                   //$NON-NLS-1$
+
+        License license = null;
+        addSectionTitle("Packages\n");
+        for (ArchiveInfo ai : entry.getArchives()) {
+            Archive aNew = ai.getNewArchive();
+            if (aNew != null) {
+                Package pNew = aNew.getParentPackage();
+                if (pNew != null) {
+                    if (license == null) {
+                        license = pNew.getLicense();
+                    } else {
+                        assert license.equals(pNew.getLicense()); // all items have the same license
+                    }
+                    addText("- ", pNew.getShortDescription(), "\n"); //$NON-NLS-1$ //$NON-NLS-2$
+                }
+            }
+        }
+
+        if (license != null) {
+            String text = license.getLicense();
+            if (text != null) {
+                addSectionTitle("\nLicense\n");
+                addText(text.trim(), "\n\n");                                   //$NON-NLS-1$
+            }
+        }
+    }
+
+    /**
+     * Computes and displays missing dependencies.
+     *
+     * If there's a selected package, check the dependency for that one.
+     * Otherwise display the first missing dependency of any other package.
+     */
+    private void displayMissingDependency(ArchiveInfo ai) {
+        String error = null;
+
+        try {
+            if (ai != null) {
+                if (ai.isAccepted()) {
+                    // Case where this package is accepted but blocked by another non-accepted one
+                    ArchiveInfo[] adeps = ai.getDependsOn();
+                    if (adeps != null) {
+                        for (ArchiveInfo adep : adeps) {
+                            if (!adep.isAccepted()) {
+                                error = String.format("This package depends on '%1$s'.",
+                                        adep.getShortDescription());
+                                return;
+                            }
+                        }
+                    }
+                } else {
+                    // Case where this package blocks another one when not accepted
+                    for (ArchiveInfo adep : ai.getDependenciesFor()) {
+                        // It only matters if the blocked one is accepted
+                        if (adep.isAccepted()) {
+                            error = String.format("Package '%1$s' depends on this one.",
+                                    adep.getShortDescription());
+                            return;
+                        }
+                    }
+                }
+            }
+
+            // If there is no missing dependency on the current selection,
+            // just find the first missing dependency of any other package.
+            for (ArchiveInfo ai2 : mArchives) {
+                if (ai2 == ai) {
+                    // We already processed that one above.
+                    continue;
+                }
+                if (ai2.isAccepted()) {
+                    // The user requested to install this package.
+                    // Check if all its dependencies are met.
+                    ArchiveInfo[] adeps = ai2.getDependsOn();
+                    if (adeps != null) {
+                        for (ArchiveInfo adep : adeps) {
+                            if (!adep.isAccepted()) {
+                                error = String.format("Package '%1$s' depends on '%2$s'",
+                                        ai2.getShortDescription(),
+                                        adep.getShortDescription());
+                                return;
+                            }
+                        }
+                    }
+                } else {
+                    // The user did not request to install this package.
+                    // Check whether this package blocks another one when not accepted.
+                    for (ArchiveInfo adep : ai2.getDependenciesFor()) {
+                        // It only matters if the blocked one is accepted
+                        // or if it's a local archive that is already installed (these
+                        // are marked as implicitly accepted, so it's the same test.)
+                        if (adep.isAccepted()) {
+                            error = String.format("Package '%1$s' depends on '%2$s'",
+                                    adep.getShortDescription(),
+                                    ai2.getShortDescription());
+                            return;
+                        }
+                    }
+                }
+            }
+        } finally {
+            mErrorLabel.setText(error == null ? "" : error);        //$NON-NLS-1$
+        }
+    }
+
+    private void addText(String...string) {
+        for (String s : string) {
+            mPackageText.append(s);
+        }
+    }
+
+    private void addSectionTitle(String string) {
+        String s = mPackageText.getText();
+        int start = (s == null ? 0 : s.length());
+        mPackageText.append(string);
+
+        StyleRange sr = new StyleRange();
+        sr.start = start;
+        sr.length = string.length();
+        sr.fontStyle = SWT.BOLD;
+        sr.underline = true;
+        mPackageText.setStyleRange(sr);
+    }
+
+    private void updateLicenceRadios(ArchiveInfo ai) {
+        if (mInternalLicenseRadioUpdate) {
+            return;
+        }
+        mInternalLicenseRadioUpdate = true;
+
+        boolean oneAccepted = false;
+
+        mLicenseRadioAcceptLicense.setSelection(mAcceptSameAllLicense);
+        oneAccepted = ai != null && ai.isAccepted();
+        mLicenseRadioAccept.setEnabled(ai != null);
+        mLicenseRadioReject.setEnabled(ai != null);
+        mLicenseRadioAccept.setSelection(oneAccepted);
+        mLicenseRadioReject.setSelection(ai != null && ai.isRejected());
+
+        // The install button is enabled if there's at least one package accepted.
+        // If the current one isn't, look for another one.
+        boolean missing = mErrorLabel.getText() != null && mErrorLabel.getText().length() > 0;
+        if (!missing && !oneAccepted) {
+            for(ArchiveInfo ai2 : mArchives) {
+                if (ai2.isAccepted()) {
+                    oneAccepted = true;
+                    break;
+                }
+            }
+        }
+
+        getButton(IDialogConstants.OK_ID).setEnabled(!missing && oneAccepted);
+
+        mInternalLicenseRadioUpdate = false;
+    }
+
+    /**
+     * Callback invoked when one of the radio license buttons is selected.
+     *
+     * - accept/refuse: toggle, update item checkbox
+     * - accept all: set accept-all, check all items with the *same* license
+     */
+    private void onLicenseRadioSelected() {
+        if (mInternalLicenseRadioUpdate) {
+            return;
+        }
+        mInternalLicenseRadioUpdate = true;
+
+        Object item = getSelectedItem();
+        ArchiveInfo ai = (item instanceof ArchiveInfo) ? (ArchiveInfo) item : null;
+        boolean needUpdate = true;
+
+        if (!mAcceptSameAllLicense && mLicenseRadioAcceptLicense.getSelection()) {
+            // Accept all has been switched on. Mark all packages as accepted
+
+            List<ArchiveInfo> list = null;
+            if (item instanceof LicenseEntry) {
+                list = ((LicenseEntry) item).getArchives();
+            } else if (ai != null) {
+                Object p = ((NewArchivesContentProvider) mTreeViewPackage.getContentProvider())
+                                                                         .getParent(ai);
+                if (p instanceof LicenseEntry) {
+                    list = ((LicenseEntry) p).getArchives();
+                }
+            }
+
+            if (list != null && list.size() > 0) {
+                mAcceptSameAllLicense = true;
+                for(ArchiveInfo ai2 : list) {
+                    ai2.setAccepted(true);
+                    ai2.setRejected(false);
+                }
+            }
+
+        } else if (ai != null && mLicenseRadioAccept.getSelection()) {
+            // Accept only this one
+            mAcceptSameAllLicense = false;
+            ai.setAccepted(true);
+            ai.setRejected(false);
+
+        } else if (ai != null && mLicenseRadioReject.getSelection()) {
+            // Reject only this one
+            mAcceptSameAllLicense = false;
+            ai.setAccepted(false);
+            ai.setRejected(true);
+
+        } else {
+            needUpdate = false;
+        }
+
+        mInternalLicenseRadioUpdate = false;
+
+        if (needUpdate) {
+            if (mAcceptSameAllLicense) {
+                mTreeViewPackage.refresh();
+            } else {
+               mTreeViewPackage.refresh(ai);
+               mTreeViewPackage.refresh(
+                       ((NewArchivesContentProvider) mTreeViewPackage.getContentProvider()).
+                       getParent(ai));
+            }
+            displayMissingDependency(ai);
+            updateLicenceRadios(ai);
+        }
+    }
+
+    /**
+     * Callback invoked when a package item is double-clicked in the list.
+     */
+    private void onPackageDoubleClick() {
+        Object item = getSelectedItem();
+
+        if (item instanceof ArchiveInfo) {
+            ArchiveInfo ai = (ArchiveInfo) item;
+            boolean wasAccepted = ai.isAccepted();
+            ai.setAccepted(!wasAccepted);
+            ai.setRejected(wasAccepted);
+
+            // update state
+            mAcceptSameAllLicense = false;
+            mTreeViewPackage.refresh(ai);
+            // refresh parent since its icon might have changed.
+            mTreeViewPackage.refresh(
+                    ((NewArchivesContentProvider) mTreeViewPackage.getContentProvider()).
+                    getParent(ai));
+
+            displayMissingDependency(ai);
+            updateLicenceRadios(ai);
+
+        } else if (item instanceof LicenseEntry) {
+            mTreeViewPackage.setExpandedState(item, !mTreeViewPackage.getExpandedState(item));
+        }
+    }
+
+    /**
+     * Provides the labels for the tree view.
+     * Root branches are {@link LicenseEntry} elements.
+     * Leave nodes are {@link ArchiveInfo} which all have the same license.
+     */
+    private class NewArchivesLabelProvider extends LabelProvider {
+        @Override
+        public Image getImage(Object element) {
+            if (element instanceof ArchiveInfo) {
+                // Archive icon: accepted (green), rejected (red), not set yet (question mark)
+                ArchiveInfo ai = (ArchiveInfo) element;
+
+                ImageFactory imgFactory = mSwtUpdaterData.getImageFactory();
+                if (imgFactory != null) {
+                    if (ai.isAccepted()) {
+                        return imgFactory.getImageByName("accept_icon16.png");
+                    } else if (ai.isRejected()) {
+                        return imgFactory.getImageByName("reject_icon16.png");
+                    }
+                    return imgFactory.getImageByName("unknown_icon16.png");
+                }
+                return super.getImage(element);
+
+            } else if (element instanceof LicenseEntry) {
+                // License icon: green if all below are accepted, red if all rejected, otherwise
+                // no icon.
+                ImageFactory imgFactory = mSwtUpdaterData.getImageFactory();
+                if (imgFactory != null) {
+                    boolean allAccepted = true;
+                    boolean allRejected = true;
+                    for (ArchiveInfo ai : ((LicenseEntry) element).getArchives()) {
+                        allAccepted = allAccepted && ai.isAccepted();
+                        allRejected = allRejected && ai.isRejected();
+                    }
+                    if (allAccepted && !allRejected) {
+                        return imgFactory.getImageByName("accept_icon16.png");
+                    } else if (!allAccepted && allRejected) {
+                        return imgFactory.getImageByName("reject_icon16.png");
+                    }
+                }
+            }
+
+            return null;
+        }
+
+        @Override
+        public String getText(Object element) {
+            if (element instanceof LicenseEntry) {
+                return ((LicenseEntry) element).getLicenseRef();
+
+            } else if (element instanceof ArchiveInfo) {
+                ArchiveInfo ai = (ArchiveInfo) element;
+
+                String desc = ai.getShortDescription();
+
+                if (ai.isDependencyFor()) {
+                    desc += " [*]";
+                }
+
+                return desc;
+
+            }
+
+            assert element instanceof String || element instanceof ArchiveInfo;
+            return null;
+        }
+    }
+
+    /**
+     * Provides the content for the tree view.
+     * Root branches are {@link LicenseEntry} elements.
+     * Leave nodes are {@link ArchiveInfo} which all have the same license.
+     */
+    private class NewArchivesContentProvider implements ITreeContentProvider {
+        private List<LicenseEntry> mInput;
+
+        @Override
+        public void dispose() {
+            // pass
+        }
+
+        @SuppressWarnings("unchecked")
+        @Override
+        public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+            // Input should be the result from createTreeInput.
+            if (newInput instanceof List<?> &&
+                    ((List<?>) newInput).size() > 0 &&
+                    ((List<?>) newInput).get(0) instanceof LicenseEntry) {
+                mInput = (List<LicenseEntry>) newInput;
+            } else {
+                mInput = null;
+            }
+        }
+
+        @Override
+        public boolean hasChildren(Object parent) {
+            if (parent instanceof List<?>) {
+                // This is the root of the tree.
+                return true;
+
+            } else if (parent instanceof LicenseEntry) {
+                return ((LicenseEntry) parent).getArchives().size() > 0;
+            }
+
+            return false;
+        }
+
+        @Override
+        public Object[] getElements(Object parent) {
+            return getChildren(parent);
+        }
+
+        @Override
+        public Object[] getChildren(Object parent) {
+            if (parent instanceof List<?>) {
+                return ((List<?>) parent).toArray();
+
+            } else if (parent instanceof LicenseEntry) {
+                return ((LicenseEntry) parent).getArchives().toArray();
+            }
+
+            return new Object[0];
+        }
+
+        @Override
+        public Object getParent(Object child) {
+            if (child instanceof LicenseEntry) {
+                return ((LicenseEntry) child).getRoot();
+
+            } else if (child instanceof ArchiveInfo && mInput != null) {
+                for (LicenseEntry entry : mInput) {
+                    if (entry.getArchives().contains(child)) {
+                        return entry;
+                    }
+                }
+            }
+
+            return null;
+        }
+    }
+
+    /**
+     * Represents a branch in the view tree: an entry where all the sub-archive info
+     * share the same license. Contains a link to the share root list for convenience.
+     */
+    private static class LicenseEntry {
+        private final List<LicenseEntry> mRoot;
+        private final String mLicenseRef;
+        private final List<ArchiveInfo> mArchives;
+
+        public LicenseEntry(
+                @NonNull List<LicenseEntry> root,
+                @NonNull String licenseRef,
+                @NonNull List<ArchiveInfo> archives) {
+            mRoot = root;
+            mLicenseRef = licenseRef;
+            mArchives = archives;
+        }
+
+        @NonNull
+        public List<LicenseEntry> getRoot() {
+            return mRoot;
+        }
+
+        @NonNull
+        public String getLicenseRef() {
+            return mLicenseRef;
+        }
+
+        @NonNull
+        public List<ArchiveInfo> getArchives() {
+            return mArchives;
+        }
+    }
+
+    /**
+     * Creates the tree structure based on the given archives.
+     * The current structure is to have a branch per license type,
+     * with all the archives sharing the same license under it.
+     * Elements with no license are left at the root.
+     *
+     * @param archives The non-null collection of archive info to display. Ideally non-empty.
+     * @return A list of {@link LicenseEntry}, each containing a list of {@link ArchiveInfo}.
+     */
+    @NonNull
+    private List<LicenseEntry> createTreeInput(@NonNull Collection<ArchiveInfo> archives) {
+        // Build an ordered map with all the licenses, ordered by license ref name.
+        final String noLicense = "No license";      //NLS
+
+        Comparator<String> comp = new Comparator<String>() {
+            @Override
+            public int compare(String s1, String s2) {
+                boolean first1 = noLicense.equals(s1);
+                boolean first2 = noLicense.equals(s2);
+                if (first1 && first2) {
+                    return 0;
+                } else if (first1) {
+                    return -1;
+                } else if (first2) {
+                    return 1;
+                }
+                return s1.compareTo(s2);
+            }
+        };
+
+        Map<String, List<ArchiveInfo>> map = new TreeMap<String, List<ArchiveInfo>>(comp);
+
+        for (ArchiveInfo info : archives) {
+            String ref = noLicense;
+            License license = getLicense(info);
+            if (license != null && license.getLicenseRef() != null) {
+                ref = prettyLicenseRef(license.getLicenseRef());
+            }
+
+            List<ArchiveInfo> list = map.get(ref);
+            if (list == null) {
+                map.put(ref, list = new ArrayList<ArchiveInfo>());
+            }
+            list.add(info);
+        }
+
+        // Transform result into a list
+        List<LicenseEntry> licensesList = new ArrayList<LicenseEntry>();
+        for (Map.Entry<String, List<ArchiveInfo>> entry : map.entrySet()) {
+            licensesList.add(new LicenseEntry(licensesList, entry.getKey(), entry.getValue()));
+        }
+
+        return licensesList;
+    }
+
+    /**
+     * Helper method to retrieve the {@link License} for a given {@link ArchiveInfo}.
+     *
+     * @param ai The archive info. Can be null.
+     * @return The license for the package owning the archive. Can be null.
+     */
+    @Nullable
+    private License getLicense(@Nullable ArchiveInfo ai) {
+        if (ai != null) {
+            Archive aNew = ai.getNewArchive();
+            if (aNew != null) {
+                Package pNew = aNew.getParentPackage();
+                if (pNew != null) {
+                    return pNew.getLicense();
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Reformats the licenseRef to be more human-readable.
+     * It's an XML ref and in practice it looks like [oem-]android-[type]-license.
+     * If it's not a format we can deal with, leave it alone.
+     */
+    private String prettyLicenseRef(String ref) {
+        // capitalize every word
+        StringBuilder sb = new StringBuilder();
+        boolean capitalize = true;
+        for (char c : ref.toCharArray()) {
+            if (c >= 'a' && c <= 'z') {
+                if (capitalize) {
+                    c = (char) (c + 'A' - 'a');
+                    capitalize = false;
+                }
+            } else {
+                if (c == '-') {
+                    c = ' ';
+                }
+                capitalize = true;
+            }
+            sb.append(c);
+        }
+
+        ref = sb.toString();
+
+        // A few acronyms should stay upper-case
+        for (String w : new String[] { "Sdk", "Mips", "Arm" }) {
+            ref = ref.replaceAll(w, w.toUpperCase(Locale.US));
+        }
+
+        return ref;
+    }
+
+    // End of hiding from SWT Designer
+    //$hide<<$
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SettingsDialog.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SettingsDialog.java
new file mode 100755
index 0000000..af1ada7
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SettingsDialog.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository;
+
+import com.android.sdklib.internal.repository.DownloadCache;
+import com.android.sdklib.internal.repository.DownloadCache.Strategy;
+import com.android.sdklib.internal.repository.updater.ISettingsPage;
+import com.android.sdklib.internal.repository.updater.SettingsController;
+import com.android.sdklib.util.FormatUtils;
+import com.android.sdkuilib.ui.GridDataBuilder;
+import com.android.sdkuilib.ui.GridLayoutBuilder;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.Properties;
+
+
+public class SettingsDialog extends UpdaterBaseDialog implements ISettingsPage {
+
+
+    // data members
+    private final DownloadCache mDownloadCache = new DownloadCache(Strategy.SERVE_CACHE);
+    private final SettingsController mSettingsController;
+    private SettingsChangedCallback mSettingsChangedCallback;
+
+    // UI widgets
+    private Text mTextProxyServer;
+    private Text mTextProxyPort;
+    private Text mTextCacheSize;
+    private Button mCheckUseCache;
+    private Button mCheckForceHttp;
+    private Button mCheckAskAdbRestart;
+    private Button mCheckEnablePreviews;
+
+    private SelectionAdapter mApplyOnSelected = new SelectionAdapter() {
+        @Override
+        public void widgetSelected(SelectionEvent e) {
+            applyNewSettings(); //$hide$
+        }
+    };
+
+    private ModifyListener mApplyOnModified = new ModifyListener() {
+        @Override
+        public void modifyText(ModifyEvent e) {
+            applyNewSettings(); //$hide$
+        }
+    };
+
+    public SettingsDialog(Shell parentShell, SwtUpdaterData swtUpdaterData) {
+        super(parentShell, swtUpdaterData, "Settings" /*title*/);
+        assert swtUpdaterData != null;
+        mSettingsController = swtUpdaterData.getSettingsController();
+    }
+
+    @Override
+    protected void createShell() {
+        super.createShell();
+        Shell shell = getShell();
+        shell.setMinimumSize(new Point(450, 370));
+        shell.setSize(450, 400);
+    }
+
+    @Override
+    protected void createContents() {
+        super.createContents();
+        Shell shell = getShell();
+
+        Group group = new Group(shell, SWT.NONE);
+        group.setText("Proxy Settings");
+        GridDataBuilder.create(group).fill().grab().hSpan(2);
+        GridLayoutBuilder.create(group).columns(2);
+
+        Label label = new Label(group, SWT.NONE);
+        GridDataBuilder.create(label).hRight().vCenter();
+        label.setText("HTTP Proxy Server");
+        String tooltip = "The hostname or IP of the HTTP & HTTPS proxy server to use (e.g. proxy.example.com).\n" +
+                         "When empty, the default Java proxy setting is used.";
+        label.setToolTipText(tooltip);
+
+        mTextProxyServer = new Text(group, SWT.BORDER);
+        GridDataBuilder.create(mTextProxyServer).hFill().hGrab().vCenter();
+        mTextProxyServer.addModifyListener(mApplyOnModified);
+        mTextProxyServer.setToolTipText(tooltip);
+
+        label = new Label(group, SWT.NONE);
+        GridDataBuilder.create(label).hRight().vCenter();
+        label.setText("HTTP Proxy Port");
+        tooltip = "The port of the HTTP & HTTPS proxy server to use (e.g. 3128).\n" +
+                  "When empty, the default Java proxy setting is used.";
+        label.setToolTipText(tooltip);
+
+        mTextProxyPort = new Text(group, SWT.BORDER);
+        GridDataBuilder.create(mTextProxyPort).hFill().hGrab().vCenter();
+        mTextProxyPort.addModifyListener(mApplyOnModified);
+        mTextProxyPort.setToolTipText(tooltip);
+
+        // ----
+        group = new Group(shell, SWT.NONE);
+        group.setText("Manifest Cache");
+        GridDataBuilder.create(group).fill().grab().hSpan(2);
+        GridLayoutBuilder.create(group).columns(3);
+
+        label = new Label(group, SWT.NONE);
+        GridDataBuilder.create(label).hRight().vCenter();
+        label.setText("Directory:");
+
+        Text text = new Text(group, SWT.NONE);
+        GridDataBuilder.create(text).hFill().hGrab().vCenter().hSpan(2);
+        text.setEnabled(false);
+        text.setText(mDownloadCache.getCacheRoot().getAbsolutePath());
+
+        label = new Label(group, SWT.NONE);
+        GridDataBuilder.create(label).hRight().vCenter();
+        label.setText("Current Size:");
+
+        mTextCacheSize = new Text(group, SWT.NONE);
+        GridDataBuilder.create(mTextCacheSize).hFill().hGrab().vCenter().hSpan(2);
+        mTextCacheSize.setEnabled(false);
+        updateDownloadCacheSize();
+
+        mCheckUseCache = new Button(group, SWT.CHECK);
+        GridDataBuilder.create(mCheckUseCache).vCenter().hSpan(1);
+        mCheckUseCache.setText("Use download cache");
+        mCheckUseCache.setToolTipText("When checked, small manifest files are cached locally.\n" +
+                                      "Large binary files are never cached locally.");
+        mCheckUseCache.addSelectionListener(mApplyOnSelected);
+
+        label = new Label(group, SWT.NONE);
+        GridDataBuilder.create(label).hFill().hGrab().hSpan(1);
+
+        Button button = new Button(group, SWT.PUSH);
+        GridDataBuilder.create(button).vCenter().hSpan(1);
+        button.setText("Clear Cache");
+        button.setToolTipText("Deletes all cached files.");
+        button.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                mDownloadCache.clearCache();
+                updateDownloadCacheSize();
+            }
+        });
+
+        // ----
+        group = new Group(shell, SWT.NONE);
+        group.setText("Others");
+        GridDataBuilder.create(group).fill().grab().hSpan(2);
+        GridLayoutBuilder.create(group).columns(2);
+
+        mCheckForceHttp = new Button(group, SWT.CHECK);
+        GridDataBuilder.create(mCheckForceHttp).hFill().hGrab().vCenter().hSpan(2);
+        mCheckForceHttp.setText("Force https://... sources to be fetched using http://...");
+        mCheckForceHttp.setToolTipText(
+            "If you are not able to connect to the official Android repository using HTTPS,\n" +
+            "enable this setting to force accessing it via HTTP.");
+        mCheckForceHttp.addSelectionListener(mApplyOnSelected);
+
+        mCheckAskAdbRestart = new Button(group, SWT.CHECK);
+        GridDataBuilder.create(mCheckAskAdbRestart).hFill().hGrab().vCenter().hSpan(2);
+        mCheckAskAdbRestart.setText("Ask before restarting ADB");
+        mCheckAskAdbRestart.setToolTipText(
+                "When checked, the user will be asked for permission to restart ADB\n" +
+                "after updating an addon-on package or a tool package.");
+        mCheckAskAdbRestart.addSelectionListener(mApplyOnSelected);
+
+        mCheckEnablePreviews = new Button(group, SWT.CHECK);
+        GridDataBuilder.create(mCheckEnablePreviews).hFill().hGrab().vCenter().hSpan(2);
+        mCheckEnablePreviews.setText("Enable Preview Tools");
+        mCheckEnablePreviews.setToolTipText(
+            "When checked, the package list will also display preview versions of the tools.\n" +
+            "These are optional future release candidates that the Android tools team\n" +
+            "publishes from time to time for early feedback.");
+        mCheckEnablePreviews.addSelectionListener(mApplyOnSelected);
+
+        Label filler = new Label(shell, SWT.NONE);
+        GridDataBuilder.create(filler).hFill().hGrab();
+
+        createCloseButton();
+    }
+
+    @Override
+    protected void postCreate() {
+        super.postCreate();
+        // This tells the controller to load the settings into the page UI.
+        mSettingsController.setSettingsPage(this);
+    }
+
+    @Override
+    protected void close() {
+        // Dissociate this page from the controller
+        mSettingsController.setSettingsPage(null);
+        super.close();
+    }
+
+
+    // -- Start of internal part ----------
+    // Hide everything down-below from SWT designer
+    //$hide>>$
+
+    /** Loads settings from the given {@link Properties} container and update the page UI. */
+    @Override
+    public void loadSettings(Properties inSettings) {
+        mTextProxyServer.setText(inSettings.getProperty(KEY_HTTP_PROXY_HOST, ""));  //$NON-NLS-1$
+        mTextProxyPort.setText(  inSettings.getProperty(KEY_HTTP_PROXY_PORT, ""));  //$NON-NLS-1$
+        mCheckForceHttp.setSelection(
+                Boolean.parseBoolean(inSettings.getProperty(KEY_FORCE_HTTP)));
+        mCheckAskAdbRestart.setSelection(
+                Boolean.parseBoolean(inSettings.getProperty(KEY_ASK_ADB_RESTART)));
+        mCheckUseCache.setSelection(
+                Boolean.parseBoolean(inSettings.getProperty(KEY_USE_DOWNLOAD_CACHE)));
+        mCheckEnablePreviews.setSelection(
+                Boolean.parseBoolean(inSettings.getProperty(KEY_ENABLE_PREVIEWS)));
+
+    }
+
+    /** Called by the application to retrieve settings from the UI and store them in
+     * the given {@link Properties} container. */
+    @Override
+    public void retrieveSettings(Properties outSettings) {
+        outSettings.setProperty(KEY_HTTP_PROXY_HOST, mTextProxyServer.getText());
+        outSettings.setProperty(KEY_HTTP_PROXY_PORT, mTextProxyPort.getText());
+        outSettings.setProperty(KEY_FORCE_HTTP,
+                Boolean.toString(mCheckForceHttp.getSelection()));
+        outSettings.setProperty(KEY_ASK_ADB_RESTART,
+                Boolean.toString(mCheckAskAdbRestart.getSelection()));
+        outSettings.setProperty(KEY_USE_DOWNLOAD_CACHE,
+                Boolean.toString(mCheckUseCache.getSelection()));
+        outSettings.setProperty(KEY_ENABLE_PREVIEWS,
+                Boolean.toString(mCheckEnablePreviews.getSelection()));
+
+    }
+
+    /**
+     * Called by the application to give a callback that the page should invoke when
+     * settings must be applied. The page does not apply the settings itself, instead
+     * it notifies the application.
+     */
+    @Override
+    public void setOnSettingsChanged(SettingsChangedCallback settingsChangedCallback) {
+        mSettingsChangedCallback = settingsChangedCallback;
+    }
+
+    /**
+     * Callback invoked when user touches one of the settings.
+     * There is no "Apply" button, settings are applied immediately as they are changed.
+     * Notify the application that settings have changed.
+     */
+    private void applyNewSettings() {
+        if (mSettingsChangedCallback != null) {
+            mSettingsChangedCallback.onSettingsChanged(this);
+        }
+    }
+
+    private void updateDownloadCacheSize() {
+        long size = mDownloadCache.getCurrentSize();
+        String str = FormatUtils.byteSizeToString(size);
+        mTextCacheSize.setText(str);
+    }
+
+
+    // End of hiding from SWT Designer
+    //$hide<<$
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SwtUpdaterData.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SwtUpdaterData.java
new file mode 100755
index 0000000..8934ae7
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SwtUpdaterData.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository;
+
+import com.android.annotations.NonNull;
+import com.android.sdklib.internal.repository.AdbWrapper;
+import com.android.sdklib.internal.repository.ITaskMonitor;
+import com.android.sdklib.internal.repository.NullTaskMonitor;
+import com.android.sdklib.internal.repository.archives.Archive;
+import com.android.sdklib.internal.repository.updater.ArchiveInfo;
+import com.android.sdklib.internal.repository.updater.SdkUpdaterLogic;
+import com.android.sdklib.internal.repository.updater.UpdaterData;
+import com.android.sdkuilib.internal.repository.icons.ImageFactory;
+import com.android.sdkuilib.internal.repository.ui.SdkUpdaterWindowImpl2;
+import com.android.utils.ILogger;
+
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Data shared between {@link SdkUpdaterWindowImpl2} and its pages.
+ */
+public class SwtUpdaterData extends UpdaterData {
+
+    private Shell mWindowShell;
+
+    /**
+     * The current {@link ImageFactory}.
+     * Set via {@link #setImageFactory(ImageFactory)} by the window implementation.
+     * It is null when invoked using the command-line interface.
+     */
+    private ImageFactory mImageFactory;
+
+    /**
+     * Creates a new updater data.
+     *
+     * @param sdkLog Logger. Cannot be null.
+     * @param osSdkRoot The OS path to the SDK root.
+     */
+    public SwtUpdaterData(String osSdkRoot, ILogger sdkLog) {
+        super(osSdkRoot, sdkLog);
+    }
+
+    // ----- getters, setters ----
+
+    public void setImageFactory(ImageFactory imageFactory) {
+        mImageFactory = imageFactory;
+    }
+
+    public ImageFactory getImageFactory() {
+        return mImageFactory;
+    }
+
+    public void setWindowShell(Shell windowShell) {
+        mWindowShell = windowShell;
+    }
+
+    public Shell getWindowShell() {
+        return mWindowShell;
+    }
+
+    @Override
+    protected void displayInitError(String error) {
+        // We may not have any UI. Only display a dialog if there's a window shell available.
+        if (mWindowShell != null && !mWindowShell.isDisposed()) {
+            MessageDialog.openError(mWindowShell,
+                "Android Virtual Devices Manager",
+                error);
+        } else {
+            super.displayInitError(error);
+        }
+    }
+
+    // -----
+
+    /**
+     * Runs the runnable on the UI thread using {@link Display#syncExec(Runnable)}.
+     *
+     * @param r Non-null runnable.
+     */
+    @Override
+    protected void runOnUiThread(@NonNull Runnable r) {
+        if (mWindowShell != null && !mWindowShell.isDisposed()) {
+            mWindowShell.getDisplay().syncExec(r);
+        }
+    }
+
+    /**
+     * Attempts to restart ADB.
+     * <p/>
+     * If the "ask before restart" setting is set (the default), prompt the user whether
+     * now is a good time to restart ADB.
+     */
+    @Override
+    protected void askForAdbRestart(ITaskMonitor monitor) {
+        final boolean[] canRestart = new boolean[] { true };
+
+        if (getWindowShell() != null &&
+                getSettingsController().getSettings().getAskBeforeAdbRestart()) {
+            // need to ask for permission first
+            final Shell shell = getWindowShell();
+            if (shell != null && !shell.isDisposed()) {
+                shell.getDisplay().syncExec(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (!shell.isDisposed()) {
+                            canRestart[0] = MessageDialog.openQuestion(shell,
+                                    "ADB Restart",
+                                    "A package that depends on ADB has been updated. \n" +
+                                    "Do you want to restart ADB now?");
+                        }
+                    }
+                });
+            }
+        }
+
+        if (canRestart[0]) {
+            AdbWrapper adb = new AdbWrapper(getOsSdkRoot(), monitor);
+            adb.stopAdb();
+            adb.startAdb();
+        }
+    }
+
+    @Override
+    protected void notifyToolsNeedsToBeRestarted(int flags) {
+        String msg = null;
+        if ((flags & TOOLS_MSG_UPDATED_FROM_ADT) != 0) {
+            msg =
+            "The Android SDK and AVD Manager that you are currently using has been updated. " +
+            "Please also run Eclipse > Help > Check for Updates to see if the Android " +
+            "plug-in needs to be updated.";
+
+        } else if ((flags & TOOLS_MSG_UPDATED_FROM_SDKMAN) != 0) {
+            msg =
+            "The Android SDK and AVD Manager that you are currently using has been updated. " +
+            "It is recommended that you now close the manager window and re-open it. " +
+            "If you use Eclipse, please run Help > Check for Updates to see if the Android " +
+            "plug-in needs to be updated.";
+        }
+
+        final String msg2 = msg;
+
+        final Shell shell = getWindowShell();
+        if (msg2 != null && shell != null && !shell.isDisposed()) {
+            shell.getDisplay().syncExec(new Runnable() {
+                @Override
+                public void run() {
+                    if (!shell.isDisposed()) {
+                        MessageDialog.openInformation(shell,
+                                "Android Tools Updated",
+                                msg2);
+                    }
+                }
+            });
+        }
+    }
+
+    /**
+     * Tries to update all the *existing* local packages.
+     * This version *requires* to be run with a GUI.
+     * <p/>
+     * There are two modes of operation:
+     * <ul>
+     * <li>If selectedArchives is null, refreshes all sources, compares the available remote
+     * packages with the current local ones and suggest updates to be done to the user (including
+     * new platforms that the users doesn't have yet).
+     * <li>If selectedArchives is not null, this represents a list of archives/packages that
+     * the user wants to install or update, so just process these.
+     * </ul>
+     *
+     * @param selectedArchives The list of remote archives to consider for the update.
+     *  This can be null, in which case a list of remote archive is fetched from all
+     *  available sources.
+     * @param includeObsoletes True if obsolete packages should be used when resolving what
+     *  to update.
+     * @param flags Optional flags for the installer, such as {@link #NO_TOOLS_MSG}.
+     * @return A list of archives that have been installed. Can be null if nothing was done.
+     */
+    @Override
+    public List<Archive> updateOrInstallAll_WithGUI(
+            Collection<Archive> selectedArchives,
+            boolean includeObsoletes,
+            int flags) {
+
+        // Note: we no longer call refreshSources(true) here. This will be done
+        // automatically by computeUpdates() iif it needs to access sources to
+        // resolve missing dependencies.
+
+        SdkUpdaterLogic ul = new SdkUpdaterLogic(this);
+        List<ArchiveInfo> archives = ul.computeUpdates(
+                selectedArchives,
+                getSources(),
+                getLocalSdkParser().getPackages(),
+                includeObsoletes);
+
+        if (selectedArchives == null) {
+            getPackageLoader().loadRemoteAddonsList(new NullTaskMonitor(getSdkLog()));
+            ul.addNewPlatforms(
+                    archives,
+                    getSources(),
+                    getLocalSdkParser().getPackages(),
+                    includeObsoletes);
+        }
+
+        // TODO if selectedArchives is null and archives.len==0, find if there are
+        // any new platform we can suggest to install instead.
+
+        Collections.sort(archives);
+
+        SdkUpdaterChooserDialog dialog =
+            new SdkUpdaterChooserDialog(getWindowShell(), this, archives);
+        dialog.open();
+
+        ArrayList<ArchiveInfo> result = dialog.getResult();
+        if (result != null && result.size() > 0) {
+            return installArchives(result, flags);
+        }
+        return null;
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/UpdaterBaseDialog.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/UpdaterBaseDialog.java
new file mode 100755
index 0000000..8955667
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/UpdaterBaseDialog.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository;
+
+import com.android.SdkConstants;
+import com.android.sdkuilib.internal.repository.icons.ImageFactory;
+import com.android.sdkuilib.internal.repository.ui.SdkUpdaterWindowImpl2;
+import com.android.sdkuilib.ui.GridDataBuilder;
+import com.android.sdkuilib.ui.GridLayoutBuilder;
+import com.android.sdkuilib.ui.SwtBaseDialog;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Shell;
+
+
+
+/**
+ * Base class for auxiliary dialogs shown in the updater (for example settings,
+ * about box or add-on site.)
+ */
+public abstract class UpdaterBaseDialog extends SwtBaseDialog {
+
+    private final SwtUpdaterData mSwtUpdaterData;
+
+    protected UpdaterBaseDialog(Shell parentShell, SwtUpdaterData swtUpdaterData, String title) {
+        super(parentShell,
+              SWT.APPLICATION_MODAL,
+              String.format("%1$s - %2$s", SdkUpdaterWindowImpl2.APP_NAME, title)); //$NON-NLS-1$
+        mSwtUpdaterData = swtUpdaterData;
+    }
+
+    public SwtUpdaterData getSwtUpdaterData() {
+        return mSwtUpdaterData;
+    }
+
+    /**
+     * Initializes the shell with a 2-column Grid layout.
+     * Caller should use {@link #createCloseButton()} to inject the
+     * close button at the bottom of the dialog.
+     */
+    @Override
+    protected void createContents() {
+        Shell shell = getShell();
+        setWindowImage(shell);
+
+        GridLayoutBuilder.create(shell).columns(2);
+    }
+
+    protected void createCloseButton() {
+        Button close = new Button(getShell(), SWT.PUSH);
+        close.setText("Close");
+        GridDataBuilder.create(close).hFill().vBottom();
+        close.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                close();
+            }
+        });
+    }
+
+    @Override
+    protected void postCreate() {
+        // pass
+    }
+
+    @Override
+    protected void close() {
+        super.close();
+    }
+
+    /**
+     * Creates the icon of the window shell.
+     *
+     * @param shell The shell on which to put the icon
+     */
+    private void setWindowImage(Shell shell) {
+        String imageName = "android_icon_16.png"; //$NON-NLS-1$
+        if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_DARWIN) {
+            imageName = "android_icon_128.png"; //$NON-NLS-1$
+        }
+
+        if (mSwtUpdaterData != null) {
+            ImageFactory imgFactory = mSwtUpdaterData.getImageFactory();
+            if (imgFactory != null) {
+                shell.setImage(imgFactory.getImageByName(imageName));
+            }
+        }
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PackagesDiffLogic.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PackagesDiffLogic.java
new file mode 100755
index 0000000..1d72bbe
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PackagesDiffLogic.java
@@ -0,0 +1,1002 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository.core;
+
+import com.android.SdkConstants;
+import com.android.sdklib.AndroidVersion;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.internal.repository.packages.BuildToolPackage;
+import com.android.sdklib.internal.repository.packages.ExtraPackage;
+import com.android.sdklib.internal.repository.packages.IAndroidVersionProvider;
+import com.android.sdklib.internal.repository.packages.IFullRevisionProvider;
+import com.android.sdklib.internal.repository.packages.Package;
+import com.android.sdklib.internal.repository.packages.Package.UpdateInfo;
+import com.android.sdklib.internal.repository.packages.PlatformPackage;
+import com.android.sdklib.internal.repository.packages.PlatformToolPackage;
+import com.android.sdklib.internal.repository.packages.SystemImagePackage;
+import com.android.sdklib.internal.repository.packages.ToolPackage;
+import com.android.sdklib.internal.repository.sources.SdkSource;
+import com.android.sdklib.internal.repository.updater.PkgItem;
+import com.android.sdklib.internal.repository.updater.PkgItem.PkgState;
+import com.android.sdklib.repository.FullRevision;
+import com.android.sdklib.repository.FullRevision.PreviewComparison;
+import com.android.sdklib.util.SparseArray;
+import com.android.sdkuilib.internal.repository.SwtUpdaterData;
+import com.android.sdkuilib.internal.repository.ui.PackagesPageIcons;
+import com.android.utils.Pair;
+import com.google.common.collect.Maps;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Helper class that separates the logic of package management from the UI
+ * so that we can test it using head-less unit tests.
+ */
+public class PackagesDiffLogic {
+    private final SwtUpdaterData mUpdaterData;
+    private boolean mFirstLoadComplete = true;
+
+    public PackagesDiffLogic(SwtUpdaterData swtUpdaterData) {
+        mUpdaterData = swtUpdaterData;
+    }
+
+    /**
+     * Removes all the internal state and resets the object.
+     * Useful for testing.
+     */
+    public void clear() {
+        mFirstLoadComplete = true;
+        mOpApi.clear();
+        mOpSource.clear();
+    }
+
+    /** Return mFirstLoadComplete and resets it to false.
+     * All following calls will returns false. */
+    public boolean isFirstLoadComplete() {
+        boolean b = mFirstLoadComplete;
+        mFirstLoadComplete = false;
+        return b;
+    }
+
+    /**
+     * Mark all new and update PkgItems as checked.
+     *
+     * @param selectNew If true, select all new packages (except the rc/preview ones).
+     * @param selectUpdates If true, select all update packages.
+     * @param selectTop If true, select the top platform.
+     *   If the top platform has nothing installed, select all items in it (except the rc/preview);
+     *   If it is partially installed, at least select the platform and system images if none of
+     *   the system images are installed.
+     * @param currentPlatform The {@link SdkConstants#currentPlatform()} value.
+     */
+    public void checkNewUpdateItems(
+            boolean selectNew,
+            boolean selectUpdates,
+            boolean selectTop,
+            int currentPlatform) {
+        int maxApi = 0;
+        Set<Integer> installedPlatforms = new HashSet<Integer>();
+        SparseArray<List<PkgItem>> platformItems = new SparseArray<List<PkgItem>>();
+
+        boolean hasTools = false;
+        Map<Class<?>, Pair<PkgItem, FullRevision>> toolsCandidates = Maps.newHashMap();
+        toolsCandidates.put(PlatformToolPackage.class, Pair.of((PkgItem)null, (FullRevision)null));
+        toolsCandidates.put(BuildToolPackage.class,    Pair.of((PkgItem)null, (FullRevision)null));
+
+        // sort items in platforms... directly deal with new/update items
+        List<PkgItem> allItems = getAllPkgItems(true /*byApi*/, true /*bySource*/);
+        for (PkgItem item : allItems) {
+            if (!item.hasCompatibleArchive()) {
+                // Ignore items that have no archive compatible with the current platform.
+                continue;
+            }
+
+            // Get the main package's API level. We don't need to look at the updates
+            // since by definition they should target the same API level.
+            int api = 0;
+            Package p = item.getMainPackage();
+            if (p instanceof IAndroidVersionProvider) {
+                api = ((IAndroidVersionProvider) p).getAndroidVersion().getApiLevel();
+            }
+
+            if (selectTop && api > 0) {
+                // Keep track of the max api seen
+                maxApi = Math.max(maxApi, api);
+
+                // keep track of what platform is currently installed (that is, has at least
+                // one thing installed.)
+                if (item.getState() == PkgState.INSTALLED) {
+                    installedPlatforms.add(api);
+                }
+
+                // for each platform, collect all its related item for later use below.
+                List<PkgItem> items = platformItems.get(api);
+                if (items == null) {
+                    platformItems.put(api, items = new ArrayList<PkgItem>());
+                }
+                items.add(item);
+            }
+
+            if ((selectUpdates || selectNew) &&
+                    item.getState() == PkgState.NEW &&
+                    !item.getRevision().isPreview()) {
+                boolean sameFound = false;
+                Package newPkg = item.getMainPackage();
+                if (newPkg instanceof IFullRevisionProvider) {
+                    // We have a potential new non-preview package; but this kind of package
+                    // supports having previews, which means we want to make sure we're not
+                    // offering an older "new" non-preview if there's a newer preview installed.
+                    //
+                    // We should get into this odd situation only when updating an RC/preview
+                    // by a final release pkg.
+
+                    IFullRevisionProvider newPkg2 = (IFullRevisionProvider) newPkg;
+                    for (PkgItem item2 : allItems) {
+                        if (item2.getState() == PkgState.INSTALLED) {
+                            Package installed = item2.getMainPackage();
+
+                            if (installed.getRevision().isPreview() &&
+                                    newPkg2.sameItemAs(installed, PreviewComparison.IGNORE)) {
+                                sameFound = true;
+
+                                if (installed.canBeUpdatedBy(newPkg) == UpdateInfo.UPDATE) {
+                                    item.setChecked(true);
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                }
+
+                if (selectNew && !sameFound) {
+                    item.setChecked(true);
+                }
+
+            } else if (selectUpdates && item.hasUpdatePkg()) {
+                item.setChecked(true);
+            }
+
+            // Keep track of the tools and offer to auto-select platform-tools/build-tools.
+            if (selectTop) {
+                if (p instanceof ToolPackage && p.isLocal()) {
+                    hasTools = true; // main tool package is installed.
+                } else if (p instanceof PlatformToolPackage || p instanceof BuildToolPackage) {
+                    for (Class<?> clazz : toolsCandidates.keySet()) {
+                        if (clazz.isInstance(p)) { // allow p to be a mock-derived class
+                            if (p.isLocal()) {
+                                // There's one such package installed, we don't need candidates.
+                                toolsCandidates.remove(clazz);
+                            } else if (toolsCandidates.containsKey(clazz)) {
+                                Pair<PkgItem, FullRevision> val = toolsCandidates.get(clazz);
+                                FullRevision rev = p.getRevision();
+                                if (!rev.isPreview()) {
+                                    // Don't auto-select previews.
+                                    if (val.getSecond() == null ||
+                                            rev.compareTo(val.getSecond()) > 0) {
+                                        // No revision: set the first candidate.
+                                        // Or we found a new higher revision
+                                        toolsCandidates.put(clazz, Pair.of(item, rev));
+                                    }
+                                }
+                            }
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+
+        // Select the top platform/build-tool found above if needed.
+        if (selectTop && hasTools) {
+            for (Pair<PkgItem, FullRevision> candidate : toolsCandidates.values()) {
+                PkgItem item = candidate.getFirst();
+                if (item != null) {
+                    item.setChecked(true);
+                }
+            }
+        }
+
+
+        // Select top platform items.
+
+        List<PkgItem> items = platformItems.get(maxApi);
+        if (selectTop && maxApi > 0 && items != null) {
+            if (!installedPlatforms.contains(maxApi)) {
+                // If the top platform has nothing installed at all, select everything in it
+                for (PkgItem item : items) {
+                    if ((item.getState() == PkgState.NEW && !item.getRevision().isPreview()) ||
+                            item.hasUpdatePkg()) {
+                        item.setChecked(true);
+                    }
+                }
+
+            } else {
+                // The top platform has at least one thing installed.
+
+                // First make sure the platform package itself is installed, or select it.
+                for (PkgItem item : items) {
+                     Package p = item.getMainPackage();
+                     if (p instanceof PlatformPackage &&
+                             item.getState() == PkgState.NEW && !item.getRevision().isPreview()) {
+                         item.setChecked(true);
+                         break;
+                     }
+                }
+
+                // Check we have at least one system image installed, otherwise select them
+                boolean hasSysImg = false;
+                for (PkgItem item : items) {
+                    Package p = item.getMainPackage();
+                    if (p instanceof PlatformPackage && item.getState() == PkgState.INSTALLED) {
+                        if (item.hasUpdatePkg() && item.isChecked()) {
+                            // If the installed platform is scheduled for update, look for the
+                            // system image in the update package, not the current one.
+                            p = item.getUpdatePkg();
+                            if (p instanceof PlatformPackage) {
+                                hasSysImg = ((PlatformPackage) p).getIncludedAbi() != null;
+                            }
+                        } else {
+                            // Otherwise look into the currently installed platform
+                            hasSysImg = ((PlatformPackage) p).getIncludedAbi() != null;
+                        }
+                        if (hasSysImg) {
+                            break;
+                        }
+                    }
+                    if (p instanceof SystemImagePackage && item.getState() == PkgState.INSTALLED) {
+                        hasSysImg = true;
+                        break;
+                    }
+                }
+                if (!hasSysImg) {
+                    // No system image installed.
+                    // Try whether the current platform or its update would bring one.
+
+                    for (PkgItem item : items) {
+                         Package p = item.getMainPackage();
+                         if (p instanceof PlatformPackage) {
+                             if (item.getState() == PkgState.NEW &&
+                                     !item.getRevision().isPreview() &&
+                                     ((PlatformPackage) p).getIncludedAbi() != null) {
+                                 item.setChecked(true);
+                                 hasSysImg = true;
+                             } else if (item.hasUpdatePkg()) {
+                                 p = item.getUpdatePkg();
+                                 if (p instanceof PlatformPackage &&
+                                         ((PlatformPackage) p).getIncludedAbi() != null) {
+                                     item.setChecked(true);
+                                     hasSysImg = true;
+                                 }
+                             }
+                         }
+                    }
+                }
+                if (!hasSysImg) {
+                    // No system image in the platform, try a system image package
+                    for (PkgItem item : items) {
+                        Package p = item.getMainPackage();
+                        if (p instanceof SystemImagePackage && item.getState() == PkgState.NEW) {
+                            item.setChecked(true);
+                        }
+                    }
+                }
+            }
+        }
+
+        if (selectTop) {
+            for (PkgItem item : getAllPkgItems(true /*byApi*/, true /*bySource*/)) {
+                Package p = item.getMainPackage();
+                if (p instanceof ExtraPackage &&
+                        item.getState() == PkgState.NEW &&
+                        !item.getRevision().isPreview()) {
+                    ExtraPackage ep = (ExtraPackage) p;
+
+                    // On Windows, we'll also auto-select the USB driver
+                    if (currentPlatform == SdkConstants.PLATFORM_WINDOWS) {
+                        if (ep.getVendorId().equals("google") &&            //$NON-NLS-1$
+                                ep.getPath().equals("usb_driver")) {        //$NON-NLS-1$
+                            item.setChecked(true);
+                            continue;
+                        }
+                    }
+
+                    // On all platforms, we'll auto-select the support library.
+                    if (ep.getVendorId().equals("android") &&               //$NON-NLS-1$
+                            ep.getPath().equals("support")) {               //$NON-NLS-1$
+                        item.setChecked(true);
+                        continue;
+                    }
+
+                }
+            }
+        }
+    }
+
+    /**
+     * Mark all PkgItems as not checked.
+     */
+    public void uncheckAllItems() {
+        for (PkgItem item : getAllPkgItems(true /*byApi*/, true /*bySource*/)) {
+            item.setChecked(false);
+        }
+    }
+
+    /**
+     * An update operation, customized to either sort by API or sort by source.
+     */
+    abstract class UpdateOp {
+        private final Set<SdkSource> mVisitedSources = new HashSet<SdkSource>();
+        private final List<PkgCategory> mCategories = new ArrayList<PkgCategory>();
+        private final Set<PkgCategory> mCatsToRemove = new HashSet<PkgCategory>();
+        private final Set<PkgItem> mItemsToRemove = new HashSet<PkgItem>();
+        private final Map<Package, PkgItem> mUpdatesToRemove = new HashMap<Package, PkgItem>();
+
+        /** Removes all internal state. */
+        public void clear() {
+            mVisitedSources.clear();
+            mCategories.clear();
+        }
+
+        /** Retrieve the sorted category list. */
+        public List<PkgCategory> getCategories() {
+            return mCategories;
+        }
+
+        /** Retrieve the category key for the given package, either local or remote. */
+        public abstract Object getCategoryKey(Package pkg);
+
+        /** Modified {@code currentCategories} to add default categories. */
+        public abstract void addDefaultCategories();
+
+        /** Creates the category for the given key and returns it. */
+        public abstract PkgCategory createCategory(Object catKey);
+        /** Adjust attributes of an existing category. */
+        public abstract void adjustCategory(PkgCategory cat, Object catKey);
+
+        /** Sorts the category list (but not the items within the categories.) */
+        public abstract void sortCategoryList();
+
+        /** Called after items of a given category have changed. Used to sort the
+         * items and/or adjust the category name. */
+        public abstract void postCategoryItemsChanged();
+
+        public void updateStart() {
+            mVisitedSources.clear();
+
+            // Note that default categories are created after the unused ones so that
+            // the callback can decide whether they should be marked as unused or not.
+            mCatsToRemove.clear();
+            mItemsToRemove.clear();
+            mUpdatesToRemove.clear();
+            for (PkgCategory cat : mCategories) {
+                mCatsToRemove.add(cat);
+                List<PkgItem> items = cat.getItems();
+                mItemsToRemove.addAll(items);
+                for (PkgItem item : items) {
+                    if (item.hasUpdatePkg()) {
+                        mUpdatesToRemove.put(item.getUpdatePkg(), item);
+                    }
+                }
+            }
+
+            addDefaultCategories();
+        }
+
+        public boolean updateSourcePackages(SdkSource source, Package[] newPackages) {
+            mVisitedSources.add(source);
+            if (source == null) {
+                return processLocals(this, newPackages);
+            } else {
+                return processSource(this, source, newPackages);
+            }
+        }
+
+        public boolean updateEnd() {
+            boolean hasChanged = false;
+
+            // Remove unused categories & items at the end of the update
+            synchronized (mCategories) {
+                for (PkgCategory unusedCat : mCatsToRemove) {
+                    if (mCategories.remove(unusedCat)) {
+                        hasChanged  = true;
+                    }
+                }
+            }
+
+            for (PkgCategory cat : mCategories) {
+                for (Iterator<PkgItem> itemIt = cat.getItems().iterator(); itemIt.hasNext(); ) {
+                    PkgItem item = itemIt.next();
+                    if (mItemsToRemove.contains(item)) {
+                        itemIt.remove();
+                        hasChanged  = true;
+                    } else if (item.hasUpdatePkg() &&
+                            mUpdatesToRemove.containsKey(item.getUpdatePkg())) {
+                        item.removeUpdate();
+                        hasChanged  = true;
+                    }
+                }
+            }
+
+            mCatsToRemove.clear();
+            mItemsToRemove.clear();
+            mUpdatesToRemove.clear();
+
+            return hasChanged;
+        }
+
+        public boolean isKeep(PkgItem item) {
+            return !mItemsToRemove.contains(item);
+        }
+
+        public void keep(Package pkg) {
+            mUpdatesToRemove.remove(pkg);
+        }
+
+        public void keep(PkgItem item) {
+            mItemsToRemove.remove(item);
+        }
+
+        public void keep(PkgCategory cat) {
+            mCatsToRemove.remove(cat);
+        }
+
+        public void dontKeep(PkgItem item) {
+            mItemsToRemove.add(item);
+        }
+
+        public void dontKeep(PkgCategory cat) {
+            mCatsToRemove.add(cat);
+        }
+    }
+
+    private final UpdateOpApi    mOpApi    = new UpdateOpApi();
+    private final UpdateOpSource mOpSource = new UpdateOpSource();
+
+    public List<PkgCategory> getCategories(boolean displayIsSortByApi) {
+        return displayIsSortByApi ? mOpApi.getCategories() : mOpSource.getCategories();
+    }
+
+    public List<PkgItem> getAllPkgItems(boolean byApi, boolean bySource) {
+        List<PkgItem> items = new ArrayList<PkgItem>();
+
+        if (byApi) {
+            List<PkgCategory> cats = getCategories(true /*displayIsSortByApi*/);
+            synchronized (cats) {
+                for (PkgCategory cat : cats) {
+                    items.addAll(cat.getItems());
+                }
+            }
+        }
+
+        if (bySource) {
+            List<PkgCategory> cats = getCategories(false /*displayIsSortByApi*/);
+            synchronized (cats) {
+                for (PkgCategory cat : cats) {
+                    items.addAll(cat.getItems());
+                }
+            }
+        }
+
+        return items;
+    }
+
+    public void updateStart() {
+        mOpApi.updateStart();
+        mOpSource.updateStart();
+    }
+
+    public boolean updateSourcePackages(
+            boolean displayIsSortByApi,
+            SdkSource source,
+            Package[] newPackages) {
+
+        boolean apiListChanged = mOpApi.updateSourcePackages(source, newPackages);
+        boolean sourceListChanged = mOpSource.updateSourcePackages(source, newPackages);
+        return displayIsSortByApi ? apiListChanged : sourceListChanged;
+    }
+
+    public boolean updateEnd(boolean displayIsSortByApi) {
+        boolean apiListChanged = mOpApi.updateEnd();
+        boolean sourceListChanged = mOpSource.updateEnd();
+        return displayIsSortByApi ? apiListChanged : sourceListChanged;
+    }
+
+
+    /** Process all local packages. Returns true if something changed. */
+    private boolean processLocals(UpdateOp op, Package[] packages) {
+        boolean hasChanged = false;
+        List<PkgCategory> cats = op.getCategories();
+        Set<PkgItem> keep = new HashSet<PkgItem>();
+
+        // For all locally installed packages, check they are either listed
+        // as installed or create new installed items for them.
+
+        nextPkg: for (Package localPkg : packages) {
+            // Check to see if we already have the exact same package
+            // (type & revision) marked as installed.
+            for (PkgCategory cat : cats) {
+                for (PkgItem currItem : cat.getItems()) {
+                    if (currItem.getState() == PkgState.INSTALLED &&
+                            currItem.isSameMainPackageAs(localPkg)) {
+                        // This package is already listed as installed.
+                        op.keep(currItem);
+                        op.keep(cat);
+                        keep.add(currItem);
+                        continue nextPkg;
+                    }
+                }
+            }
+
+            // If not found, create a new installed package item
+            keep.add(addNewItem(op, localPkg, PkgState.INSTALLED));
+            hasChanged = true;
+        }
+
+        // Remove installed items that we don't want to keep anymore. They would normally be
+        // cleanup up in UpdateOp.updateEnd(); however it's easier to remove them before we
+        // run processSource() to avoid merging updates in items that would be removed later.
+
+        for (PkgCategory cat : cats) {
+            for (Iterator<PkgItem> itemIt = cat.getItems().iterator(); itemIt.hasNext(); ) {
+                PkgItem item = itemIt.next();
+                if (item.getState() == PkgState.INSTALLED && !keep.contains(item)) {
+                    itemIt.remove();
+                    hasChanged = true;
+                }
+            }
+        }
+
+        if (hasChanged) {
+            op.postCategoryItemsChanged();
+        }
+
+        return hasChanged;
+    }
+
+    /**
+     * {@link PkgState}s to check in {@link #processSource(UpdateOp, SdkSource, Package[])}.
+     * The order matters.
+     * When installing the diff will have both the new and the installed item and we
+     * need to merge with the installed one before the new one.
+     */
+    private final static PkgState[] PKG_STATES = { PkgState.INSTALLED, PkgState.NEW };
+
+    /** Process all remote packages. Returns true if something changed. */
+    private boolean processSource(UpdateOp op, SdkSource source, Package[] packages) {
+        boolean hasChanged = false;
+        List<PkgCategory> cats = op.getCategories();
+
+        boolean enablePreviews =
+            mUpdaterData.getSettingsController().getSettings().getEnablePreviews();
+
+        nextPkg: for (Package newPkg : packages) {
+
+            if (!enablePreviews && newPkg.getRevision().isPreview()) {
+                // This is a preview and previews are not enabled. Ignore the package.
+                continue nextPkg;
+            }
+
+            for (PkgCategory cat : cats) {
+                for (PkgState state : PKG_STATES) {
+                    for (Iterator<PkgItem> currItemIt = cat.getItems().iterator();
+                                           currItemIt.hasNext(); ) {
+                        PkgItem currItem = currItemIt.next();
+                        // We need to merge with installed items first. When installing
+                        // the diff will have both the new and the installed item and we
+                        // need to merge with the installed one before the new one.
+                        if (currItem.getState() != state) {
+                            continue;
+                        }
+                        // Only process current items if they represent the same item (but
+                        // with a different revision number) than the new package.
+                        Package mainPkg = currItem.getMainPackage();
+                        if (!mainPkg.sameItemAs(newPkg)) {
+                            continue;
+                        }
+
+                        // Check to see if we already have the exact same package
+                        // (type & revision) marked as main or update package.
+                        if (currItem.isSameMainPackageAs(newPkg)) {
+                            op.keep(currItem);
+                            op.keep(cat);
+                            continue nextPkg;
+                        } else if (currItem.hasUpdatePkg() &&
+                                currItem.isSameUpdatePackageAs(newPkg)) {
+                            op.keep(currItem.getUpdatePkg());
+                            op.keep(cat);
+                            continue nextPkg;
+                        }
+
+                        switch (currItem.getState()) {
+                        case NEW:
+                            if (newPkg.getRevision().compareTo(mainPkg.getRevision()) < 0) {
+                                if (!op.isKeep(currItem)) {
+                                    // The new item has a lower revision than the current one,
+                                    // but the current one hasn't been marked as being kept so
+                                    // it's ok to downgrade it.
+                                    currItemIt.remove();
+                                    addNewItem(op, newPkg, PkgState.NEW);
+                                    hasChanged = true;
+                                }
+                            } else if (newPkg.getRevision().compareTo(mainPkg.getRevision()) > 0) {
+                                // We have a more recent new version, remove the current one
+                                // and replace by a new one
+                                currItemIt.remove();
+                                addNewItem(op, newPkg, PkgState.NEW);
+                                hasChanged = true;
+                            }
+                            break;
+                        case INSTALLED:
+                            // if newPkg.revision<=mainPkg.revision: it's already installed, ignore.
+                            if (newPkg.getRevision().compareTo(mainPkg.getRevision()) > 0) {
+                                // This is a new update for the main package.
+                                if (currItem.mergeUpdate(newPkg)) {
+                                    op.keep(currItem.getUpdatePkg());
+                                    op.keep(cat);
+                                    hasChanged = true;
+                                }
+                            }
+                            break;
+                        }
+                        continue nextPkg;
+                    }
+                }
+            }
+            // If not found, create a new package item
+            addNewItem(op, newPkg, PkgState.NEW);
+            hasChanged = true;
+        }
+
+        if (hasChanged) {
+            op.postCategoryItemsChanged();
+        }
+
+        return hasChanged;
+    }
+
+    private PkgItem addNewItem(UpdateOp op, Package pkg, PkgState state) {
+        List<PkgCategory> cats = op.getCategories();
+        Object catKey = op.getCategoryKey(pkg);
+        PkgCategory cat = findCurrentCategory(cats, catKey);
+
+        if (cat == null) {
+            // This is a new category. Create it and add it to the list.
+            cat = op.createCategory(catKey);
+            synchronized (cats) {
+                cats.add(cat);
+            }
+            op.sortCategoryList();
+        } else {
+            // Not a new category. Give op a chance to adjust the category attributes
+            op.adjustCategory(cat, catKey);
+        }
+
+        PkgItem item = new PkgItem(pkg, state);
+        op.keep(item);
+        cat.getItems().add(item);
+        op.keep(cat);
+        return item;
+    }
+
+    private PkgCategory findCurrentCategory(
+            List<PkgCategory> currentCategories,
+            Object categoryKey) {
+        for (PkgCategory cat : currentCategories) {
+            if (cat.getKey().equals(categoryKey)) {
+                return cat;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * {@link UpdateOp} describing the Sort-by-API operation.
+     */
+    private class UpdateOpApi extends UpdateOp {
+        @Override
+        public Object getCategoryKey(Package pkg) {
+            // Sort by API
+
+            if (pkg instanceof IAndroidVersionProvider) {
+                return ((IAndroidVersionProvider) pkg).getAndroidVersion();
+
+            } else if (pkg instanceof ToolPackage ||
+                    pkg instanceof PlatformToolPackage ||
+                    pkg instanceof BuildToolPackage) {
+                if (pkg.getRevision().isPreview()) {
+                    return PkgCategoryApi.KEY_TOOLS_PREVIEW;
+                } else {
+                    return PkgCategoryApi.KEY_TOOLS;
+                }
+            } else {
+                return PkgCategoryApi.KEY_EXTRA;
+            }
+        }
+
+        @Override
+        public void addDefaultCategories() {
+            boolean needTools = true;
+            boolean needExtras = true;
+
+            List<PkgCategory> cats = getCategories();
+            for (PkgCategory cat : cats) {
+                if (cat.getKey().equals(PkgCategoryApi.KEY_TOOLS)) {
+                    // Mark them as not unused to prevent their removal in updateEnd().
+                    keep(cat);
+                    needTools = false;
+                } else if (cat.getKey().equals(PkgCategoryApi.KEY_EXTRA)) {
+                    keep(cat);
+                    needExtras = false;
+                }
+            }
+
+            // Always add the tools & extras categories, even if empty (unlikely anyway)
+            if (needTools) {
+                PkgCategoryApi acat = new PkgCategoryApi(
+                   PkgCategoryApi.KEY_TOOLS,
+                   null,
+                   mUpdaterData.getImageFactory().getImageByName(PackagesPageIcons.ICON_CAT_OTHER));
+                synchronized (cats) {
+                    cats.add(acat);
+                }
+            }
+
+            if (needExtras) {
+                PkgCategoryApi acat = new PkgCategoryApi(
+                   PkgCategoryApi.KEY_EXTRA,
+                   null,
+                   mUpdaterData.getImageFactory().getImageByName(PackagesPageIcons.ICON_CAT_OTHER));
+                synchronized (cats) {
+                    cats.add(acat);
+                }
+            }
+        }
+
+        @Override
+        public PkgCategory createCategory(Object catKey) {
+            // Create API category.
+            PkgCategory cat = null;
+
+            assert catKey instanceof AndroidVersion;
+            AndroidVersion key = (AndroidVersion) catKey;
+
+            // We should not be trying to recreate the tools or extra categories.
+            assert !key.equals(PkgCategoryApi.KEY_TOOLS) && !key.equals(PkgCategoryApi.KEY_EXTRA);
+
+            // We need a label for the category.
+            // If we have an API level, try to get the info from the SDK Manager.
+            // If we don't (e.g. when installing a new platform that isn't yet available
+            // locally in the SDK Manager), it's OK we'll try to find the first platform
+            // package available.
+            String platformName = null;
+            for (IAndroidTarget target :
+                    mUpdaterData.getSdkManager().getTargets()) {
+                if (target.isPlatform() && key.equals(target.getVersion())) {
+                    platformName = target.getVersionName();
+                    break;
+                }
+            }
+
+            cat = new PkgCategoryApi(
+                key,
+                platformName,
+                mUpdaterData.getImageFactory().getImageByName(PackagesPageIcons.ICON_CAT_PLATFORM));
+
+            return cat;
+        }
+
+        @Override
+        public void adjustCategory(PkgCategory cat, Object catKey) {
+            // Pass. Nothing to do for API-sorted categories
+        }
+
+        @Override
+        public void sortCategoryList() {
+            // Sort the categories list.
+            // We always want categories in order tools..platforms..extras.
+            // For platform, we compare in descending order (o2-o1).
+            // This order is achieved by having the category keys ordered as
+            // needed for the sort to just do what we expect.
+
+            synchronized (getCategories()) {
+                Collections.sort(getCategories(), new Comparator<PkgCategory>() {
+                    @Override
+                    public int compare(PkgCategory cat1, PkgCategory cat2) {
+                        assert cat1 instanceof PkgCategoryApi;
+                        assert cat2 instanceof PkgCategoryApi;
+                        assert cat1.getKey() instanceof AndroidVersion;
+                        assert cat2.getKey() instanceof AndroidVersion;
+                        AndroidVersion v1 = (AndroidVersion) cat1.getKey();
+                        AndroidVersion v2 = (AndroidVersion) cat2.getKey();
+                        return v2.compareTo(v1);
+                    }
+                });
+            }
+        }
+
+        @Override
+        public void postCategoryItemsChanged() {
+            // Sort the items
+            for (PkgCategory cat : getCategories()) {
+                Collections.sort(cat.getItems());
+
+                // When sorting by API, we can't always get the platform name
+                // from the package manager. In this case at the very end we
+                // look for a potential platform package we can use to extract
+                // the platform version name (e.g. '1.5') from the first suitable
+                // platform package we can find.
+
+                assert cat instanceof PkgCategoryApi;
+                PkgCategoryApi pac = (PkgCategoryApi) cat;
+                if (pac.getPlatformName() == null) {
+                    // Check whether we can get the actual platform version name (e.g. "1.5")
+                    // from the first Platform package we find in this category.
+
+                    for (PkgItem item : cat.getItems()) {
+                        Package p = item.getMainPackage();
+                        if (p instanceof PlatformPackage) {
+                            String platformName = ((PlatformPackage) p).getVersionName();
+                            if (platformName != null) {
+                                pac.setPlatformName(platformName);
+                                break;
+                            }
+                        }
+                    }
+                }
+            }
+
+        }
+    }
+
+    /**
+     * {@link UpdateOp} describing the Sort-by-Source operation.
+     */
+    private class UpdateOpSource extends UpdateOp {
+
+        @Override
+        public boolean updateSourcePackages(SdkSource source, Package[] newPackages) {
+            // When displaying the repo by source, we want to create all the
+            // categories so that they can appear on the UI even if empty.
+            if (source != null) {
+                List<PkgCategory> cats = getCategories();
+                Object catKey = source;
+                PkgCategory cat = findCurrentCategory(cats, catKey);
+
+                if (cat == null) {
+                    // This is a new category. Create it and add it to the list.
+                    cat = createCategory(catKey);
+                    synchronized (cats) {
+                        cats.add(cat);
+                    }
+                    sortCategoryList();
+                }
+
+                keep(cat);
+            }
+
+            return super.updateSourcePackages(source, newPackages);
+        }
+
+        @Override
+        public Object getCategoryKey(Package pkg) {
+            // Sort by source
+            SdkSource source = pkg.getParentSource();
+            if (source == null) {
+                return PkgCategorySource.UNKNOWN_SOURCE;
+            }
+            return source;
+        }
+
+        @Override
+        public void addDefaultCategories() {
+            List<PkgCategory> cats = getCategories();
+            for (PkgCategory cat : cats) {
+                if (cat.getKey().equals(PkgCategorySource.UNKNOWN_SOURCE)) {
+                    // Already present.
+                    return;
+                }
+            }
+
+            // Always add the local categories, even if empty (unlikely anyway)
+            PkgCategorySource cat = new PkgCategorySource(
+                    PkgCategorySource.UNKNOWN_SOURCE,
+                    mUpdaterData);
+            // Mark it so that it can be cleared in updateEnd() if not used.
+            dontKeep(cat);
+            synchronized (cats) {
+                cats.add(cat);
+            }
+        }
+
+        /**
+         * Create a new source category.
+         * <p/>
+         * One issue is that local archives are processed first and we don't have the
+         * full source information on them (e.g. we know the referral URL but not
+         * the referral name of the site).
+         * In this case this will just create {@link PkgCategorySource} where the label isn't
+         * known yet.
+         */
+        @Override
+        public PkgCategory createCategory(Object catKey) {
+            assert catKey instanceof SdkSource;
+            PkgCategory cat = new PkgCategorySource((SdkSource) catKey, mUpdaterData);
+            return cat;
+        }
+
+        /**
+         * Checks whether the category needs to be adjust.
+         * As mentioned in {@link #createCategory(Object)}, local archives are processed
+         * first and result in a {@link PkgCategorySource} where the label isn't known.
+         * Once we process the external source with the actual name, we'll update it.
+         */
+        @Override
+        public void adjustCategory(PkgCategory cat, Object catKey) {
+            assert cat instanceof PkgCategorySource;
+            assert catKey instanceof SdkSource;
+            if (cat instanceof PkgCategorySource) {
+                ((PkgCategorySource) cat).adjustLabel((SdkSource) catKey);
+            }
+        }
+
+        @Override
+        public void sortCategoryList() {
+            // Sort the sources in ascending source name order,
+            // with the local packages always first.
+
+            synchronized (getCategories()) {
+                Collections.sort(getCategories(), new Comparator<PkgCategory>() {
+                    @Override
+                    public int compare(PkgCategory cat1, PkgCategory cat2) {
+                        assert cat1 instanceof PkgCategorySource;
+                        assert cat2 instanceof PkgCategorySource;
+
+                        SdkSource src1 = ((PkgCategorySource) cat1).getSource();
+                        SdkSource src2 = ((PkgCategorySource) cat2).getSource();
+
+                        if (src1 == src2) {
+                            return 0;
+                        } else if (src1 == PkgCategorySource.UNKNOWN_SOURCE) {
+                            return -1;
+                        } else if (src2 == PkgCategorySource.UNKNOWN_SOURCE) {
+                            return 1;
+                        }
+                        assert src1 != null; // true because LOCAL_SOURCE==null
+                        assert src2 != null;
+                        return src1.toString().compareTo(src2.toString());
+                    }
+                });
+            }
+        }
+
+        @Override
+        public void postCategoryItemsChanged() {
+            // Sort the items
+            for (PkgCategory cat : getCategories()) {
+                Collections.sort(cat.getItems());
+            }
+        }
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategory.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategory.java
new file mode 100755
index 0000000..e46c8b1
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategory.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository.core;
+
+
+import com.android.sdklib.internal.repository.updater.PkgItem;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class PkgCategory {
+    private final Object mKey;
+    private final Object mIconRef;
+    private final List<PkgItem> mItems = new ArrayList<PkgItem>();
+    private String mLabel;
+
+    public PkgCategory(Object key, String label, Object iconRef) {
+        mKey = key;
+        mLabel = label;
+        mIconRef = iconRef;
+    }
+
+    public Object getKey() {
+        return mKey;
+    }
+
+    public String getLabel() {
+        return mLabel;
+    }
+
+    public void setLabel(String label) {
+        mLabel = label;
+    }
+
+    public Object getIconRef() {
+        return mIconRef;
+    }
+
+    public List<PkgItem> getItems() {
+        return mItems;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("%s <key=%s, label=%s, #items=%d>",
+                this.getClass().getSimpleName(),
+                mKey == null ? "null" : mKey.toString(),
+                mLabel,
+                mItems.size());
+    }
+
+    /** {@link PkgCategory}s are equal if their internal keys are equal. */
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((mKey == null) ? 0 : mKey.hashCode());
+        return result;
+    }
+
+    /** {@link PkgCategory}s are equal if their internal keys are equal. */
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+        if (obj == null) return false;
+        if (getClass() != obj.getClass()) return false;
+        PkgCategory other = (PkgCategory) obj;
+        if (mKey == null) {
+            if (other.mKey != null) return false;
+        } else if (!mKey.equals(other.mKey)) return false;
+        return true;
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategoryApi.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategoryApi.java
new file mode 100755
index 0000000..aff11e5
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategoryApi.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository.core;
+
+import com.android.sdklib.AndroidVersion;
+
+
+public class PkgCategoryApi extends PkgCategory {
+
+    /** Platform name, in the form "Android 1.2". Can be null if we don't have the name. */
+    private String mPlatformName;
+
+    // When sorting by Source, key is the hash of the source's name.
+    // When storing by API, key is the AndroidVersion (API level >=1 + optional codename).
+    // We always want categories in order tools..platforms..extras; to achieve that tools
+    // and extras have the special values so they get "naturally" sorted the way we want
+    // them.
+    // (Note: don't use integer.max to avoid integers wrapping in comparisons. We can
+    // revisit the day we get 2^30 platforms.)
+    public final static AndroidVersion KEY_TOOLS = new AndroidVersion(Integer.MAX_VALUE / 2, null);
+    public final static AndroidVersion KEY_TOOLS_PREVIEW =
+                                               new AndroidVersion(Integer.MAX_VALUE / 2 - 1, null);
+    public final static AndroidVersion KEY_EXTRA = new AndroidVersion(-1, null);
+
+    public PkgCategoryApi(AndroidVersion version, String platformName, Object iconRef) {
+        super(version, null /*label*/, iconRef);
+        setPlatformName(platformName);
+    }
+
+    public String getPlatformName() {
+        return mPlatformName;
+    }
+
+    public void setPlatformName(String platformName) {
+        if (platformName != null) {
+            // Normal case for actual platform categories
+            mPlatformName = String.format("Android %1$s", platformName);
+            super.setLabel(null);
+        }
+    }
+
+    public String getApiLabel() {
+        AndroidVersion key = (AndroidVersion) getKey();
+        if (key.equals(KEY_TOOLS)) {
+            return "TOOLS";             //$NON-NLS-1$ // for internal debug use only
+        } else if (key.equals(KEY_TOOLS_PREVIEW)) {
+                return "TOOLS-PREVIEW"; //$NON-NLS-1$ // for internal debug use only
+        } else if (key.equals(KEY_EXTRA)) {
+            return "EXTRAS";            //$NON-NLS-1$ // for internal debug use only
+        } else {
+            return key.toString();
+        }
+    }
+
+    @Override
+    public String getLabel() {
+        String label = super.getLabel();
+        if (label == null) {
+            AndroidVersion key = (AndroidVersion) getKey();
+
+            if (key.equals(KEY_TOOLS)) {
+                label = "Tools";
+            } else if (key.equals(KEY_TOOLS_PREVIEW)) {
+                label = "Tools (Preview Channel)";
+            } else if (key.equals(KEY_EXTRA)) {
+                label = "Extras";
+            } else {
+                if (mPlatformName != null) {
+                    label = String.format("%1$s (%2$s)", mPlatformName, getApiLabel());
+                } else {
+                    label = getApiLabel();
+                }
+            }
+            super.setLabel(label);
+        }
+        return label;
+    }
+
+    @Override
+    public void setLabel(String label) {
+        throw new UnsupportedOperationException("Use setPlatformName() instead.");
+    }
+
+    @Override
+    public String toString() {
+        return String.format("%s <API=%s, label=%s, #items=%d>",
+                this.getClass().getSimpleName(),
+                getApiLabel(),
+                getLabel(),
+                getItems().size());
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategorySource.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategorySource.java
new file mode 100755
index 0000000..e3968b6
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategorySource.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository.core;
+
+import com.android.sdklib.internal.repository.sources.SdkRepoSource;
+import com.android.sdklib.internal.repository.sources.SdkSource;
+import com.android.sdkuilib.internal.repository.SwtUpdaterData;
+import com.android.sdkuilib.internal.repository.ui.PackagesPageIcons;
+
+
+public class PkgCategorySource extends PkgCategory {
+
+    /**
+     * A special {@link SdkSource} object that represents the locally installed
+     * items, or more exactly a lack of remote source.
+     */
+    public final static SdkSource UNKNOWN_SOURCE =
+        new SdkRepoSource("http://no.source", "Local Packages");
+    private final SdkSource mSource;
+
+    /**
+     * Creates a new {@link PkgCategorySource}.
+     * This uses {@link SdkSource#toString()} to get the source's description.
+     * Note that if the name of the source isn't known, the description will use its URL.
+     */
+    public PkgCategorySource(SdkSource source, SwtUpdaterData swtUpdaterData) {
+        super(
+            source, // the source is the key and it can be null
+            source == UNKNOWN_SOURCE ? "Local Packages" : source.toString(),
+            source == UNKNOWN_SOURCE ?
+                swtUpdaterData.getImageFactory()
+                              .getImageByName(PackagesPageIcons.ICON_PKG_INSTALLED) :
+                source);
+        mSource = source;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("%s <source=%s, #items=%d>",
+                this.getClass().getSimpleName(),
+                mSource.toString(),
+                getItems().size());
+    }
+
+    public SdkSource getSource() {
+        return mSource;
+    }
+
+    /** Sets the label to match the source's UI name if the label wasn't already set. */
+    public void adjustLabel(SdkSource source) {
+        if (getLabel() == null || getLabel().startsWith("http")) {  //$NON-NLS-1$
+            setLabel(source == UNKNOWN_SOURCE ? "Local Packages" : source.toString());
+        }
+    }
+
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgContentProvider.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgContentProvider.java
new file mode 100755
index 0000000..7f124d5
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgContentProvider.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository.core;
+
+import com.android.sdklib.internal.repository.IDescription;
+import com.android.sdklib.internal.repository.archives.Archive;
+import com.android.sdklib.internal.repository.packages.Package;
+import com.android.sdklib.internal.repository.sources.SdkSource;
+import com.android.sdklib.internal.repository.updater.PkgItem;
+import com.android.sdkuilib.internal.repository.ui.PackagesPage;
+
+import org.eclipse.jface.viewers.IInputProvider;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Content provider for the main tree view in {@link PackagesPage}.
+ */
+public class PkgContentProvider implements ITreeContentProvider {
+
+    private final IInputProvider mViewer;
+    private boolean mDisplayArchives;
+
+    public PkgContentProvider(IInputProvider viewer) {
+        mViewer = viewer;
+    }
+
+    public void setDisplayArchives(boolean displayArchives) {
+        mDisplayArchives = displayArchives;
+    }
+
+    @Override
+    public Object[] getChildren(Object parentElement) {
+        if (parentElement instanceof ArrayList<?>) {
+            return ((ArrayList<?>) parentElement).toArray();
+
+        } else if (parentElement instanceof PkgCategorySource) {
+            return getSourceChildren((PkgCategorySource) parentElement);
+
+        } else if (parentElement instanceof PkgCategory) {
+            return ((PkgCategory) parentElement).getItems().toArray();
+
+        } else if (parentElement instanceof PkgItem) {
+            if (mDisplayArchives) {
+
+                Package pkg = ((PkgItem) parentElement).getUpdatePkg();
+
+                // Display update packages as sub-items if the details mode is activated.
+                if (pkg != null) {
+                    return new Object[] { pkg };
+                }
+
+                return ((PkgItem) parentElement).getArchives();
+            }
+
+        } else if (parentElement instanceof Package) {
+            if (mDisplayArchives) {
+                return ((Package) parentElement).getArchives();
+            }
+
+        }
+
+        return new Object[0];
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public Object getParent(Object element) {
+        // This operation is expensive, so we do the minimum
+        // and don't try to cover all cases.
+
+        if (element instanceof PkgItem) {
+            Object input = mViewer.getInput();
+            if (input != null) {
+                for (PkgCategory cat : (List<PkgCategory>) input) {
+                    if (cat.getItems().contains(element)) {
+                        return cat;
+                    }
+                }
+            }
+        }
+
+        return null;
+    }
+
+    @Override
+    public boolean hasChildren(Object parentElement) {
+        if (parentElement instanceof ArrayList<?>) {
+            return true;
+
+        } else if (parentElement instanceof PkgCategory) {
+            return true;
+
+        } else if (parentElement instanceof PkgItem) {
+            if (mDisplayArchives) {
+                Package pkg = ((PkgItem) parentElement).getUpdatePkg();
+
+                // Display update packages as sub-items if the details mode is activated.
+                if (pkg != null) {
+                    return true;
+                }
+
+                Archive[] archives = ((PkgItem) parentElement).getArchives();
+                return archives.length > 0;
+            }
+        } else if (parentElement instanceof Package) {
+            if (mDisplayArchives) {
+                return ((Package) parentElement).getArchives().length > 0;
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    public Object[] getElements(Object inputElement) {
+        return getChildren(inputElement);
+    }
+
+    @Override
+    public void dispose() {
+        // unused
+
+    }
+
+    @Override
+    public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+        // unused
+    }
+
+
+    private Object[] getSourceChildren(PkgCategorySource parentElement) {
+        List<?> children = parentElement.getItems();
+
+        SdkSource source = parentElement.getSource();
+        IDescription error = null;
+        IDescription empty = null;
+
+        String errStr = source.getFetchError();
+        if (errStr != null) {
+            error = new RepoSourceError(source);
+        }
+        if (!source.isEnabled() || children.isEmpty()) {
+            empty = new RepoSourceNotification(source);
+        }
+
+        if (error != null || empty != null) {
+            ArrayList<Object> children2 = new ArrayList<Object>();
+            if (error != null) {
+                children2.add(error);
+            }
+            if (empty != null) {
+                children2.add(empty);
+            }
+            children2.addAll(children);
+            children = children2;
+        }
+
+        return children.toArray();
+    }
+
+
+    /**
+     * A dummy entry returned for sources which had load errors.
+     * It displays a summary of the error as its short description or
+     * it displays the source's long description.
+     */
+    public static class RepoSourceError implements IDescription {
+
+        private final SdkSource mSource;
+
+        public RepoSourceError(SdkSource source) {
+            mSource = source;
+        }
+
+        @Override
+        public String getLongDescription() {
+            return mSource.getLongDescription();
+        }
+
+        @Override
+        public String getShortDescription() {
+            return mSource.getFetchError();
+        }
+    }
+
+    /**
+     * A dummy entry returned for sources with no packages.
+     * We need that to force the SWT tree to display an open/close triangle
+     * even for empty sources.
+     */
+    public static class RepoSourceNotification implements IDescription {
+
+        private final SdkSource mSource;
+
+        public RepoSourceNotification(SdkSource source) {
+            mSource = source;
+        }
+
+        @Override
+        public String getLongDescription() {
+            if (mSource.isEnabled()) {
+                return mSource.getLongDescription();
+            } else {
+                return "Loading from this site has been disabled. " +
+                       "To enable it, use Tools > Manage Add-ons Sites.";
+            }
+        }
+
+        @Override
+        public String getShortDescription() {
+            if (mSource.isEnabled()) {
+                return "No packages found.";
+            } else {
+                return "This site is disabled. ";
+            }
+        }
+    }
+
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/SdkLogAdapter.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/SdkLogAdapter.java
new file mode 100755
index 0000000..5f24030
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/SdkLogAdapter.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository.core;
+
+import com.android.sdkuilib.internal.tasks.ILogUiProvider;
+import com.android.utils.ILogger;
+
+
+/**
+ * Adapter that transform log from an {@link ILogUiProvider} to an {@link ILogger}.
+ */
+public final class SdkLogAdapter implements ILogUiProvider {
+
+    private ILogger mSdkLog;
+    private String mLastLogMsg;
+
+    /**
+     * Creates a new adapter to output log on the given {@code sdkLog}.
+     *
+     * @param sdkLog The logger to output to. Must not be null.
+     */
+    public SdkLogAdapter(ILogger sdkLog) {
+        mSdkLog = sdkLog;
+    }
+
+    /**
+     * Sets the description in the current task dialog.
+     * This method can be invoked from a non-UI thread.
+     */
+    @Override
+    public void setDescription(final String description) {
+        if (acceptLog(description)) {
+            mSdkLog.info("%1$s", description);    //$NON-NLS-1$
+        }
+    }
+
+    /**
+     * Logs a "normal" information line.
+     * This method can be invoked from a non-UI thread.
+     */
+    @Override
+    public void log(String log) {
+        if (acceptLog(log)) {
+            mSdkLog.info("  %1$s", log);          //$NON-NLS-1$
+        }
+    }
+
+    /**
+     * Logs an "error" information line.
+     * This method can be invoked from a non-UI thread.
+     */
+    @Override
+    public void logError(String log) {
+        if (acceptLog(log)) {
+            mSdkLog.error(null, "  %1$s", log);     //$NON-NLS-1$
+        }
+    }
+
+    /**
+     * Logs a "verbose" information line, that is extra details which are typically
+     * not that useful for the end-user and might be hidden until explicitly shown.
+     * This method can be invoked from a non-UI thread.
+     */
+    @Override
+    public void logVerbose(String log) {
+        if (acceptLog(log)) {
+            mSdkLog.verbose("    %1$s", log);        //$NON-NLS-1$
+        }
+    }
+
+    // ----
+
+    /**
+     * Filter messages displayed in the log: <br/>
+     * - Messages with a % are typical part of a progress update and shouldn't be in the log. <br/>
+     * - Messages that are the same as the same output message should be output a second time.
+     *
+     * @param msg The potential log line to print.
+     * @return True if the log line should be printed, false otherwise.
+     */
+    private boolean acceptLog(String msg) {
+        if (msg == null) {
+            return false;
+        }
+
+        msg = msg.trim();
+        if (msg.indexOf('%') != -1) {
+            return false;
+        }
+
+        if (msg.equals(mLastLogMsg)) {
+            return false;
+        }
+
+        mLastLogMsg = msg;
+        return true;
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/SwtPackageLoader.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/SwtPackageLoader.java
new file mode 100755
index 0000000..c108640
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/SwtPackageLoader.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository.core;
+
+import com.android.annotations.NonNull;
+import com.android.sdklib.internal.repository.DownloadCache;
+import com.android.sdklib.internal.repository.updater.PackageLoader;
+import com.android.sdkuilib.internal.repository.SwtUpdaterData;
+
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+/**
+ * Loads packages fetched from the remote SDK Repository and keeps track
+ * of their state compared with the current local SDK installation.
+ */
+public class SwtPackageLoader extends PackageLoader {
+
+    /**
+     * Creates a new PackageManager associated with the given {@link SwtUpdaterData}
+     * and using the {@link SwtUpdaterData}'s default {@link DownloadCache}.
+     *
+     * @param swtUpdaterData The {@link SwtUpdaterData}. Must not be null.
+     */
+    public SwtPackageLoader(SwtUpdaterData swtUpdaterData) {
+        super(swtUpdaterData);
+    }
+
+    /**
+     * Creates a new PackageManager associated with the given {@link SwtUpdaterData}
+     * but using the specified {@link DownloadCache} instead of the one from
+     * {@link SwtUpdaterData}.
+     *
+     * @param swtUpdaterData The {@link SwtUpdaterData}. Must not be null.
+     * @param cache The {@link DownloadCache} to use instead of the one from {@link SwtUpdaterData}.
+     */
+    public SwtPackageLoader(SwtUpdaterData swtUpdaterData, DownloadCache cache) {
+        super(swtUpdaterData, cache);
+    }
+
+    /**
+     * Runs the runnable on the UI thread using {@link Display#syncExec(Runnable)}.
+     *
+     * @param r Non-null runnable.
+     */
+    @Override
+    protected void runOnUiThread(@NonNull Runnable r) {
+        SwtUpdaterData swtUpdaterData = (SwtUpdaterData) getUpdaterData();
+        Shell shell = swtUpdaterData.getWindowShell();
+
+        if (shell != null && !shell.isDisposed()) {
+            shell.getDisplay().syncExec(r);
+        }
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/ImageFactory.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/ImageFactory.java
new file mode 100755
index 0000000..ab84cc2
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/ImageFactory.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository.icons;
+
+import com.android.sdklib.internal.repository.archives.Archive;
+import com.android.sdklib.internal.repository.packages.Package;
+import com.android.sdklib.internal.repository.sources.SdkSource;
+import com.android.sdklib.internal.repository.sources.SdkSourceCategory;
+import com.android.sdkuilib.internal.repository.core.PkgContentProvider;
+
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+
+
+/**
+ * An utility class to serve {@link Image} correspond to the various icons
+ * present in this package and dispose of them correctly at the end.
+ */
+public class ImageFactory {
+
+    private final Display mDisplay;
+    private final Map<String, Image> mImages = new HashMap<String, Image>();
+
+    public ImageFactory(Display display) {
+        mDisplay = display;
+    }
+
+    /**
+     * Loads an image given its filename (with its extension).
+     * Might return null if the image cannot be loaded.
+     * The image is cached. Successive calls will return the <em>same</em> object.
+     *
+     * @param imageName The filename (with extension) of the image to load.
+     * @return A new or existing {@link Image}. The caller must NOT dispose the image (the
+     *  image will disposed by {@link #dispose()}). The returned image can be null if the
+     *  expected file is missing.
+     */
+    public Image getImageByName(String imageName) {
+
+        Image image = mImages.get(imageName);
+        if (image != null) {
+            return image;
+        }
+
+        InputStream stream = getClass().getResourceAsStream(imageName);
+        if (stream != null) {
+            try {
+                image = new Image(mDisplay, stream);
+            } catch (SWTException e) {
+                // ignore
+            } catch (IllegalArgumentException e) {
+                // ignore
+            }
+        }
+
+        // Store the image in the hash, even if this failed. If it fails now, it will fail later.
+        mImages.put(imageName, image);
+
+        return image;
+    }
+
+    /**
+     * Loads and returns the appropriate image for a given package, archive or source object.
+     * The image is cached. Successive calls will return the <em>same</em> object.
+     *
+     * @param object A {@link SdkSource} or {@link Package} or {@link Archive}.
+     * @return A new or existing {@link Image}. The caller must NOT dispose the image (the
+     *  image will disposed by {@link #dispose()}). The returned image can be null if the
+     *  object is of an unknown type.
+     */
+    public Image getImageForObject(Object object) {
+
+        if (object == null) {
+            return null;
+        }
+
+        if (object instanceof Image) {
+            return (Image) object;
+        }
+
+        String clz = object.getClass().getSimpleName();
+        if (clz.endsWith(Package.class.getSimpleName())) {
+            String name = clz.replaceFirst(Package.class.getSimpleName(), "")   //$NON-NLS-1$
+                             .replace("SystemImage", "sysimg")    //$NON-NLS-1$ //$NON-NLS-2$
+                             .toLowerCase(Locale.US);
+            name += "_pkg_16.png";                                              //$NON-NLS-1$
+            return getImageByName(name);
+        }
+
+        if (object instanceof SdkSourceCategory) {
+            return getImageByName("source_cat_icon_16.png");                     //$NON-NLS-1$
+
+        } else if (object instanceof SdkSource) {
+            return getImageByName("source_icon_16.png");                         //$NON-NLS-1$
+
+        } else if (object instanceof PkgContentProvider.RepoSourceError) {
+            return getImageByName("error_icon_16.png");                       //$NON-NLS-1$
+
+        } else if (object instanceof PkgContentProvider.RepoSourceNotification) {
+            return getImageByName("nopkg_icon_16.png");                       //$NON-NLS-1$
+        }
+
+        if (object instanceof Archive) {
+            if (((Archive) object).isCompatible()) {
+                return getImageByName("archive_icon16.png");                    //$NON-NLS-1$
+            } else {
+                return getImageByName("incompat_icon16.png");                   //$NON-NLS-1$
+            }
+        }
+
+        if (object instanceof String) {
+            return getImageByName((String) object);
+        }
+
+
+        if (object != null) {
+            // For debugging
+            // System.out.println("No image for object " + object.getClass().getSimpleName());
+        }
+
+        return null;
+    }
+
+    /**
+     * Dispose all the images created by this factory so far.
+     */
+    public void dispose() {
+        Iterator<Image> it = mImages.values().iterator();
+        while(it.hasNext()) {
+            Image img = it.next();
+            if (img != null && img.isDisposed() == false) {
+                img.dispose();
+            }
+            it.remove();
+        }
+    }
+
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/accept_icon16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/accept_icon16.png
new file mode 100755
index 0000000..a9483fb
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/accept_icon16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/addon_pkg_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/addon_pkg_16.png
new file mode 100755
index 0000000..ca6a231
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/addon_pkg_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/android_icon_128.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/android_icon_128.png
new file mode 100644
index 0000000..830c04b
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/android_icon_128.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/android_icon_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/android_icon_16.png
new file mode 100644
index 0000000..08ffda8
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/android_icon_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/archive_icon16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/archive_icon16.png
new file mode 100755
index 0000000..be5edd7
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/archive_icon16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/broken_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/broken_16.png
new file mode 100755
index 0000000..945d871
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/broken_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/broken_pkg_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/broken_pkg_16.png
new file mode 100755
index 0000000..6daa67b
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/broken_pkg_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/buildtool_pkg_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/buildtool_pkg_16.png
new file mode 100755
index 0000000..a4cb335
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/buildtool_pkg_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/devman_generic_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/devman_generic_16.png
new file mode 100755
index 0000000..6f59cd4
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/devman_generic_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/devman_manufacturer_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/devman_manufacturer_16.png
new file mode 100755
index 0000000..422276d
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/devman_manufacturer_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/devman_user_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/devman_user_16.png
new file mode 100755
index 0000000..f8a173c
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/devman_user_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/doc_pkg_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/doc_pkg_16.png
new file mode 100755
index 0000000..186b3b1
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/doc_pkg_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/error_icon_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/error_icon_16.png
new file mode 100755
index 0000000..ccb4d0a
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/error_icon_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/extra_pkg_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/extra_pkg_16.png
new file mode 100755
index 0000000..a6529f0
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/extra_pkg_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/incompat_icon16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/incompat_icon16.png
new file mode 100755
index 0000000..2a307e9
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/incompat_icon16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/log_off_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/log_off_16.png
new file mode 100755
index 0000000..c9d7cb7
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/log_off_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/log_on_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/log_on_16.png
new file mode 100755
index 0000000..58f4195
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/log_on_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/nopkg_icon_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/nopkg_icon_16.png
new file mode 100755
index 0000000..147837f
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/nopkg_icon_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_incompat_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_incompat_16.png
new file mode 100755
index 0000000..7ef989e
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_incompat_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_installed_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_installed_16.png
new file mode 100755
index 0000000..78b7e5a
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_installed_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_new_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_new_16.png
new file mode 100755
index 0000000..0976ad4
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_new_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_update_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_update_16.png
new file mode 100755
index 0000000..e766251
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_update_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkgcat_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkgcat_16.png
new file mode 100755
index 0000000..cd9b807
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkgcat_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkgcat_other_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkgcat_other_16.png
new file mode 100755
index 0000000..395a240
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkgcat_other_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/platform_pkg_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/platform_pkg_16.png
new file mode 100755
index 0000000..0b0744b
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/platform_pkg_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/platformtool_pkg_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/platformtool_pkg_16.png
new file mode 100755
index 0000000..606a100
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/platformtool_pkg_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/reject_icon16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/reject_icon16.png
new file mode 100755
index 0000000..b87bbc9
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/reject_icon16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/sample_pkg_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/sample_pkg_16.png
new file mode 100755
index 0000000..8d31865
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/sample_pkg_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/sdkman_logo_128.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/sdkman_logo_128.png
new file mode 100644
index 0000000..0f1670d
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/sdkman_logo_128.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/source_cat_icon_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/source_cat_icon_16.png
new file mode 100755
index 0000000..13c8bb3
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/source_cat_icon_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/source_icon_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/source_icon_16.png
new file mode 100755
index 0000000..5eb1ead
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/source_icon_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/source_pkg_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/source_pkg_16.png
new file mode 100755
index 0000000..9992cda
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/source_pkg_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/status_ok_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/status_ok_16.png
new file mode 100755
index 0000000..eeb0a6f
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/status_ok_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/stop_disabled_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/stop_disabled_16.png
new file mode 100755
index 0000000..ae6da31
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/stop_disabled_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/stop_enabled_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/stop_enabled_16.png
new file mode 100755
index 0000000..7ce1864
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/stop_enabled_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/sysimg_pkg_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/sysimg_pkg_16.png
new file mode 100755
index 0000000..7795c2c
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/sysimg_pkg_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tool_pkg_16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tool_pkg_16.png
new file mode 100755
index 0000000..8ca7710
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tool_pkg_16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/unknown_icon16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/unknown_icon16.png
new file mode 100755
index 0000000..1b97eb7
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/unknown_icon16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/warning_icon16.png b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/warning_icon16.png
new file mode 100755
index 0000000..ca3b6ed
Binary files /dev/null and b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/warning_icon16.png differ
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AddonSitesDialog.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AddonSitesDialog.java
new file mode 100755
index 0000000..b28cccd
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AddonSitesDialog.java
@@ -0,0 +1,574 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository.ui;
+
+import com.android.sdklib.internal.repository.sources.SdkAddonSource;
+import com.android.sdklib.internal.repository.sources.SdkSource;
+import com.android.sdklib.internal.repository.sources.SdkSourceCategory;
+import com.android.sdklib.internal.repository.sources.SdkSourceProperties;
+import com.android.sdklib.internal.repository.sources.SdkSources;
+import com.android.sdklib.internal.repository.sources.SdkSysImgSource;
+import com.android.sdklib.repository.SdkSysImgConstants;
+import com.android.sdkuilib.internal.repository.UpdaterBaseDialog;
+import com.android.sdkuilib.internal.repository.SwtUpdaterData;
+import com.android.sdkuilib.ui.GridDataBuilder;
+import com.android.sdkuilib.ui.GridLayoutBuilder;
+
+import org.eclipse.jface.dialogs.IInputValidator;
+import org.eclipse.jface.dialogs.InputDialog;
+import org.eclipse.jface.viewers.CheckStateChangedEvent;
+import org.eclipse.jface.viewers.CheckboxTableViewer;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.ICheckStateListener;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.MessageBox;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.TabFolder;
+import org.eclipse.swt.widgets.TabItem;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Dialog that displays 2 tabs: <br/>
+ * - one tab with the list of extra add-ons sites defined by the user. <br/>
+ * - one tab with the list of 3rd-party add-ons currently available, which the user can
+ *   deactivate to prevent from loading them.
+ */
+public class AddonSitesDialog extends UpdaterBaseDialog {
+
+    private final SdkSources mSources;
+    private Table mUserTable;
+    private TableViewer mUserTableViewer;
+    private CheckboxTableViewer mSitesTableViewer;
+    private Button mUserButtonNew;
+    private Button mUserButtonDelete;
+    private Button mUserButtonEdit;
+    private Runnable mSourcesChangeListener;
+
+    /**
+     * Create the dialog.
+     *
+     * @param parent The parent's shell
+     * @wbp.parser.entryPoint
+     */
+    public AddonSitesDialog(Shell parent, SwtUpdaterData updaterData) {
+        super(parent, updaterData, "Add-on Sites");
+        mSources = updaterData.getSources();
+        assert mSources != null;
+    }
+
+    /**
+     * Create contents of the dialog.
+     * @wbp.parser.entryPoint
+     */
+    @Override
+    protected void createContents() {
+        super.createContents();
+        Shell shell = getShell();
+        shell.setMinimumSize(new Point(300, 300));
+        shell.setSize(600, 400);
+
+        TabFolder tabFolder = new TabFolder(shell, SWT.NONE);
+        GridDataBuilder.create(tabFolder).fill().grab().hSpan(2);
+
+        TabItem sitesTabItem = new TabItem(tabFolder, SWT.NONE);
+        sitesTabItem.setText("Official Add-on Sites");
+        createTabOfficialSites(tabFolder, sitesTabItem);
+
+        TabItem userTabItem = new TabItem(tabFolder, SWT.NONE);
+        userTabItem.setText("User Defined Sites");
+        createTabUserSites(tabFolder, userTabItem);
+
+        // placeholder for aligning close button
+        Label label = new Label(shell, SWT.NONE);
+        GridDataBuilder.create(label).hFill().hGrab();
+
+        createCloseButton();
+    }
+
+    void createTabOfficialSites(TabFolder tabFolder, TabItem sitesTabItem) {
+        Composite root = new Composite(tabFolder, SWT.NONE);
+        sitesTabItem.setControl(root);
+        GridLayoutBuilder.create(root).columns(3);
+
+        Label label = new Label(root, SWT.NONE);
+        GridDataBuilder.create(label).hGrab().vCenter().hSpan(3);
+        label.setText(
+            "This lets select which official 3rd-party sites you want to load.\n" +
+            "\n" +
+            "These sites are managed by non-Android vendors to provide add-ons and extra packages.\n" +
+            "They are by default all enabled. When you disable one, the SDK Manager will not check the site for new packages."
+        );
+
+        mSitesTableViewer = CheckboxTableViewer.newCheckList(root, SWT.BORDER | SWT.FULL_SELECTION);
+        mSitesTableViewer.setContentProvider(new SourcesContentProvider());
+
+        Table sitesTable = mSitesTableViewer.getTable();
+        sitesTable.setToolTipText("Enable 3rd-Party Site");
+        sitesTable.setLinesVisible(true);
+        sitesTable.setHeaderVisible(true);
+        GridDataBuilder.create(sitesTable).fill().grab().hSpan(3);
+
+        TableViewerColumn columnViewer = new TableViewerColumn(mSitesTableViewer, SWT.NONE);
+        TableColumn column = columnViewer.getColumn();
+        column.setResizable(true);
+        column.setWidth(150);
+        column.setText("Name");
+        columnViewer.setLabelProvider(new ColumnLabelProvider() {
+            @Override
+            public String getText(Object element) {
+                if (element instanceof SdkSource) {
+                    String name = ((SdkSource) element).getUiName();
+                    if (name != null) {
+                        return name;
+                    }
+                    return ((SdkSource) element).getShortDescription();
+                }
+                return super.getText(element);
+            }
+        });
+
+        columnViewer = new TableViewerColumn(mSitesTableViewer, SWT.NONE);
+        column = columnViewer.getColumn();
+        column.setResizable(true);
+        column.setWidth(400);
+        column.setText("URL");
+        columnViewer.setLabelProvider(new ColumnLabelProvider() {
+            @Override
+            public String getText(Object element) {
+                if (element instanceof SdkSource) {
+                    return ((SdkSource) element).getUrl();
+                }
+                return super.getText(element);
+            }
+        });
+
+        mSitesTableViewer.addCheckStateListener(new ICheckStateListener() {
+            @Override
+            public void checkStateChanged(CheckStateChangedEvent event) {
+                on_SitesTableViewer_checkStateChanged(event);
+            }
+        });
+
+        // "enable all" and "disable all" buttons under the table
+        Button selectAll = new Button(root, SWT.NONE);
+        selectAll.setText("Enable All");
+        GridDataBuilder.create(selectAll).hLeft();
+        selectAll.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent event) {
+                on_SitesTableViewer_selectAll();
+            }
+        });
+
+        // placeholder between both buttons
+        label = new Label(root, SWT.NONE);
+        GridDataBuilder.create(label).hFill().hGrab();
+
+        Button deselectAll = new Button(root, SWT.NONE);
+        deselectAll.setText("Disable All");
+        GridDataBuilder.create(deselectAll).hRight();
+        deselectAll.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent event) {
+                on_SitesTableViewer_deselectAll();
+            }
+        });
+    }
+
+    void createTabUserSites(TabFolder tabFolder, TabItem userTabItem) {
+        Composite root = new Composite(tabFolder, SWT.NONE);
+        userTabItem.setControl(root);
+        GridLayoutBuilder.create(root).columns(2);
+
+        Label label = new Label(root, SWT.NONE);
+        GridDataBuilder.create(label).hLeft().vCenter().hSpan(2);
+        label.setText(
+            "This lets you manage a list of user-contributed external add-on sites URLs.\n" +
+            "\n" +
+            "Add-on sites can provide new add-ons and extra packages.\n" +
+            "They cannot provide standard Android platforms, system images or docs.\n" +
+            "Adding a URL here will not allow you to clone an official Android repository."
+        );
+
+        mUserTableViewer = new TableViewer(root, SWT.BORDER | SWT.FULL_SELECTION);
+        mUserTableViewer.setContentProvider(new SourcesContentProvider());
+
+        mUserTableViewer.addPostSelectionChangedListener(new ISelectionChangedListener() {
+            @Override
+            public void selectionChanged(SelectionChangedEvent event) {
+                on_UserTableViewer_selectionChanged(event);
+            }
+        });
+        mUserTable = mUserTableViewer.getTable();
+        mUserTable.setLinesVisible(true);
+        mUserTable.addMouseListener(new MouseAdapter() {
+            @Override
+            public void mouseUp(MouseEvent event) {
+                on_UserTable_mouseUp(event);
+            }
+        });
+        GridDataBuilder.create(mUserTable).fill().grab().vSpan(5);
+
+        TableViewerColumn tableViewerColumn = new TableViewerColumn(mUserTableViewer, SWT.NONE);
+        TableColumn userColumnUrl = tableViewerColumn.getColumn();
+        userColumnUrl.setWidth(100);
+
+        // Implementation detail: set the label provider on the table viewer *after* associating
+        // a column. This will set the label provider on the column for us.
+        mUserTableViewer.setLabelProvider(new LabelProvider());
+
+
+        mUserButtonNew = new Button(root, SWT.NONE);
+        mUserButtonNew.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                userNewOrEdit(false /*isEdit*/);
+            }
+        });
+        GridDataBuilder.create(mUserButtonNew).hFill().vCenter();
+        mUserButtonNew.setText("New...");
+
+        mUserButtonEdit = new Button(root, SWT.NONE);
+        mUserButtonEdit.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                userNewOrEdit(true /*isEdit*/);
+            }
+        });
+        GridDataBuilder.create(mUserButtonEdit).hFill().vCenter();
+        mUserButtonEdit.setText("Edit...");
+
+        mUserButtonDelete = new Button(root, SWT.NONE);
+        mUserButtonDelete.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                on_UserButtonDelete_widgetSelected(e);
+            }
+        });
+        GridDataBuilder.create(mUserButtonDelete).hFill().vCenter();
+        mUserButtonDelete.setText("Delete...");
+
+        adjustColumnsWidth(mUserTable, userColumnUrl);
+    }
+
+    @Override
+    protected void close() {
+        if (mSources != null && mSourcesChangeListener != null) {
+            mSources.removeChangeListener(mSourcesChangeListener);
+        }
+        SdkSourceProperties p = new SdkSourceProperties();
+        p.save();
+        super.close();
+    }
+
+    /**
+     * Adds a listener to adjust the column width when the parent is resized.
+     */
+    private void adjustColumnsWidth(final Table table, final TableColumn column0) {
+        // Add a listener to resize the column to the full width of the table
+        table.addControlListener(new ControlAdapter() {
+            @Override
+            public void controlResized(ControlEvent e) {
+                Rectangle r = table.getClientArea();
+                column0.setWidth(r.width * 100 / 100); // 100%
+            }
+        });
+    }
+
+    private void userNewOrEdit(final boolean isEdit) {
+        final SdkSource[] knownSources = mSources.getAllSources();
+        String title = isEdit ? "Edit Add-on Site URL" : "Add Add-on Site URL";
+        String msg = "Please enter the URL of the addon.xml:";
+        IStructuredSelection sel = (IStructuredSelection) mUserTableViewer.getSelection();
+        final String initialValue = !isEdit || sel.isEmpty() ? null :
+                                                               sel.getFirstElement().toString();
+
+        if (isEdit && initialValue == null) {
+            // Edit with no actual value is not supposed to happen. Ignore this case.
+            return;
+        }
+
+        InputDialog dlg = new InputDialog(
+                getShell(),
+                title,
+                msg,
+                initialValue,
+                new IInputValidator() {
+            @Override
+            public String isValid(String newText) {
+
+                newText = newText == null ? null : newText.trim();
+
+                if (newText == null || newText.length() == 0) {
+                    return "Error: URL field is empty. Please enter a URL.";
+                }
+
+                // A URL should have one of the following prefixes
+                if (!newText.startsWith("file://") &&                 //$NON-NLS-1$
+                        !newText.startsWith("ftp://") &&              //$NON-NLS-1$
+                        !newText.startsWith("http://") &&             //$NON-NLS-1$
+                        !newText.startsWith("https://")) {            //$NON-NLS-1$
+                    return "Error: The URL must start by one of file://, ftp://, http:// or https://";
+                }
+
+                if (isEdit && newText.equals(initialValue)) {
+                    // Edited value hasn't changed. This isn't an error.
+                    return null;
+                }
+
+                // Reject URLs that are already in the source list.
+                // URLs are generally case-insensitive (except for file:// where it all depends
+                // on the current OS so we'll ignore this case.)
+                for (SdkSource s : knownSources) {
+                    if (newText.equalsIgnoreCase(s.getUrl())) {
+                        return "Error: This site is already listed.";
+                    }
+                }
+
+                return null;
+            }
+        });
+
+        if (dlg.open() == Window.OK) {
+            String url = dlg.getValue().trim();
+
+            if (!url.equals(initialValue)) {
+                if (isEdit && initialValue != null) {
+                    // Remove the old value before we add the new one, which is we just
+                    // asserted will be different.
+                    for (SdkSource source : mSources.getSources(SdkSourceCategory.USER_ADDONS)) {
+                        if (initialValue.equals(source.getUrl())) {
+                            mSources.remove(source);
+                            break;
+                        }
+                    }
+
+                }
+
+                // create the source, store it and update the list
+                SdkSource newSource;
+                // use url suffix to decide whether this is a SysImg or Addon;
+                // see SdkSources.loadUserAddons() for another check like this
+                if (url.endsWith(SdkSysImgConstants.URL_DEFAULT_FILENAME)) {
+                     newSource = new SdkSysImgSource(url, null/*uiName*/);
+                } else {
+                     newSource = new SdkAddonSource(url, null/*uiName*/);
+                }
+                mSources.add(SdkSourceCategory.USER_ADDONS, newSource);
+                setReturnValue(true);
+                // notify sources change listeners. This will invoke our own loadUserUrlsList().
+                mSources.notifyChangeListeners();
+
+                // select the new source
+                IStructuredSelection newSel = new StructuredSelection(newSource);
+                mUserTableViewer.setSelection(newSel, true /*reveal*/);
+            }
+        }
+    }
+
+    private void on_UserButtonDelete_widgetSelected(SelectionEvent e) {
+        IStructuredSelection sel = (IStructuredSelection) mUserTableViewer.getSelection();
+        String selectedUrl = sel.isEmpty() ? null : sel.getFirstElement().toString();
+
+        if (selectedUrl == null) {
+            return;
+        }
+
+        MessageBox mb = new MessageBox(getShell(),
+                SWT.YES | SWT.NO | SWT.ICON_QUESTION | SWT.APPLICATION_MODAL);
+        mb.setText("Delete add-on site");
+        mb.setMessage(String.format("Do you want to delete the URL %1$s?", selectedUrl));
+        if (mb.open() == SWT.YES) {
+            for (SdkSource source : mSources.getSources(SdkSourceCategory.USER_ADDONS)) {
+                if (selectedUrl.equals(source.getUrl())) {
+                    mSources.remove(source);
+                    setReturnValue(true);
+                    mSources.notifyChangeListeners();
+                    break;
+                }
+            }
+        }
+    }
+
+    private void on_UserTable_mouseUp(MouseEvent event) {
+        Point p = new Point(event.x, event.y);
+        if (mUserTable.getItem(p) == null) {
+            mUserTable.deselectAll();
+            on_UserTableViewer_selectionChanged(null /*event*/);
+        }
+    }
+
+    private void on_UserTableViewer_selectionChanged(SelectionChangedEvent event) {
+        ISelection sel = mUserTableViewer.getSelection();
+        mUserButtonDelete.setEnabled(!sel.isEmpty());
+        mUserButtonEdit.setEnabled(!sel.isEmpty());
+    }
+
+    private void on_SitesTableViewer_checkStateChanged(CheckStateChangedEvent event) {
+        Object element = event.getElement();
+        if (element instanceof SdkSource) {
+            SdkSource source = (SdkSource) element;
+            boolean isChecked = event.getChecked();
+            if (source.isEnabled() != isChecked) {
+                setReturnValue(true);
+                source.setEnabled(isChecked);
+                mSources.notifyChangeListeners();
+            }
+        }
+    }
+
+    private void on_SitesTableViewer_selectAll() {
+        for (Object item : (Object[]) mSitesTableViewer.getInput()) {
+            if (!mSitesTableViewer.getChecked(item)) {
+                mSitesTableViewer.setChecked(item, true);
+                on_SitesTableViewer_checkStateChanged(
+                        new CheckStateChangedEvent(mSitesTableViewer, item, true));
+            }
+        }
+    }
+
+    private void on_SitesTableViewer_deselectAll() {
+        for (Object item : (Object[]) mSitesTableViewer.getInput()) {
+            if (mSitesTableViewer.getChecked(item)) {
+                mSitesTableViewer.setChecked(item, false);
+                on_SitesTableViewer_checkStateChanged(
+                        new CheckStateChangedEvent(mSitesTableViewer, item, false));
+            }
+        }
+    }
+
+
+    @Override
+    protected void postCreate() {
+        // A runnable to initially load and then update the user urls & sites lists.
+        final Runnable updateInUiThread = new Runnable() {
+            @Override
+            public void run() {
+                loadUserUrlsList();
+                loadSiteUrlsList();
+            }
+        };
+
+        // A listener that runs when the sources have changed.
+        // This is most likely called on a worker thread.
+        mSourcesChangeListener = new Runnable() {
+            @Override
+            public void run() {
+                Shell shell = getShell();
+                if (shell != null) {
+                    Display display = shell.getDisplay();
+                    if (display != null) {
+                        display.syncExec(updateInUiThread);
+                    }
+                }
+            }
+        };
+
+        mSources.addChangeListener(mSourcesChangeListener);
+
+        // initialize the list
+        updateInUiThread.run();
+    }
+
+    private void loadUserUrlsList() {
+        SdkSource[] knownSources = mSources.getSources(SdkSourceCategory.USER_ADDONS);
+        Arrays.sort(knownSources);
+
+        ISelection oldSelection = mUserTableViewer.getSelection();
+
+        mUserTableViewer.setInput(knownSources);
+        mUserTableViewer.refresh();
+        // initialize buttons' state that depend on the list
+        on_UserTableViewer_selectionChanged(null /*event*/);
+
+        if (oldSelection != null && !oldSelection.isEmpty()) {
+            mUserTableViewer.setSelection(oldSelection, true /*reveal*/);
+        }
+    }
+
+    private void loadSiteUrlsList() {
+        SdkSource[] knownSources = mSources.getSources(SdkSourceCategory.ADDONS_3RD_PARTY);
+        Arrays.sort(knownSources);
+
+        ISelection oldSelection = mSitesTableViewer.getSelection();
+
+        mSitesTableViewer.setInput(knownSources);
+        mSitesTableViewer.refresh();
+
+        if (oldSelection != null && !oldSelection.isEmpty()) {
+            mSitesTableViewer.setSelection(oldSelection, true /*reveal*/);
+        }
+
+        // Check the sources which are currently enabled.
+        ArrayList<SdkSource> disabled = new ArrayList<SdkSource>(knownSources.length);
+        for (SdkSource source : knownSources) {
+            if (source.isEnabled()) {
+                disabled.add(source);
+            }
+        }
+        mSitesTableViewer.setCheckedElements(disabled.toArray());
+    }
+
+
+    private static class SourcesContentProvider implements IStructuredContentProvider {
+        @Override
+        public void dispose() {
+            // pass
+        }
+
+        @Override
+        public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+            // pass
+        }
+
+        @Override
+        public Object[] getElements(Object inputElement) {
+            if (inputElement instanceof SdkSource[]) {
+                return (Object[]) inputElement;
+            } else {
+                return new Object[0];
+            }
+        }
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AdtUpdateDialog.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AdtUpdateDialog.java
new file mode 100755
index 0000000..2ffa9a9
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AdtUpdateDialog.java
@@ -0,0 +1,494 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository.ui;
+
+
+import com.android.sdklib.AndroidVersion;
+import com.android.sdklib.internal.repository.packages.ExtraPackage;
+import com.android.sdklib.internal.repository.packages.Package;
+import com.android.sdklib.internal.repository.packages.PlatformPackage;
+import com.android.sdklib.internal.repository.packages.PlatformToolPackage;
+import com.android.sdklib.internal.repository.packages.ToolPackage;
+import com.android.sdklib.internal.repository.sources.SdkSource;
+import com.android.sdklib.internal.repository.updater.PackageLoader;
+import com.android.sdklib.internal.repository.updater.SettingsController;
+import com.android.sdklib.internal.repository.updater.PackageLoader.IAutoInstallTask;
+import com.android.sdkuilib.internal.repository.SwtUpdaterData;
+import com.android.sdkuilib.internal.repository.core.SdkLogAdapter;
+import com.android.sdkuilib.internal.tasks.ProgressView;
+import com.android.sdkuilib.internal.tasks.ProgressViewFactory;
+import com.android.sdkuilib.ui.GridDataBuilder;
+import com.android.sdkuilib.ui.GridLayoutBuilder;
+import com.android.sdkuilib.ui.SwtBaseDialog;
+import com.android.utils.ILogger;
+import com.android.utils.Pair;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.ProgressBar;
+import org.eclipse.swt.widgets.Shell;
+
+import java.io.File;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * This is a private implementation of UpdateWindow for ADT,
+ * designed to install a very specific package.
+ * <p/>
+ * Example of usage:
+ * <pre>
+ * AdtUpdateDialog dialog = new AdtUpdateDialog(
+ *     AdtPlugin.getDisplay().getActiveShell(),
+ *     new AdtConsoleSdkLog(),
+ *     sdk.getSdkLocation());
+ *
+ * Pair<Boolean, File> result = dialog.installExtraPackage(
+ *     "android", "compatibility");  //$NON-NLS-1$ //$NON-NLS-2$
+ * or
+ * Pair<Boolean, File> result = dialog.installPlatformPackage(11);
+ * </pre>
+ */
+public class AdtUpdateDialog extends SwtBaseDialog {
+
+    public static final int USE_MAX_REMOTE_API_LEVEL = 0;
+
+    private static final String APP_NAME = "Android SDK Manager";
+    private final SwtUpdaterData mUpdaterData;
+
+    private Boolean mResultCode = Boolean.FALSE;
+    private Map<Package, File> mResultPaths = null;
+    private SettingsController mSettingsController;
+    private PackageFilter mPackageFilter;
+    private PackageLoader mPackageLoader;
+
+    private ProgressBar mProgressBar;
+    private Label mStatusText;
+
+    /**
+     * Creates a new {@link AdtUpdateDialog}.
+     * Callers will want to call {@link #installExtraPackage} or
+     * {@link #installPlatformPackage} after this.
+     *
+     * @param parentShell The existing parent shell. Must not be null.
+     * @param sdkLog An SDK logger. Must not be null.
+     * @param osSdkRoot The current SDK root OS path. Must not be null or empty.
+     */
+    public AdtUpdateDialog(
+            Shell parentShell,
+            ILogger sdkLog,
+            String osSdkRoot) {
+        super(parentShell, SWT.NONE, APP_NAME);
+        mUpdaterData = new SwtUpdaterData(osSdkRoot, sdkLog);
+    }
+
+    /**
+     * Displays the update dialog and triggers installation of the requested {@code extra}
+     * package with the specified vendor and path attributes.
+     * <p/>
+     * Callers must not try to reuse this dialog after this call.
+     *
+     * @param vendor The extra package vendor string to match.
+     * @param path   The extra package path   string to match.
+     * @return A boolean indicating whether the installation was successful (meaning the package
+     *   was either already present, or got installed or updated properly) and a {@link File}
+     *   with the path to the root folder of the package. The file is null when the boolean
+     *   is false, otherwise it should point to an existing valid folder.
+     * @wbp.parser.entryPoint
+     */
+    public Pair<Boolean, File> installExtraPackage(String vendor, String path) {
+        mPackageFilter = createExtraFilter(vendor, path);
+        open();
+
+        File installPath = null;
+        if (mResultPaths != null) {
+            for (Entry<Package, File> entry : mResultPaths.entrySet()) {
+                if (entry.getKey() instanceof ExtraPackage) {
+                    installPath = entry.getValue();
+                    break;
+                }
+            }
+        }
+
+        return Pair.of(mResultCode, installPath);
+    }
+
+    /**
+     * Displays the update dialog and triggers installation of platform-tools package.
+     * <p/>
+     * Callers must not try to reuse this dialog after this call.
+     *
+     * @return A boolean indicating whether the installation was successful (meaning the package
+     *   was either already present, or got installed or updated properly) and a {@link File}
+     *   with the path to the root folder of the package. The file is null when the boolean
+     *   is false, otherwise it should point to an existing valid folder.
+     * @wbp.parser.entryPoint
+     */
+    public Pair<Boolean, File> installPlatformTools() {
+        mPackageFilter = createPlatformToolsFilter();
+        open();
+
+        File installPath = null;
+        if (mResultPaths != null) {
+            for (Entry<Package, File> entry : mResultPaths.entrySet()) {
+                if (entry.getKey() instanceof ExtraPackage) {
+                    installPath = entry.getValue();
+                    break;
+                }
+            }
+        }
+
+        return Pair.of(mResultCode, installPath);
+    }
+
+    /**
+     * Displays the update dialog and triggers installation of the requested platform
+     * package with the specified API  level.
+     * <p/>
+     * Callers must not try to reuse this dialog after this call.
+     *
+     * @param apiLevel The platform API level to match.
+     *  The special value {@link #USE_MAX_REMOTE_API_LEVEL} means to use
+     *  the highest API level available on the remote repository.
+     * @return A boolean indicating whether the installation was successful (meaning the package
+     *   was either already present, or got installed or updated properly) and a {@link File}
+     *   with the path to the root folder of the package. The file is null when the boolean
+     *   is false, otherwise it should point to an existing valid folder.
+     */
+    public Pair<Boolean, File> installPlatformPackage(int apiLevel) {
+        mPackageFilter = createPlatformFilter(apiLevel);
+        open();
+
+        File installPath = null;
+        if (mResultPaths != null) {
+            for (Entry<Package, File> entry : mResultPaths.entrySet()) {
+                if (entry.getKey() instanceof PlatformPackage) {
+                    installPath = entry.getValue();
+                    break;
+                }
+            }
+        }
+
+        return Pair.of(mResultCode, installPath);
+    }
+
+    /**
+     * Displays the update dialog and triggers installation of a new SDK. This works by
+     * requesting a remote platform package with the specified API levels as well as
+     * the first tools or platform-tools packages available.
+     * <p/>
+     * Callers must not try to reuse this dialog after this call.
+     *
+     * @param apiLevels A set of platform API levels to match.
+     *  The special value {@link #USE_MAX_REMOTE_API_LEVEL} means to use
+     *  the highest API level available in the repository.
+     * @return A boolean indicating whether the installation was successful (meaning the packages
+     *   were either already present, or got installed or updated properly).
+     */
+    public boolean installNewSdk(Set<Integer> apiLevels) {
+        mPackageFilter = createNewSdkFilter(apiLevels);
+        open();
+        return mResultCode.booleanValue();
+    }
+
+    @Override
+    protected void createContents() {
+        Shell shell = getShell();
+        shell.setMinimumSize(new Point(450, 100));
+        shell.setSize(450, 100);
+
+        mUpdaterData.setWindowShell(shell);
+
+        GridLayoutBuilder.create(shell).columns(1);
+
+        Composite composite1 = new Composite(shell, SWT.NONE);
+        composite1.setLayout(new GridLayout(1, false));
+        GridDataBuilder.create(composite1).fill().grab();
+
+        mProgressBar = new ProgressBar(composite1, SWT.NONE);
+        GridDataBuilder.create(mProgressBar).hFill().hGrab();
+
+        mStatusText = new Label(composite1, SWT.NONE);
+        mStatusText.setText("Status Placeholder");  //$NON-NLS-1$ placeholder
+        GridDataBuilder.create(mStatusText).hFill().hGrab();
+    }
+
+    @Override
+    protected void postCreate() {
+        ProgressViewFactory factory = new ProgressViewFactory();
+        factory.setProgressView(new ProgressView(
+                mStatusText,
+                mProgressBar,
+                null /*buttonStop*/,
+                new SdkLogAdapter(mUpdaterData.getSdkLog())));
+        mUpdaterData.setTaskFactory(factory);
+
+        setupSources();
+        initializeSettings();
+
+        if (mUpdaterData.checkIfInitFailed()) {
+            close();
+            return;
+        }
+
+        mUpdaterData.broadcastOnSdkLoaded();
+
+        mPackageLoader = new PackageLoader(mUpdaterData);
+    }
+
+    @Override
+    protected void eventLoop() {
+        mPackageLoader.loadPackagesWithInstallTask(
+                mPackageFilter.installFlags(),
+                new IAutoInstallTask() {
+            @Override
+            public Package[] filterLoadedSource(SdkSource source, Package[] packages) {
+                for (Package pkg : packages) {
+                    mPackageFilter.visit(pkg);
+                }
+                return packages;
+            }
+
+            @Override
+            public boolean acceptPackage(Package pkg) {
+                // Is this the package we want to install?
+                return mPackageFilter.accept(pkg);
+            }
+
+            @Override
+            public void setResult(boolean success, Map<Package, File> installPaths) {
+                // Capture the result from the installation.
+                mResultCode = Boolean.valueOf(success);
+                mResultPaths = installPaths;
+            }
+
+            @Override
+            public void taskCompleted() {
+                // We can close that window now.
+                close();
+            }
+        });
+
+        super.eventLoop();
+    }
+
+    // -- Start of internal part ----------
+    // Hide everything down-below from SWT designer
+    //$hide>>$
+
+    // --- Public API -----------
+
+
+    // --- Internals & UI Callbacks -----------
+
+    /**
+     * Used to initialize the sources.
+     */
+    private void setupSources() {
+        mUpdaterData.setupDefaultSources();
+    }
+
+    /**
+     * Initializes settings.
+     */
+    private void initializeSettings() {
+        mSettingsController = mUpdaterData.getSettingsController();
+        mSettingsController.loadSettings();
+        mSettingsController.applySettings();
+    }
+
+    // ----
+
+    private static abstract class PackageFilter {
+        /** Returns the installer flags for the corresponding mode. */
+        abstract int installFlags();
+
+        /** Visit a new package definition, in case we need to adjust the filter dynamically. */
+        abstract void visit(Package pkg);
+
+        /** Checks whether this is the package we've been looking for. */
+        abstract boolean accept(Package pkg);
+    }
+
+    public static PackageFilter createExtraFilter(
+            final String vendor,
+            final String path) {
+        return new PackageFilter() {
+            String mVendor = vendor;
+            String mPath = path;
+
+            @Override
+            boolean accept(Package pkg) {
+                if (pkg instanceof ExtraPackage) {
+                    ExtraPackage ep = (ExtraPackage) pkg;
+                    if (ep.getVendorId().equals(mVendor)) {
+                        // Check actual extra <path> field first
+                        if (ep.getPath().equals(mPath)) {
+                            return true;
+                        }
+                        // If not, check whether this is one of the <old-paths> values.
+                        for (String oldPath : ep.getOldPaths()) {
+                            if (oldPath.equals(mPath)) {
+                                return true;
+                            }
+                        }
+                    }
+                }
+                return false;
+            }
+
+            @Override
+            void visit(Package pkg) {
+                // nop
+            }
+
+            @Override
+            int installFlags() {
+                return SwtUpdaterData.TOOLS_MSG_UPDATED_FROM_ADT;
+            }
+        };
+    }
+
+    private PackageFilter createPlatformToolsFilter() {
+        return new PackageFilter() {
+            @Override
+            boolean accept(Package pkg) {
+                return pkg instanceof PlatformToolPackage;
+            }
+
+            @Override
+            void visit(Package pkg) {
+                // nop
+            }
+
+            @Override
+            int installFlags() {
+                return SwtUpdaterData.TOOLS_MSG_UPDATED_FROM_ADT;
+            }
+        };
+    }
+
+    public static PackageFilter createPlatformFilter(final int apiLevel) {
+        return new PackageFilter() {
+            int mApiLevel = apiLevel;
+            boolean mFindMaxApi = apiLevel == USE_MAX_REMOTE_API_LEVEL;
+
+            @Override
+            boolean accept(Package pkg) {
+                if (pkg instanceof PlatformPackage) {
+                    PlatformPackage pp = (PlatformPackage) pkg;
+                    AndroidVersion v = pp.getAndroidVersion();
+                    return !v.isPreview() && v.getApiLevel() == mApiLevel;
+                }
+                return false;
+            }
+
+            @Override
+            void visit(Package pkg) {
+                // Try to find the max API in all remote packages
+                if (mFindMaxApi &&
+                        pkg instanceof PlatformPackage &&
+                        !pkg.isLocal()) {
+                    PlatformPackage pp = (PlatformPackage) pkg;
+                    AndroidVersion v = pp.getAndroidVersion();
+                    if (!v.isPreview()) {
+                        int api = v.getApiLevel();
+                        if (api > mApiLevel) {
+                            mApiLevel = api;
+                        }
+                    }
+                }
+            }
+
+            @Override
+            int installFlags() {
+                return SwtUpdaterData.TOOLS_MSG_UPDATED_FROM_ADT;
+            }
+        };
+    }
+
+    public static PackageFilter createNewSdkFilter(final Set<Integer> apiLevels) {
+        return new PackageFilter() {
+            int mMaxApiLevel;
+            boolean mFindMaxApi = apiLevels.contains(USE_MAX_REMOTE_API_LEVEL);
+            boolean mNeedTools = true;
+            boolean mNeedPlatformTools = true;
+
+            @Override
+            boolean accept(Package pkg) {
+                if (!pkg.isLocal()) {
+                    if (pkg instanceof PlatformPackage) {
+                        PlatformPackage pp = (PlatformPackage) pkg;
+                        AndroidVersion v = pp.getAndroidVersion();
+                        if (!v.isPreview()) {
+                            int level = v.getApiLevel();
+                            if ((mFindMaxApi && level == mMaxApiLevel) ||
+                                    (level > 0 && apiLevels.contains(level))) {
+                                return true;
+                            }
+                        }
+                    } else if (mNeedTools && pkg instanceof ToolPackage) {
+                        // We want a tool package. There should be only one,
+                        // but in case of error just take the first one.
+                        mNeedTools = false;
+                        return true;
+                    } else if (mNeedPlatformTools && pkg instanceof PlatformToolPackage) {
+                        // We want a platform-tool package. There should be only one,
+                        // but in case of error just take the first one.
+                        mNeedPlatformTools = false;
+                        return true;
+                    }
+                }
+                return false;
+            }
+
+            @Override
+            void visit(Package pkg) {
+                // Try to find the max API in all remote packages
+                if (mFindMaxApi &&
+                        pkg instanceof PlatformPackage &&
+                        !pkg.isLocal()) {
+                    PlatformPackage pp = (PlatformPackage) pkg;
+                    AndroidVersion v = pp.getAndroidVersion();
+                    if (!v.isPreview()) {
+                        int api = v.getApiLevel();
+                        if (api > mMaxApiLevel) {
+                            mMaxApiLevel = api;
+                        }
+                    }
+                }
+            }
+
+            @Override
+            int installFlags() {
+                return SwtUpdaterData.NO_TOOLS_MSG;
+            }
+        };
+    }
+
+
+
+    // End of hiding from SWT Designer
+    //$hide<<$
+
+    // -----
+
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerPage.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerPage.java
new file mode 100755
index 0000000..f27cbcb
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerPage.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository.ui;
+
+import com.android.prefs.AndroidLocation.AndroidLocationException;
+import com.android.sdklib.devices.DeviceManager;
+import com.android.sdklib.devices.DeviceManager.DevicesChangedListener;
+import com.android.sdklib.internal.avd.AvdInfo;
+import com.android.sdklib.internal.avd.AvdManager;
+import com.android.sdklib.repository.ISdkChangeListener;
+import com.android.sdkuilib.internal.repository.SwtUpdaterData;
+import com.android.sdkuilib.internal.widgets.AvdSelector;
+import com.android.sdkuilib.internal.widgets.AvdSelector.DisplayMode;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+
+/**
+ * An Update page displaying AVD Manager entries.
+ * This is the sole page displayed by {@link AvdManagerWindowImpl1}.
+ *
+ * Note: historically the SDK Manager was a single window with several sub-pages and a tab
+ * switcher. For simplicity each page was separated in its own window. The AVD Manager is
+ * thus composed of the {@link AvdManagerWindowImpl1} (the window shell itself) and this
+ * page displays the actually list of AVDs and various action buttons.
+ */
+public class AvdManagerPage extends Composite
+    implements ISdkChangeListener, DevicesChangedListener, DisposeListener {
+
+    private AvdSelector mAvdSelector;
+
+    private final SwtUpdaterData mSwtUpdaterData;
+    private final DeviceManager mDeviceManager;
+    /**
+     * Create the composite.
+     * @param parent The parent of the composite.
+     * @param swtUpdaterData An instance of {@link SwtUpdaterData}.
+     */
+    public AvdManagerPage(Composite parent,
+            int swtStyle,
+            SwtUpdaterData swtUpdaterData,
+            DeviceManager deviceManager) {
+        super(parent, swtStyle);
+
+        mSwtUpdaterData = swtUpdaterData;
+        mSwtUpdaterData.addListeners(this);
+
+        mDeviceManager = deviceManager;
+        mDeviceManager.registerListener(this);
+
+        createContents(this);
+        postCreate();  //$hide$
+    }
+
+    private void createContents(Composite parent) {
+        parent.setLayout(new GridLayout(1, false));
+
+        Label label = new Label(parent, SWT.NONE);
+        label.setLayoutData(new GridData());
+
+        try {
+            if (mSwtUpdaterData != null && mSwtUpdaterData.getAvdManager() != null) {
+                label.setText(String.format(
+                        "List of existing Android Virtual Devices located at %s",
+                        mSwtUpdaterData.getAvdManager().getBaseAvdFolder()));
+            } else {
+                label.setText("Error: cannot find the AVD folder location.\r\n Please set the 'ANDROID_SDK_HOME' env variable.");
+            }
+        } catch (AndroidLocationException e) {
+            label.setText(e.getMessage());
+        }
+
+        mAvdSelector = new AvdSelector(parent,
+                mSwtUpdaterData.getOsSdkRoot(),
+                mSwtUpdaterData.getAvdManager(),
+                DisplayMode.MANAGER,
+                mSwtUpdaterData.getSdkLog());
+        mAvdSelector.setSettingsController(mSwtUpdaterData.getSettingsController());
+    }
+
+    @Override
+    public void widgetDisposed(DisposeEvent e) {
+        dispose();
+    }
+
+    @Override
+    public void dispose() {
+        mSwtUpdaterData.removeListener(this);
+        mDeviceManager.unregisterListener(this);
+        super.dispose();
+    }
+
+    @Override
+    protected void checkSubclass() {
+        // Disable the check that prevents subclassing of SWT components
+    }
+
+    public void selectAvd(AvdInfo avdInfo, boolean reloadAvdList) {
+        if (reloadAvdList) {
+            mAvdSelector.refresh(true /*reload*/);
+
+            // Reloading the AVDs created new objects, so the reference to avdInfo
+            // will never be selected. Instead reselect it based on its unique name.
+            AvdManager am = mSwtUpdaterData.getAvdManager();
+            avdInfo = am.getAvd(avdInfo.getName(), false /*validAvdOnly*/);
+        }
+        mAvdSelector.setSelection(avdInfo);
+    }
+
+    // -- Start of internal part ----------
+    // Hide everything down-below from SWT designer
+    //$hide>>$
+
+    /**
+     * Called by the constructor right after {@link #createContents(Composite)}.
+     */
+    private void postCreate() {
+        // nothing to be done for now.
+    }
+
+    // --- Implementation of ISdkChangeListener ---
+
+    @Override
+    public void onSdkLoaded() {
+        onSdkReload();
+    }
+
+    @Override
+    public void onSdkReload() {
+        mAvdSelector.refresh(false /*reload*/);
+    }
+
+    @Override
+    public void preInstallHook() {
+        // nothing to be done for now.
+    }
+
+    @Override
+    public void postInstallHook() {
+        // nothing to be done for now.
+    }
+
+    // --- Implementation of DevicesChangeListener
+
+    @Override
+    public void onDevicesChanged() {
+        mAvdSelector.refresh(false /*reload*/);
+    }
+
+
+    // End of hiding from SWT Designer
+    //$hide<<$
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerWindowImpl1.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerWindowImpl1.java
new file mode 100755
index 0000000..b8dc9ae
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerWindowImpl1.java
@@ -0,0 +1,411 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository.ui;
+
+
+import com.android.SdkConstants;
+import com.android.sdklib.devices.DeviceManager;
+import com.android.sdklib.internal.avd.AvdInfo;
+import com.android.sdklib.internal.repository.ITaskFactory;
+import com.android.sdklib.internal.repository.updater.SettingsController;
+import com.android.sdklib.repository.ISdkChangeListener;
+import com.android.sdkuilib.internal.repository.AboutDialog;
+import com.android.sdkuilib.internal.repository.MenuBarWrapper;
+import com.android.sdkuilib.internal.repository.SettingsDialog;
+import com.android.sdkuilib.internal.repository.SwtUpdaterData;
+import com.android.sdkuilib.internal.repository.icons.ImageFactory;
+import com.android.sdkuilib.internal.repository.ui.DeviceManagerPage.IAvdCreatedListener;
+import com.android.sdkuilib.repository.AvdManagerWindow.AvdInvocationContext;
+import com.android.sdkuilib.repository.SdkUpdaterWindow;
+import com.android.sdkuilib.ui.GridDataBuilder;
+import com.android.sdkuilib.ui.GridLayoutBuilder;
+import com.android.utils.ILogger;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.TabFolder;
+import org.eclipse.swt.widgets.TabItem;
+
+/**
+ * This is an intermediate version of the {@link AvdManagerPage}
+ * wrapped in its own standalone window for use from the SDK Manager 2.
+ */
+public class AvdManagerWindowImpl1 {
+
+    private static final String APP_NAME = "Android Virtual Device Manager";
+    private static final String APP_NAME_MAC_MENU = "AVD Manager";
+    private static final String SIZE_POS_PREFIX = "avdman1"; //$NON-NLS-1$
+
+    private final Shell mParentShell;
+    private final AvdInvocationContext mContext;
+    /** Internal data shared between the window and its pages. */
+    private final SwtUpdaterData mSwtUpdaterData;
+    /** True if this window created the UpdaterData, in which case it needs to dispose it. */
+    private final boolean mOwnUpdaterData;
+    private final DeviceManager mDeviceManager;
+
+
+    // --- UI members ---
+
+    protected Shell mShell;
+    private AvdManagerPage mAvdPage;
+    private SettingsController mSettingsController;
+    private TabFolder mTabFolder;
+
+    /**
+     * Creates a new window. Caller must call open(), which will block.
+     *
+     * @param parentShell Parent shell.
+     * @param sdkLog Logger. Cannot be null.
+     * @param osSdkRoot The OS path to the SDK root.
+     * @param context The {@link AvdInvocationContext} to change the behavior depending on who's
+     *  opening the SDK Manager.
+     */
+    public AvdManagerWindowImpl1(
+            Shell parentShell,
+            ILogger sdkLog,
+            String osSdkRoot,
+            AvdInvocationContext context) {
+        mParentShell = parentShell;
+        mContext = context;
+        mSwtUpdaterData = new SwtUpdaterData(osSdkRoot, sdkLog);
+        mOwnUpdaterData = true;
+        mDeviceManager = DeviceManager.createInstance(osSdkRoot, sdkLog);
+    }
+
+    /**
+     * Creates a new window. Caller must call open(), which will block.
+     * <p/>
+     * This is to be used when the window is opened from {@link SdkUpdaterWindowImpl2}
+     * to share the same {@link SwtUpdaterData} structure.
+     *
+     * @param parentShell Parent shell.
+     * @param swtUpdaterData The parent's updater data.
+     * @param context The {@link AvdInvocationContext} to change the behavior depending on who's
+     *  opening the SDK Manager.
+     */
+    public AvdManagerWindowImpl1(
+            Shell parentShell,
+            SwtUpdaterData swtUpdaterData,
+            AvdInvocationContext context) {
+        mParentShell = parentShell;
+        mContext = context;
+        mSwtUpdaterData = swtUpdaterData;
+        mOwnUpdaterData = false;
+        mDeviceManager = DeviceManager.createInstance(mSwtUpdaterData.getOsSdkRoot(),
+                                                      mSwtUpdaterData.getSdkLog());
+    }
+
+    /**
+     * Opens the window.
+     * @wbp.parser.entryPoint
+     */
+    public void open() {
+        if (mParentShell == null) {
+            Display.setAppName(APP_NAME); //$hide$ (hide from SWT designer)
+        }
+
+        createShell();
+        preCreateContent();
+        createContents();
+        createMenuBar();
+        mShell.open();
+        mShell.layout();
+
+        boolean ok = postCreateContent();
+
+        if (ok && mContext == AvdInvocationContext.STANDALONE) {
+            Display display = Display.getDefault();
+            while (!mShell.isDisposed()) {
+                if (!display.readAndDispatch()) {
+                    display.sleep();
+                }
+            }
+
+            dispose();  //$hide$
+        }
+    }
+
+    private void createShell() {
+        // The AVD Manager must use a shell trim when standalone
+        // or a dialog trim when invoked from somewhere else.
+        int style = SWT.SHELL_TRIM;
+        if (mContext != AvdInvocationContext.STANDALONE) {
+            style |= SWT.APPLICATION_MODAL;
+        }
+
+        mShell = new Shell(mParentShell, style);
+        mShell.addDisposeListener(new DisposeListener() {
+            @Override
+            public void widgetDisposed(DisposeEvent e) {
+                ShellSizeAndPos.saveSizeAndPos(mShell, SIZE_POS_PREFIX);    //$hide$
+                onAndroidSdkUpdaterDispose();                               //$hide$
+                mAvdPage.dispose();                                         //$hide$
+            }
+        });
+
+        GridLayout glShell = new GridLayout(2, false);
+        mShell.setLayout(glShell);
+
+        mShell.setMinimumSize(new Point(500, 300));
+        mShell.setSize(700, 500);
+        mShell.setText(APP_NAME);
+
+        ShellSizeAndPos.loadSizeAndPos(mShell, SIZE_POS_PREFIX);
+    }
+
+    private void createContents() {
+
+        mTabFolder = new TabFolder(mShell, SWT.NONE);
+        GridDataBuilder.create(mTabFolder).fill().grab().hSpan(2);
+
+        // avd tab
+        TabItem avdTabItem = new TabItem(mTabFolder, SWT.NONE);
+        avdTabItem.setText("Android Virtual Devices");
+        createAvdTab(mTabFolder, avdTabItem);
+
+        // device tab
+        TabItem devTabItem = new TabItem(mTabFolder, SWT.NONE);
+        devTabItem.setText("Device Definitions");
+        createDeviceTab(mTabFolder, devTabItem);
+    }
+
+    private void createAvdTab(TabFolder tabFolder, TabItem avdTabItem) {
+        Composite root = new Composite(tabFolder, SWT.NONE);
+        avdTabItem.setControl(root);
+        GridLayoutBuilder.create(root).columns(1);
+
+        mAvdPage = new AvdManagerPage(root, SWT.NONE, mSwtUpdaterData, mDeviceManager);
+        GridDataBuilder.create(mAvdPage).fill().grab();
+    }
+
+    private void createDeviceTab(TabFolder tabFolder, TabItem devTabItem) {
+        Composite root = new Composite(tabFolder, SWT.NONE);
+        devTabItem.setControl(root);
+        GridLayoutBuilder.create(root).columns(1);
+
+        DeviceManagerPage devicePage =
+            new DeviceManagerPage(root, SWT.NONE, mSwtUpdaterData, mDeviceManager);
+        GridDataBuilder.create(devicePage).fill().grab();
+
+        devicePage.setAvdCreatedListener(new IAvdCreatedListener() {
+            @Override
+            public void onAvdCreated(AvdInfo avdInfo) {
+                if (avdInfo != null) {
+                    mTabFolder.setSelection(0);      // display mAvdPage
+                    mAvdPage.selectAvd(avdInfo, true /*reloadAvdList*/);
+                }
+            }
+        });
+    }
+
+    @SuppressWarnings("unused")
+    // MenuBarWrapper works using side effects
+    private void createMenuBar() {
+        Menu menuBar = new Menu(mShell, SWT.BAR);
+        mShell.setMenuBar(menuBar);
+
+        // Only create the tools menu when running as standalone.
+        // We don't need the tools menu when invoked from the IDE, or the SDK Manager
+        // or from the AVD Chooser dialog. The only point of the tools menu is to
+        // get the about box, and invoke Tools > SDK Manager, which we don't
+        // need to do in these cases.
+        if (mContext == AvdInvocationContext.STANDALONE) {
+
+            MenuItem menuBarTools = new MenuItem(menuBar, SWT.CASCADE);
+            menuBarTools.setText("Tools");
+
+            Menu menuTools = new Menu(menuBarTools);
+            menuBarTools.setMenu(menuTools);
+
+            MenuItem manageSdk = new MenuItem(menuTools, SWT.NONE);
+            manageSdk.setText("Manage SDK...");
+            manageSdk.addSelectionListener(new SelectionAdapter() {
+                @Override
+                public void widgetSelected(SelectionEvent event) {
+                    onSdkManager();
+                }
+            });
+
+            try {
+                new MenuBarWrapper(APP_NAME_MAC_MENU, menuTools) {
+                    @Override
+                    public void onPreferencesMenuSelected() {
+                        SettingsDialog sd = new SettingsDialog(mShell, mSwtUpdaterData);
+                        sd.open();
+                    }
+
+                    @Override
+                    public void onAboutMenuSelected() {
+                        AboutDialog ad = new AboutDialog(mShell, mSwtUpdaterData);
+                        ad.open();
+                    }
+
+                    @Override
+                    public void printError(String format, Object... args) {
+                        if (mSwtUpdaterData != null) {
+                            mSwtUpdaterData.getSdkLog().error(null, format, args);
+                        }
+                    }
+                };
+            } catch (Throwable e) {
+                mSwtUpdaterData.getSdkLog().error(e, "Failed to setup menu bar");
+                e.printStackTrace();
+            }
+        }
+    }
+
+
+    // -- Start of internal part ----------
+    // Hide everything down-below from SWT designer
+    //$hide>>$
+
+    // --- Public API -----------
+
+    /**
+     * Adds a new listener to be notified when a change is made to the content of the SDK.
+     */
+    public void addListener(ISdkChangeListener listener) {
+        mSwtUpdaterData.addListeners(listener);
+    }
+
+    /**
+     * Removes a new listener to be notified anymore when a change is made to the content of
+     * the SDK.
+     */
+    public void removeListener(ISdkChangeListener listener) {
+        mSwtUpdaterData.removeListener(listener);
+    }
+
+    // --- Internals & UI Callbacks -----------
+
+    /**
+     * Called before the UI is created.
+     */
+    private void preCreateContent() {
+        mSwtUpdaterData.setWindowShell(mShell);
+        // We need the UI factory to create the UI
+        mSwtUpdaterData.setImageFactory(new ImageFactory(mShell.getDisplay()));
+        // Note: we can't create the TaskFactory yet because we need the UI
+        // to be created first, so this is done in postCreateContent().
+    }
+
+    /**
+     * Once the UI has been created, initializes the content.
+     * This creates the pages, selects the first one, setup sources and scan for local folders.
+     *
+     * Returns true if we should show the window.
+     */
+    private boolean postCreateContent() {
+        setWindowImage(mShell);
+
+        setupSources();
+        initializeSettings();
+
+        if (mSwtUpdaterData.checkIfInitFailed()) {
+            return false;
+        }
+
+        mSwtUpdaterData.broadcastOnSdkLoaded();
+
+        return true;
+    }
+
+    /**
+     * Creates the icon of the window shell.
+     *
+     * @param shell The shell on which to put the icon
+     */
+    private void setWindowImage(Shell shell) {
+        String imageName = "android_icon_16.png"; //$NON-NLS-1$
+        if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_DARWIN) {
+            imageName = "android_icon_128.png";
+        }
+
+        if (mSwtUpdaterData != null) {
+            ImageFactory imgFactory = mSwtUpdaterData.getImageFactory();
+            if (imgFactory != null) {
+                shell.setImage(imgFactory.getImageByName(imageName));
+            }
+        }
+    }
+
+    /**
+     * Called by the main loop when the window has been disposed.
+     */
+    private void dispose() {
+        mSwtUpdaterData.getSources().saveUserAddons(mSwtUpdaterData.getSdkLog());
+    }
+
+    /**
+     * Callback called when the window shell is disposed.
+     */
+    private void onAndroidSdkUpdaterDispose() {
+        if (mOwnUpdaterData && mSwtUpdaterData != null) {
+            ImageFactory imgFactory = mSwtUpdaterData.getImageFactory();
+            if (imgFactory != null) {
+                imgFactory.dispose();
+            }
+        }
+    }
+
+    /**
+     * Used to initialize the sources.
+     */
+    private void setupSources() {
+        mSwtUpdaterData.setupDefaultSources();
+    }
+
+    /**
+     * Initializes settings.
+     * This must be called after addExtraPages(), which created a settings page.
+     * Iterate through all the pages to find the first (and supposedly unique) setting page,
+     * and use it to load and apply these settings.
+     */
+    private void initializeSettings() {
+        mSettingsController = mSwtUpdaterData.getSettingsController();
+        mSettingsController.loadSettings();
+        mSettingsController.applySettings();
+    }
+
+    private void onSdkManager() {
+        ITaskFactory oldFactory = mSwtUpdaterData.getTaskFactory();
+
+        try {
+            SdkUpdaterWindowImpl2 win = new SdkUpdaterWindowImpl2(
+                    mShell,
+                    mSwtUpdaterData,
+                    SdkUpdaterWindow.SdkInvocationContext.AVD_MANAGER);
+
+            win.open();
+        } catch (Exception e) {
+            mSwtUpdaterData.getSdkLog().error(e, "SDK Manager window error");
+        } finally {
+            mSwtUpdaterData.setTaskFactory(oldFactory);
+        }
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/DeviceManagerPage.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/DeviceManagerPage.java
new file mode 100755
index 0000000..041af8e
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/DeviceManagerPage.java
@@ -0,0 +1,832 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository.ui;
+
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.devices.DeviceManager;
+import com.android.sdklib.devices.DeviceManager.DevicesChangedListener;
+import com.android.sdklib.devices.Hardware;
+import com.android.sdklib.devices.Screen;
+import com.android.sdklib.devices.Storage;
+import com.android.sdklib.devices.Storage.Unit;
+import com.android.sdklib.internal.avd.AvdInfo;
+import com.android.sdklib.repository.ISdkChangeListener;
+import com.android.sdkuilib.internal.repository.SwtUpdaterData;
+import com.android.sdkuilib.internal.repository.icons.ImageFactory;
+import com.android.sdkuilib.internal.widgets.AvdCreationDialog;
+import com.android.sdkuilib.internal.widgets.AvdSelector;
+import com.android.sdkuilib.internal.widgets.DeviceCreationDialog;
+import com.android.sdkuilib.ui.GridDataBuilder;
+import com.android.sdkuilib.ui.GridLayoutBuilder;
+
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.graphics.Resource;
+import org.eclipse.swt.graphics.TextLayout;
+import org.eclipse.swt.graphics.TextStyle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.TableItem;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A page displaying Device Manager entries.
+ * <p/>
+ * This is displayed as a second tab in the AVD Manager window.
+ * The layout purposely matches the one from {@link AvdManagerPage} and {@link AvdSelector}
+ * so that there's a good consistency when switching tabs.
+ * The table displays a few properties of each device as well as actions to edit/add/delete
+ * devices and a button to create an AVD from a given device.
+ *
+ * Non-goals: this tries to keep it simple for a first iteration. Possible enhancements:
+ * - a way to sort the device list by name, manufacturer or screen size.
+ * - possibly a tree organized by manufacturer.
+ * - a filter box to do a string search on any part of the display.
+ */
+public class DeviceManagerPage extends Composite
+    implements ISdkChangeListener, DevicesChangedListener, DisposeListener {
+
+    public interface IAvdCreatedListener {
+        public void onAvdCreated(AvdInfo createdAvdInfo);
+    }
+
+    private final SwtUpdaterData mSwtUpdaterData;
+    private final DeviceManager mDeviceManager;
+    private Table mTable;
+    private Button mNewButton;
+    private Button mEditButton;
+    private Button mDeleteButton;
+    private Button mNewAvdButton;
+    private Button mRefreshButton;
+    private ImageFactory mImageFactory;
+    private Image mUserImage;
+    private Image mGenericImage;
+    private Image mOtherImage;
+    private int mImageWidth;
+    private boolean mDisableRefresh;
+    private IAvdCreatedListener mAvdCreatedListener;
+
+    /**
+     * Create the composite.
+     * @param parent The parent of the composite.
+     * @param swtUpdaterData An instance of {@link SwtUpdaterData}.
+     */
+    public DeviceManagerPage(Composite parent,
+            int swtStyle,
+            SwtUpdaterData swtUpdaterData,
+            DeviceManager deviceManager) {
+        super(parent, swtStyle);
+
+        mSwtUpdaterData = swtUpdaterData;
+        mSwtUpdaterData.addListeners(this);
+
+        mDeviceManager = deviceManager;
+        mDeviceManager.registerListener(this);
+
+        createContents(this);
+        postCreate();  //$hide$
+    }
+
+    public void setAvdCreatedListener(IAvdCreatedListener avdCreatedListener) {
+        mAvdCreatedListener = avdCreatedListener;
+    }
+
+    private void createContents(Composite parent) {
+
+        // get some bitmaps.
+        mImageFactory = new ImageFactory(parent.getDisplay());
+        mUserImage = mImageFactory.getImageByName("devman_user_16.png");
+        mGenericImage = mImageFactory.getImageByName("devman_generic_16.png");
+        mOtherImage = mImageFactory.getImageByName("devman_manufacturer_16.png");
+        mImageWidth = Math.max(mGenericImage.getImageData().width,
+                        Math.max(mUserImage.getImageData().width,
+                                  mOtherImage.getImageData().width));
+
+        // Layout has 2 columns
+        GridLayoutBuilder.create(parent).columns(2);
+
+        // Insert a top label explanation. This matches the design in AvdManagerPage so
+        // that the table starts at the same height on both tabs.
+        Label label = new Label(parent, SWT.NONE);
+        label.setText("List of known device definitions. This can later be used to create Android Virtual Devices.");
+        GridDataBuilder.create(label).hSpan(2);
+
+        // Device table.
+        mTable = new Table(parent, SWT.FULL_SELECTION | SWT.SINGLE | SWT.BORDER);
+        mTable.setHeaderVisible(true);
+        mTable.setLinesVisible(true);
+        mTable.setFont(parent.getFont());
+        setTableHeightHint(30);
+
+        // Buttons on the side.
+        Composite buttons = new Composite(parent, SWT.NONE);
+        GridLayoutBuilder.create(buttons).columns(1).noMargins();
+        GridDataBuilder.create(buttons).vFill();
+        buttons.setFont(parent.getFont());
+
+        mNewButton = new Button(buttons, SWT.PUSH | SWT.FLAT);
+        mNewButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mNewButton.setText("New Device...");
+        mNewButton.setToolTipText("Creates a new user device definition.");
+        mNewButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                onNewDevice();
+            }
+        });
+
+        mEditButton = new Button(buttons, SWT.PUSH | SWT.FLAT);
+        mEditButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mEditButton.setText("Edit...");
+        mEditButton.setToolTipText("Edit an existing device definition.");
+        mEditButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                onEditDevice();
+            }
+        });
+
+        mDeleteButton = new Button(buttons, SWT.PUSH | SWT.FLAT);
+        mDeleteButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mDeleteButton.setText("Delete...");
+        mDeleteButton.setToolTipText("Deletes the selected AVD.");
+        mDeleteButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                onDeleteDevice();
+            }
+        });
+
+        @SuppressWarnings("unused")
+        Label spacing = new Label(buttons, SWT.NONE);
+
+        mNewAvdButton = new Button(buttons, SWT.PUSH | SWT.FLAT);
+        mNewAvdButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mNewAvdButton.setText("Create AVD...");
+        mNewAvdButton.setToolTipText("Creates a new AVD based on this device definition.");
+        mNewAvdButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                onCreateAvd();
+            }
+        });
+
+        Composite padding = new Composite(buttons, SWT.NONE);
+        padding.setLayoutData(new GridData(GridData.FILL_VERTICAL));
+
+        mRefreshButton = new Button(buttons, SWT.PUSH | SWT.FLAT);
+        mRefreshButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mRefreshButton.setText("Refresh");
+        mRefreshButton.setToolTipText("Reloads the list of devices.");
+        mRefreshButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                onRefresh();
+            }
+        });
+
+        // Legend at the bottom.
+        // This matches the one on AvdSelector so that the table height in the tab be similar.
+        Composite legend = new Composite(parent, SWT.NONE);
+        GridLayoutBuilder.create(legend).columns(4).noMargins();
+        GridDataBuilder.create(legend).hFill().vTop().hGrab().hSpan(2);
+        legend.setFont(parent.getFont());
+
+        new Label(legend, SWT.NONE).setImage(mUserImage);
+        new Label(legend, SWT.NONE).setText("A user-created device definition.");
+        new Label(legend, SWT.NONE).setImage(mGenericImage);
+        new Label(legend, SWT.NONE).setText("A generic device definition.");
+        Label icon = new Label(legend, SWT.NONE);
+        icon.setImage(mOtherImage);
+        Label l = new Label(legend, SWT.NONE);
+        l.setText("A manufacturer-specific device definition.");
+        GridData gd;
+        l.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+        gd.horizontalSpan = 3;
+        icon.setVisible(false);
+        l.setVisible(false);
+
+        // create the table columns
+        final TableColumn column0 = new TableColumn(mTable, SWT.NONE);
+        column0.setText("Device");
+
+        adjustColumnsWidth(mTable, column0);
+        setupSelectionListener(mTable);
+        fillTable(mTable);
+        updateButtonStates();
+        setEnabled(true);
+    }
+
+    private void adjustColumnsWidth(final Table table, final TableColumn column0) {
+        // Add a listener to resize the column to the full width of the table
+        table.addControlListener(new ControlAdapter() {
+            @Override
+            public void controlResized(ControlEvent e) {
+                Rectangle r = table.getClientArea();
+                column0.setWidth(r.width * 100 / 100 - 1); // 100%
+            }
+        });
+    }
+
+    private void setupSelectionListener(Table table) {
+        // TODO Auto-generated method stub
+
+    }
+
+    /**
+     * Sets the table grid layout data.
+     *
+     * @param heightHint If > 0, the height hint is set to the requested value.
+     */
+    public void setTableHeightHint(int heightHint) {
+        GridData data = new GridData();
+        if (heightHint > 0) {
+            data.heightHint = heightHint;
+        }
+        data.grabExcessVerticalSpace = true;
+        data.grabExcessHorizontalSpace = true;
+        data.horizontalAlignment = GridData.FILL;
+        data.verticalAlignment = GridData.FILL;
+        mTable.setLayoutData(data);
+    }
+
+    @Override
+    public void widgetDisposed(DisposeEvent e) {
+        dispose();
+    }
+
+    @Override
+    public void dispose() {
+        mSwtUpdaterData.removeListener(this);
+        mDeviceManager.unregisterListener(this);
+        super.dispose();
+    }
+
+    @Override
+    protected void checkSubclass() {
+        // Disable the check that prevents subclassing of SWT components
+    }
+
+    // -- Start of internal part ----------
+    // Hide everything down-below from SWT designer
+    //$hide>>$
+
+    /**
+     * Called by the constructor right after {@link #createContents(Composite)}.
+     */
+    private void postCreate() {
+        // nothing to be done for now.
+    }
+
+
+    // -------
+
+    private static class CellInfo {
+        final boolean mIsUser;
+        final Device  mDevice;
+        final TextLayout mWidget;
+        Rectangle mBounds;
+
+        CellInfo(boolean isUser, Device device, TextLayout widget) {
+            mIsUser = isUser;
+            mDevice = device;
+            mWidget = widget;
+        }
+    }
+
+    private void fillTable(final Table table) {
+
+        table.removeAll();
+        disposeTableResources(table.getData("disposeResources"));
+
+        final List<Resource> disposables = new ArrayList<Resource>();
+
+        Font boldFont = getBoldFont(table);
+        if (boldFont != null) {
+            disposables.add(boldFont);
+        } else {
+            boldFont = table.getFont();
+        }
+
+        try {
+            mDisableRefresh = true;
+            disposables.addAll(fillDevices(table, boldFont, true,
+                    mDeviceManager.getDevices(DeviceManager.USER_DEVICES)));
+            disposables.addAll(fillDevices(table, boldFont, false,
+                    mDeviceManager.getDevices(DeviceManager.DEFAULT_DEVICES |
+                                              DeviceManager.VENDOR_DEVICES)));
+        } finally {
+            mDisableRefresh = false;
+        }
+
+        table.setData("disposeResources", disposables);
+
+        if (!Boolean.TRUE.equals(table.getData("createdTableListeners"))) {
+            table.addListener(SWT.PaintItem, new Listener() {
+                @Override
+                public void handleEvent(Event event) {
+                    if (event.item != null) {
+                        Object info = event.item.getData();
+                        if (info instanceof CellInfo) {
+                            ((CellInfo) info).mWidget.draw(event.gc, event.x, event.y + 1);
+                        }
+                    }
+                }
+            });
+
+            table.addListener(SWT.MeasureItem, new Listener() {
+                @Override
+                public void handleEvent(Event event) {
+                    if (event.item != null) {
+                        Object info = event.item.getData();
+                        if (info instanceof CellInfo) {
+                            CellInfo ci = (CellInfo) info;
+                            Rectangle bounds = ci.mBounds;
+                            if (bounds == null) {
+                                // TextLayout.getBounds() seems expensive, so let's cache it.
+                                ci.mBounds = bounds = ci.mWidget.getBounds();
+                            }
+                            event.width = bounds.width + 2;
+                            event.height = bounds.height + 4;
+                        }
+                    }
+                }
+            });
+
+            table.addDisposeListener(new DisposeListener() {
+                @Override
+                public void widgetDisposed(DisposeEvent event) {
+                    disposeTableResources(table.getData("disposeResources"));
+                }
+            });
+
+            table.addSelectionListener(new SelectionListener() {
+                /** Handles single clicks on a row. */
+                @Override
+                public void widgetSelected(SelectionEvent event) {
+                    updateButtonStates();
+                }
+
+                /** Handles double click on a row. */
+                @Override
+                public void widgetDefaultSelected(SelectionEvent event) {
+                    // FIXME: should double-click be to edit a device or create a new AVD?
+                    onEditDevice();
+                }
+            });
+        }
+
+        if (table.getItemCount() == 0) {
+            table.setEnabled(true);
+            TableItem item = new TableItem(table, SWT.NONE);
+            item.setData(null);
+            item.setText(0, "No devices available");
+            return;
+        }
+
+        table.setData("createdTableListeners", Boolean.TRUE);
+    }
+
+    private void disposeTableResources(Object disposablesList) {
+        if (disposablesList instanceof List<?>) {
+            for (Object obj : (List<?>) disposablesList) {
+                if (obj instanceof Resource) {
+                    ((Resource) obj).dispose();
+                }
+            }
+        }
+    }
+
+    private Font getBoldFont(Table table) {
+        Display display = table.getDisplay();
+        FontData[] fds = table.getFont().getFontData();
+        if (fds != null && fds.length > 0) {
+            fds[0].setStyle(SWT.BOLD);
+            return new Font(display, fds[0]);
+        }
+        return null;
+    }
+
+    private List<Resource> fillDevices(
+            Table table,
+            Font boldFont,
+            boolean isUser,
+            List<Device> devices) {
+        List<Resource> disposables = new ArrayList<Resource>();
+        Display display = table.getDisplay();
+
+        TextStyle boldStyle = new TextStyle();
+        boldStyle.font = boldFont;
+
+        // We need the list to be be modifiable so that we can sort it.
+        devices = new ArrayList<Device>(devices);
+
+        if (isUser) {
+            // Just sort user devices by alphabetical name. They will show up at the top.
+            Collections.sort(devices, new Comparator<Device>() {
+                @Override
+                public int compare(Device d1, Device d2) {
+                    String s1 = d1 == null ? "" : d1.getName();
+                    String s2 = d2 == null ? "" : d2.getName();
+                    return s1.compareTo(s2);
+                }});
+        } else {
+            // Sort non-user devices by descending "pretty name"
+            // TODO revisit. Doesn't perform as well as expected.
+            Collections.sort(devices, new Comparator<Device>() {
+                @Override
+                public int compare(Device d1, Device d2) {
+                    String s1 = getPrettyName(d1, true /*leadZeroes*/);
+                    String s2 = getPrettyName(d2, true /*leadZeroes*/);
+                    return s2.compareTo(s1);
+                }});
+        }
+
+        // Generate a list of the AVD names using these devices
+        Map<Device, List<String>> device2avdMap = new HashMap<Device, List<String>>();
+        for (AvdInfo avd : mSwtUpdaterData.getAvdManager().getAllAvds()) {
+            String n = avd.getDeviceName();
+            String m = avd.getDeviceManufacturer();
+            if (n == null || m == null || n.isEmpty() || m.isEmpty()) {
+                continue;
+            }
+            for (Device device : devices) {
+                if (m.equals(device.getManufacturer()) && n.equals(device.getName())) {
+                    List<String> list = device2avdMap.get(device);
+                    if (list == null) {
+                        list = new LinkedList<String>();
+                        device2avdMap.put(device, list);
+                    }
+                    list.add(avd.getName());
+                }
+            }
+        }
+
+        final String prefix = "\n    ";
+
+        for (Device device : devices) {
+            TableItem item = new TableItem(table, SWT.NONE);
+            TextLayout widget = new TextLayout(display);
+            CellInfo ci = new CellInfo(isUser, device, widget);
+            item.setData(ci);
+
+            widget.setIndent(mImageWidth * 2);
+            widget.setFont(table.getFont());
+
+            StringBuilder sb = new StringBuilder();
+            String name = getPrettyName(device, false /*leadZeroes*/);
+            sb.append(name);
+            int pos1 = sb.length();
+
+            String manufacturer = device.getManufacturer();
+            String manu = manufacturer;
+            if (isUser) {
+                item.setImage(mUserImage);
+            } else if (GENERIC.equals(manu)) {
+                item.setImage(mGenericImage);
+            } else {
+                item.setImage(mOtherImage);
+                if (!manufacturer.contains(NEXUS)) {
+                    sb.append("  by ").append(manufacturer);
+                }
+            }
+
+            Hardware hw = device.getDefaultHardware();
+            Screen screen = hw.getScreen();
+            sb.append(prefix);
+            sb.append(String.format(java.util.Locale.US,
+                        "Screen:   %1$.1f\", %2$d \u00D7 %3$d, %4$s %5$s", // U+00D7: Unicode multiplication sign
+                        screen.getDiagonalLength(),
+                        screen.getXDimension(),
+                        screen.getYDimension(),
+                        screen.getSize().getShortDisplayValue(),
+                        screen.getPixelDensity().getResourceValue()
+                        ));
+
+            Storage sto = hw.getRam();
+            Unit unit = sto.getSizeAsUnit(Unit.GiB) > 1 ? Unit.GiB : Unit.MiB;
+            sb.append(prefix);
+            sb.append(String.format(java.util.Locale.US,
+                    "RAM:       %1$d %2$s",
+                    sto.getSizeAsUnit(unit),
+                    unit));
+
+            List<String> avdList = device2avdMap.get(device);
+            if (avdList != null && !avdList.isEmpty()) {
+                sb.append(prefix);
+                sb.append("Used by: ");
+                boolean first = true;
+                for (String avd : avdList) {
+                    if (!first) {
+                        sb.append(", ");
+                    }
+                    sb.append(avd);
+                    first = false;
+                }
+            }
+
+            widget.setText(sb.toString());
+            widget.setStyle(boldStyle, 0, pos1);
+        }
+
+        return disposables;
+    }
+
+    // Constants extracted from DeviceMenuListerner -- TODO refactor somewhere else.
+    private static final String NEXUS   = "Nexus";     //$NON-NLS-1$
+    private static final String GENERIC = "Generic";   //$NON-NLS-1$
+    private static Pattern PATTERN = Pattern.compile(
+            "(\\d+\\.?\\d*)in (.+?)( \\(.*Nexus.*\\))?"); //$NON-NLS-1$
+    /**
+     * Returns a pretty name for the device.
+     *
+     * Extracted from DeviceMenuListener.
+     * Modified to remove the leading space insertion as it doesn't render
+     * neatly in the avd manager. Instead added the option to add leading
+     * zeroes to make the string names sort properly.
+     *
+     * Replace "'in'" with '"' (e.g. 2.7" QVGA instead of 2.7in QVGA)
+     * Use the same precision for all devices (all but one specify decimals)
+     * Add in screen resolution and density
+     */
+    private static String getPrettyName(Device d, boolean leadZeroes) {
+        if (d == null) {
+            return "";
+        }
+        String name = d.getName();
+        if (name.equals("3.7 FWVGA slider")) {                        //$NON-NLS-1$
+            // Fix metadata: this one entry doesn't have "in" like the rest of them
+            name = "3.7in FWVGA slider";                              //$NON-NLS-1$
+        }
+
+        Matcher matcher = PATTERN.matcher(name);
+        if (matcher.matches()) {
+            String size = matcher.group(1);
+            String n = matcher.group(2);
+            int dot = size.indexOf('.');
+            if (dot == -1) {
+                size = size + ".0";
+                dot = size.length() - 2;
+            }
+            if (leadZeroes && dot < 3) {
+                // Pad to have at least 3 digits before the dot, for sorting purposes.
+                // We can revisit this once we get devices that are more than 999 inches wide.
+                size = "000".substring(dot) + size;
+            }
+            name = size + "\" " + n;
+        }
+
+        return name;
+    }
+
+    /**
+     * Returns the currently selected cell info in the table or null
+     */
+    private CellInfo getTableSelection() {
+        if (mTable.isDisposed()) {
+            return null;
+        }
+        int selIndex = mTable.getSelectionIndex();
+        if (selIndex >= 0) {
+            return (CellInfo) mTable.getItem(selIndex).getData();
+        }
+
+        return null;
+    }
+
+    private void updateButtonStates() {
+        CellInfo ci = getTableSelection();
+
+        mNewButton.setEnabled(true);
+        mEditButton.setEnabled(ci != null);
+        mEditButton.setText((ci != null && !ci.mIsUser) ? "Clone..." : "Edit...");
+        mDeleteButton.setEnabled(ci != null && ci.mIsUser);
+        mNewAvdButton.setEnabled(ci != null);
+        mRefreshButton.setEnabled(true);
+    }
+
+    private void onNewDevice() {
+        DeviceCreationDialog dlg = new DeviceCreationDialog(
+                getShell(),
+                mDeviceManager,
+                mSwtUpdaterData.getImageFactory(),
+                null /*device*/);
+        if (dlg.open() == Window.OK) {
+            onRefresh();
+
+            // Select the new device, if any.
+            selectCellByDevice(dlg.getCreatedDevice());
+            updateButtonStates();
+        }
+    }
+
+    private void onEditDevice() {
+        CellInfo ci = getTableSelection();
+        if (ci == null || ci.mDevice == null) {
+            return;
+        }
+
+        DeviceCreationDialog dlg = new DeviceCreationDialog(
+                getShell(),
+                mDeviceManager,
+                mSwtUpdaterData.getImageFactory(),
+                ci.mDevice);
+        if (dlg.open() == Window.OK) {
+            onRefresh();
+
+            // Select the new device, if any.
+            selectCellByDevice(dlg.getCreatedDevice());
+            updateButtonStates();
+        }
+    }
+
+    private void onDeleteDevice() {
+        CellInfo ci = getTableSelection();
+        if (ci == null || ci.mDevice == null || !ci.mIsUser) {
+            return;
+        }
+
+        final String name = getPrettyName(ci.mDevice, false /*leadZeroes*/);
+        final AtomicBoolean result = new AtomicBoolean(false);
+        getDisplay().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                Shell shell = getDisplay().getActiveShell();
+                boolean ok = MessageDialog.openQuestion(shell,
+                        "Delete Device Definition",
+                        String.format(
+                                "Please confirm that you want to delete the device definition named '%s'. This operation cannot be reverted.",
+                                name));
+                result.set(ok);
+            }
+        });
+
+        if (result.get()) {
+            mDeviceManager.removeUserDevice(ci.mDevice);
+            mDeviceManager.saveUserDevices();
+            onRefresh();
+        }
+    }
+
+    private void onCreateAvd() {
+        CellInfo ci = getTableSelection();
+        if (ci == null || ci.mDevice == null) {
+            return;
+        }
+
+        final AvdCreationDialog dlg = new AvdCreationDialog(mTable.getShell(),
+                mSwtUpdaterData.getAvdManager(),
+                mImageFactory,
+                mSwtUpdaterData.getSdkLog(),
+                null);
+        dlg.selectInitialDevice(ci.mDevice);
+
+        if (dlg.open() == Window.OK) {
+            onRefresh();
+
+            if (mAvdCreatedListener != null) {
+                mAvdCreatedListener.onAvdCreated(dlg.getCreatedAvd());
+            }
+        }
+    }
+
+    private void onRefresh() {
+        if (mDisableRefresh || mTable.isDisposed()) {
+            return;
+        }
+        int selIndex = mTable.getSelectionIndex();
+        CellInfo selected = getTableSelection();
+
+        fillTable(mTable);
+
+        if (selected != null) {
+            if (selectCellByName(selected)) {
+                updateButtonStates();
+                return;
+            }
+        }
+        // If not found by name, use the position if available.
+        if (selIndex >= 0 && selIndex < mTable.getItemCount()) {
+            mTable.select(selIndex);
+        }
+    }
+
+    private boolean selectCellByName(CellInfo selected) {
+        if (mTable.isDisposed() || selected == null || selected.mDevice == null) {
+            return false;
+        }
+        String name = selected.mDevice.getName();
+        for (int n = mTable.getItemCount() - 1; n >= 0; n--) {
+            TableItem item = mTable.getItem(n);
+            Object data = item.getData();
+            if (data instanceof CellInfo) {
+                CellInfo ci = (CellInfo) data;
+                if (ci != null && ci.mDevice != null && name.equals(ci.mDevice.getName())) {
+                    // Same cell object. Select it.
+                    mTable.select(n);
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private boolean selectCellByDevice(Device selected) {
+        if (mTable.isDisposed() || selected == null) {
+            return false;
+        }
+        for (int n = mTable.getItemCount() - 1; n >= 0; n--) {
+            TableItem item = mTable.getItem(n);
+            Object data = item.getData();
+            if (data instanceof CellInfo) {
+                CellInfo ci = (CellInfo) data;
+                if (ci != null && ci.mDevice == selected) {
+                    // Same device object. Select it.
+                    mTable.select(n);
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    // -------
+
+
+    // --- Implementation of ISdkChangeListener ---
+
+    @Override
+    public void onSdkLoaded() {
+        onSdkReload();
+    }
+
+    @Override
+    public void onSdkReload() {
+        onRefresh();
+    }
+
+    @Override
+    public void preInstallHook() {
+        // nothing to be done for now.
+    }
+
+    @Override
+    public void postInstallHook() {
+        // nothing to be done for now.
+    }
+
+    // --- Implementation of DevicesChangeListener
+
+    @Override
+    public void onDevicesChanged() {
+        onRefresh();
+    }
+
+
+    // End of hiding from SWT Designer
+    //$hide<<$
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/LogWindow.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/LogWindow.java
new file mode 100755
index 0000000..43dbaf5
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/LogWindow.java
@@ -0,0 +1,379 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository.ui;
+
+import com.android.sdkuilib.internal.tasks.ILogUiProvider;
+import com.android.sdkuilib.ui.GridDataBuilder;
+import com.android.sdkuilib.ui.GridLayoutBuilder;
+import com.android.utils.ILogger;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.StyleRange;
+import org.eclipse.swt.custom.StyledText;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.ShellAdapter;
+import org.eclipse.swt.events.ShellEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Widget;
+
+
+/**
+ * A floating log window that can be displayed or hidden by the main SDK Manager 2 window.
+ * It displays a log of the sdk manager operation (listing, install, delete) including
+ * any errors (e.g. network error or install/delete errors.)
+ * <p/>
+ * Since the SDK Manager will direct all log to this window, its purpose is to be
+ * opened by the main window at startup and left open all the time. When not needed
+ * the floating window is hidden but not closed. This way it can easily accumulate
+ * all the log.
+ */
+class LogWindow implements ILogUiProvider {
+
+    private Shell mParentShell;
+    private Shell mShell;
+    private Composite mRootComposite;
+    private StyledText mStyledText;
+    private Label mLogDescription;
+    private Button mCloseButton;
+
+    private final ILogger mSecondaryLog;
+    private boolean mCloseRequested;
+    private boolean mInitPosition = true;
+    private String mLastLogMsg = null;
+
+    private enum TextStyle {
+        DEFAULT,
+        TITLE,
+        ERROR
+    }
+
+    /**
+     * Creates the floating window. Callers should use {@link #open()} later.
+     *
+     * @param parentShell Parent container
+     * @param secondaryLog An optional logger where messages will <em>also</em> be output.
+     */
+    public LogWindow(Shell parentShell, ILogger secondaryLog) {
+        mParentShell = parentShell;
+        mSecondaryLog = secondaryLog;
+    }
+
+    /**
+     * For testing only. See {@link #open()} and {@link #close()} for normal usage.
+     * @wbp.parser.entryPoint
+     */
+    void openBlocking() {
+        open();
+        Display display = Display.getDefault();
+        while (!mShell.isDisposed()) {
+            if (!display.readAndDispatch()) {
+                display.sleep();
+            }
+        }
+        close();
+    }
+
+    /**
+     * Opens the window.
+     * This call does not block and relies on the fact that the main window is
+     * already running an SWT event dispatch loop.
+     * Caller should use {@link #close()} later.
+     */
+    public void open() {
+        createShell();
+        createContents();
+        mShell.open();
+        mShell.layout();
+        mShell.setVisible(false);
+    }
+
+    /**
+     * Closes and <em>destroys</em> the window.
+     * This must be called just before quitting the app.
+     * <p/>
+     * To simply hide/show the window, use {@link #setVisible(boolean)} instead.
+     */
+    public void close() {
+        if (mShell != null && !mShell.isDisposed()) {
+            mCloseRequested = true;
+            mShell.close();
+            mShell = null;
+        }
+    }
+
+    /**
+     * Determines whether the window is currently shown or not.
+     *
+     * @return True if the window is shown.
+     */
+    public boolean isVisible() {
+        return mShell != null && !mShell.isDisposed() && mShell.isVisible();
+    }
+
+    /**
+     * Toggles the window visibility.
+     *
+     * @param visible True to make the window visible, false to hide it.
+     */
+    public void setVisible(boolean visible) {
+        if (mShell != null && !mShell.isDisposed()) {
+            mShell.setVisible(visible);
+            if (visible && mInitPosition) {
+                mInitPosition = false;
+                positionWindow();
+            }
+        }
+    }
+
+    private void createShell() {
+        mShell = new Shell(mParentShell, SWT.SHELL_TRIM | SWT.TOOL);
+        mShell.setMinimumSize(new Point(600, 300));
+        mShell.setSize(450, 300);
+        mShell.setText("Android SDK Manager Log");
+        GridLayoutBuilder.create(mShell);
+
+        mShell.addShellListener(new ShellAdapter() {
+            @Override
+            public void shellClosed(ShellEvent e) {
+                if (!mCloseRequested) {
+                    e.doit = false;
+                    setVisible(false);
+                }
+            }
+        });
+    }
+
+    /**
+     * Create contents of the dialog.
+     */
+    private void createContents() {
+        mRootComposite = new Composite(mShell, SWT.NONE);
+        GridLayoutBuilder.create(mRootComposite).columns(2);
+        GridDataBuilder.create(mRootComposite).fill().grab();
+
+        mStyledText = new StyledText(mRootComposite,
+                SWT.BORDER | SWT.MULTI | SWT.READ_ONLY | SWT.WRAP | SWT.V_SCROLL);
+        GridDataBuilder.create(mStyledText).hSpan(2).fill().grab();
+
+        mLogDescription = new Label(mRootComposite, SWT.NONE);
+        GridDataBuilder.create(mLogDescription).hFill().hGrab();
+
+        mCloseButton = new Button(mRootComposite, SWT.NONE);
+        mCloseButton.setText("Close");
+        mCloseButton.setToolTipText("Closes the log window");
+        mCloseButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                setVisible(false);  //$hide$
+            }
+        });
+    }
+
+    // --- Implementation of ILogUiProvider ---
+
+
+    /**
+     * Sets the description in the current task dialog.
+     * This method can be invoked from a non-UI thread.
+     */
+    @Override
+    public void setDescription(final String description) {
+        syncExec(mLogDescription, new Runnable() {
+            @Override
+            public void run() {
+                mLogDescription.setText(description);
+
+                if (acceptLog(description, true /*isDescription*/)) {
+                    appendLine(TextStyle.TITLE, description);
+
+                    if (mSecondaryLog != null) {
+                        mSecondaryLog.info("%1$s", description);  //$NON-NLS-1$
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * Logs a "normal" information line.
+     * This method can be invoked from a non-UI thread.
+     */
+    @Override
+    public void log(final String log) {
+        if (acceptLog(log, false /*isDescription*/)) {
+            syncExec(mLogDescription, new Runnable() {
+                @Override
+                public void run() {
+                    appendLine(TextStyle.DEFAULT, log);
+                }
+            });
+
+            if (mSecondaryLog != null) {
+                mSecondaryLog.info("  %1$s", log);                //$NON-NLS-1$
+            }
+        }
+    }
+
+    /**
+     * Logs an "error" information line.
+     * This method can be invoked from a non-UI thread.
+     */
+    @Override
+    public void logError(final String log) {
+        if (acceptLog(log, false /*isDescription*/)) {
+            syncExec(mLogDescription, new Runnable() {
+                @Override
+                public void run() {
+                    appendLine(TextStyle.ERROR, log);
+                }
+            });
+
+            if (mSecondaryLog != null) {
+                mSecondaryLog.error(null, "%1$s", log);             //$NON-NLS-1$
+            }
+        }
+    }
+
+    /**
+     * Logs a "verbose" information line, that is extra details which are typically
+     * not that useful for the end-user and might be hidden until explicitly shown.
+     * This method can be invoked from a non-UI thread.
+     */
+    @Override
+    public void logVerbose(final String log) {
+        if (acceptLog(log, false /*isDescription*/)) {
+            syncExec(mLogDescription, new Runnable() {
+                @Override
+                public void run() {
+                    appendLine(TextStyle.DEFAULT, "  " + log);      //$NON-NLS-1$
+                }
+            });
+
+            if (mSecondaryLog != null) {
+                mSecondaryLog.info("    %1$s", log);              //$NON-NLS-1$
+            }
+        }
+    }
+
+
+    // ----
+
+
+    /**
+     * Centers the dialog in its parent shell.
+     */
+    private void positionWindow() {
+        // Centers the dialog in its parent shell
+        Shell child = mShell;
+        if (child != null && mParentShell != null) {
+            // get the parent client area with a location relative to the display
+            Rectangle parentArea = mParentShell.getClientArea();
+            Point parentLoc = mParentShell.getLocation();
+            int px = parentLoc.x;
+            int py = parentLoc.y;
+            int pw = parentArea.width;
+            int ph = parentArea.height;
+
+            Point childSize = child.getSize();
+            int cw = Math.max(childSize.x, pw);
+            int ch = childSize.y;
+
+            int x = 30 + px + (pw - cw) / 2;
+            if (x < 0) x = 0;
+
+            int y = py + (ph - ch) / 2;
+            if (y < py) y = py;
+
+            child.setLocation(x, y);
+            child.setSize(cw, ch);
+        }
+    }
+
+    private void appendLine(TextStyle style, String text) {
+        if (!text.endsWith("\n")) {                                 //$NON-NLS-1$
+            text += '\n';
+        }
+
+        int start = mStyledText.getCharCount();
+
+        if (style == TextStyle.DEFAULT) {
+            mStyledText.append(text);
+
+        } else {
+            mStyledText.append(text);
+
+            StyleRange sr = new StyleRange();
+            sr.start = start;
+            sr.length = text.length();
+            sr.fontStyle = SWT.BOLD;
+            if (style == TextStyle.ERROR) {
+                sr.foreground = mStyledText.getDisplay().getSystemColor(SWT.COLOR_DARK_RED);
+            }
+            sr.underline = false;
+            mStyledText.setStyleRange(sr);
+        }
+
+        // Scroll caret if it was already at the end before we added new text.
+        // Ideally we would scroll if the scrollbar is at the bottom but we don't
+        // have direct access to the scrollbar without overriding the SWT impl.
+        if (mStyledText.getCaretOffset() >= start) {
+            mStyledText.setSelection(mStyledText.getCharCount());
+        }
+    }
+
+
+    private void syncExec(final Widget widget, final Runnable runnable) {
+        if (widget != null && !widget.isDisposed()) {
+            widget.getDisplay().syncExec(runnable);
+        }
+    }
+
+    /**
+     * Filter messages displayed in the log: <br/>
+     * - Messages with a % are typical part of a progress update and shouldn't be in the log. <br/>
+     * - Messages that are the same as the same output message should be output a second time.
+     *
+     * @param msg The potential log line to print.
+     * @return True if the log line should be printed, false otherwise.
+     */
+    private boolean acceptLog(String msg, boolean isDescription) {
+        if (msg == null) {
+            return false;
+        }
+
+        msg = msg.trim();
+
+        // Descriptions also have the download progress status (0..100%) which we want to avoid
+        if (isDescription && msg.indexOf('%') != -1) {
+            return false;
+        }
+
+        if (msg.equals(mLastLogMsg)) {
+            return false;
+        }
+
+        mLastLogMsg = msg;
+        return true;
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPage.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPage.java
new file mode 100755
index 0000000..7f5c6b6
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPage.java
@@ -0,0 +1,1301 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository.ui;
+
+import com.android.sdklib.internal.repository.ITask;
+import com.android.sdklib.internal.repository.ITaskMonitor;
+import com.android.sdklib.internal.repository.archives.Archive;
+import com.android.sdklib.internal.repository.archives.ArchiveInstaller;
+import com.android.sdklib.internal.repository.packages.Package;
+import com.android.sdklib.internal.repository.updater.PkgItem;
+import com.android.sdklib.internal.repository.updater.PkgItem.PkgState;
+import com.android.sdklib.repository.ISdkChangeListener;
+import com.android.sdkuilib.internal.repository.SwtUpdaterData;
+import com.android.sdkuilib.internal.repository.core.PkgCategory;
+import com.android.sdkuilib.internal.repository.core.PkgCategoryApi;
+import com.android.sdkuilib.internal.repository.core.PkgContentProvider;
+import com.android.sdkuilib.internal.repository.icons.ImageFactory;
+import com.android.sdkuilib.repository.SdkUpdaterWindow.SdkInvocationContext;
+import com.android.sdkuilib.ui.GridDataBuilder;
+import com.android.sdkuilib.ui.GridLayoutBuilder;
+
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.viewers.CheckStateChangedEvent;
+import org.eclipse.jface.viewers.CheckboxTreeViewer;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.ColumnViewerToolTipSupport;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.ICheckStateListener;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.jface.viewers.TreeViewerColumn;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerFilter;
+import org.eclipse.jface.window.ToolTip;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Link;
+import org.eclipse.swt.widgets.MenuItem;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeColumn;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Page that displays both locally installed packages as well as all known
+ * remote available packages. This gives an overview of what is installed
+ * vs what is available and allows the user to update or install packages.
+ */
+public final class PackagesPage extends Composite implements ISdkChangeListener {
+
+    enum MenuAction {
+        RELOAD                      (SWT.NONE,  "Reload"),
+        SHOW_ADDON_SITES            (SWT.NONE,  "Manage Add-on Sites..."),
+        TOGGLE_SHOW_ARCHIVES        (SWT.CHECK, "Show Archives Details"),
+        TOGGLE_SHOW_INSTALLED_PKG   (SWT.CHECK, "Show Installed Packages"),
+        TOGGLE_SHOW_OBSOLETE_PKG    (SWT.CHECK, "Show Obsolete Packages"),
+        TOGGLE_SHOW_UPDATE_NEW_PKG  (SWT.CHECK, "Show Updates/New Packages"),
+        SORT_API_LEVEL              (SWT.RADIO, "Sort by API Level"),
+        SORT_SOURCE                 (SWT.RADIO, "Sort by Repository")
+        ;
+
+        private final int mMenuStyle;
+        private final String mMenuTitle;
+
+        MenuAction(int menuStyle, String menuTitle) {
+            mMenuStyle = menuStyle;
+            mMenuTitle = menuTitle;
+        }
+
+        public int getMenuStyle() {
+            return mMenuStyle;
+        }
+
+        public String getMenuTitle() {
+            return mMenuTitle;
+        }
+    };
+
+    private final Map<MenuAction, MenuItem> mMenuActions = new HashMap<MenuAction, MenuItem>();
+
+    private final PackagesPageImpl mImpl;
+    private final SdkInvocationContext mContext;
+
+    private boolean mDisplayArchives = false;
+    private boolean mOperationPending;
+
+    private Composite mGroupPackages;
+    private Text mTextSdkOsPath;
+    private Button mCheckSortSource;
+    private Button mCheckSortApi;
+    private Button mCheckFilterObsolete;
+    private Button mCheckFilterInstalled;
+    private Button mCheckFilterNew;
+    private Composite mGroupOptions;
+    private Composite mGroupSdk;
+    private Button mButtonDelete;
+    private Button mButtonInstall;
+    private Font mTreeFontItalic;
+    private TreeColumn mTreeColumnName;
+    private CheckboxTreeViewer mTreeViewer;
+
+    public PackagesPage(
+            Composite parent,
+            int swtStyle,
+            SwtUpdaterData swtUpdaterData,
+            SdkInvocationContext context) {
+        super(parent, swtStyle);
+        mImpl = new PackagesPageImpl(swtUpdaterData) {
+            @Override
+            protected boolean isUiDisposed() {
+                return mGroupPackages == null || mGroupPackages.isDisposed();
+            };
+            @Override
+            protected void syncExec(Runnable runnable) {
+                if (!isUiDisposed()) {
+                    mGroupPackages.getDisplay().syncExec(runnable);
+                }
+            };
+
+            @Override
+            protected void syncViewerSelection() {
+                PackagesPage.this.syncViewerSelection();
+            }
+
+            @Override
+            protected void refreshViewerInput() {
+                PackagesPage.this.refreshViewerInput();
+            }
+
+            @Override
+            protected boolean isSortByApi() {
+                return PackagesPage.this.isSortByApi();
+            }
+
+            @Override
+            protected Font getTreeFontItalic() {
+                return mTreeFontItalic;
+            }
+
+            @Override
+            protected void loadPackages(boolean useLocalCache, boolean overrideExisting) {
+                PackagesPage.this.loadPackages(useLocalCache, overrideExisting);
+            }
+        };
+        mContext = context;
+
+        createContents(this);
+        postCreate();  //$hide$
+    }
+
+    public void performFirstLoad() {
+        mImpl.performFirstLoad();
+    }
+
+    @SuppressWarnings("unused")
+    private void createContents(Composite parent) {
+        GridLayoutBuilder.create(parent).noMargins().columns(2);
+
+        mGroupSdk = new Composite(parent, SWT.NONE);
+        GridDataBuilder.create(mGroupSdk).hFill().vCenter().hGrab().hSpan(2);
+        GridLayoutBuilder.create(mGroupSdk).columns(2);
+
+        Label label1 = new Label(mGroupSdk, SWT.NONE);
+        label1.setText("SDK Path:");
+
+        mTextSdkOsPath = new Text(mGroupSdk, SWT.NONE);
+        GridDataBuilder.create(mTextSdkOsPath).hFill().vCenter().hGrab();
+        mTextSdkOsPath.setEnabled(false);
+
+        Group groupPackages = new Group(parent, SWT.NONE);
+        mGroupPackages = groupPackages;
+        GridDataBuilder.create(mGroupPackages).fill().grab().hSpan(2);
+        groupPackages.setText("Packages");
+        GridLayoutBuilder.create(groupPackages).columns(1);
+
+        mTreeViewer = new CheckboxTreeViewer(groupPackages, SWT.BORDER);
+        mImpl.setITreeViewer(new PackagesPageImpl.ICheckboxTreeViewer() {
+            @Override
+            public Object getInput() {
+                return mTreeViewer.getInput();
+            }
+
+            @Override
+            public void setInput(List<PkgCategory> cats) {
+                mTreeViewer.setInput(cats);
+            }
+
+            @Override
+            public void setContentProvider(PkgContentProvider pkgContentProvider) {
+                mTreeViewer.setContentProvider(pkgContentProvider);
+            }
+
+            @Override
+            public void refresh() {
+                mTreeViewer.refresh();
+            }
+
+            @Override
+            public Object[] getCheckedElements() {
+                return mTreeViewer.getCheckedElements();
+            }
+        });
+        mTreeViewer.addFilter(new ViewerFilter() {
+            @Override
+            public boolean select(Viewer viewer, Object parentElement, Object element) {
+                return filterViewerItem(element);
+            }
+        });
+
+        mTreeViewer.addCheckStateListener(new ICheckStateListener() {
+            @Override
+            public void checkStateChanged(CheckStateChangedEvent event) {
+                onTreeCheckStateChanged(event); //$hide$
+            }
+        });
+
+        mTreeViewer.addDoubleClickListener(new IDoubleClickListener() {
+            @Override
+            public void doubleClick(DoubleClickEvent event) {
+                onTreeDoubleClick(event); //$hide$
+            }
+        });
+
+        Tree tree = mTreeViewer.getTree();
+        tree.setLinesVisible(true);
+        tree.setHeaderVisible(true);
+        GridDataBuilder.create(tree).fill().grab();
+
+        // column name icon is set when loading depending on the current filter type
+        // (e.g. API level or source)
+        TreeViewerColumn columnName = new TreeViewerColumn(mTreeViewer, SWT.NONE);
+        mTreeColumnName = columnName.getColumn();
+        mTreeColumnName.setText("Name");
+        mTreeColumnName.setWidth(340);
+
+        TreeViewerColumn columnApi = new TreeViewerColumn(mTreeViewer, SWT.NONE);
+        TreeColumn treeColumn2 = columnApi.getColumn();
+        treeColumn2.setText("API");
+        treeColumn2.setAlignment(SWT.CENTER);
+        treeColumn2.setWidth(50);
+
+        TreeViewerColumn columnRevision = new TreeViewerColumn(mTreeViewer, SWT.NONE);
+        TreeColumn treeColumn3 = columnRevision.getColumn();
+        treeColumn3.setText("Rev.");
+        treeColumn3.setToolTipText("Revision currently installed");
+        treeColumn3.setAlignment(SWT.CENTER);
+        treeColumn3.setWidth(50);
+
+
+        TreeViewerColumn columnStatus = new TreeViewerColumn(mTreeViewer, SWT.NONE);
+        TreeColumn treeColumn4 = columnStatus.getColumn();
+        treeColumn4.setText("Status");
+        treeColumn4.setAlignment(SWT.LEAD);
+        treeColumn4.setWidth(190);
+
+        mImpl.setIColumns(
+                wrapColumn(columnName),
+                wrapColumn(columnApi),
+                wrapColumn(columnRevision),
+                wrapColumn(columnStatus));
+
+        mGroupOptions = new Composite(groupPackages, SWT.NONE);
+        GridDataBuilder.create(mGroupOptions).hFill().vCenter().hGrab();
+        GridLayoutBuilder.create(mGroupOptions).columns(7).noMargins();
+
+        // Options line 1, 7 columns
+
+        Label label3 = new Label(mGroupOptions, SWT.NONE);
+        label3.setText("Show:");
+
+        mCheckFilterNew = new Button(mGroupOptions, SWT.CHECK);
+        mCheckFilterNew.setText("Updates/New");
+        mCheckFilterNew.setToolTipText("Show Updates and New");
+        mCheckFilterNew.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                refreshViewerInput();
+            }
+        });
+        mCheckFilterNew.setSelection(true);
+
+        mCheckFilterInstalled = new Button(mGroupOptions, SWT.CHECK);
+        mCheckFilterInstalled.setToolTipText("Show Installed");
+        mCheckFilterInstalled.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                refreshViewerInput();
+            }
+        });
+        mCheckFilterInstalled.setSelection(true);
+        mCheckFilterInstalled.setText("Installed");
+
+        mCheckFilterObsolete = new Button(mGroupOptions, SWT.CHECK);
+        mCheckFilterObsolete.setText("Obsolete");
+        mCheckFilterObsolete.setToolTipText("Also show obsolete packages");
+        mCheckFilterObsolete.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                refreshViewerInput();
+            }
+        });
+        mCheckFilterObsolete.setSelection(false);
+
+        Link linkSelectNew = new Link(mGroupOptions, SWT.NONE);
+        // Note for i18n: we need to identify which link is used, and this is done by using the
+        // text itself so for translation purposes we want to keep the <a> link strings separate.
+        final String strLinkNew = "New";
+        final String strLinkUpdates = "Updates";
+        linkSelectNew.setText(
+                String.format("Select <a>%1$s</a> or <a>%2$s</a>", strLinkNew, strLinkUpdates));
+        linkSelectNew.setToolTipText("Selects all items that are either new or updates.");
+        GridDataBuilder.create(linkSelectNew).hFill();
+        linkSelectNew.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                super.widgetSelected(e);
+                boolean selectNew = e.text == null || e.text.equals(strLinkNew);
+                onSelectNewUpdates(selectNew, !selectNew, false/*selectTop*/);
+            }
+        });
+
+        // placeholder between "select all" and "install"
+        Label placeholder = new Label(mGroupOptions, SWT.NONE);
+        GridDataBuilder.create(placeholder).hFill().hGrab();
+
+        mButtonInstall = new Button(mGroupOptions, SWT.NONE);
+        mButtonInstall.setText("");  //$NON-NLS-1$  placeholder, filled in updateButtonsState()
+        mButtonInstall.setToolTipText("Install one or more packages");
+        GridDataBuilder.create(mButtonInstall).vCenter().wHint(150);
+        mButtonInstall.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                onButtonInstall();  //$hide$
+            }
+        });
+
+        // Options line 2, 7 columns
+
+        Label label2 = new Label(mGroupOptions, SWT.NONE);
+        label2.setText("Sort by:");
+
+        mCheckSortApi = new Button(mGroupOptions, SWT.RADIO);
+        mCheckSortApi.setToolTipText("Sort by API level");
+        mCheckSortApi.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                if (mCheckSortApi.getSelection()) {
+                    refreshViewerInput();
+                    copySelection(true /*toApi*/);
+                    syncViewerSelection();
+                }
+            }
+        });
+        mCheckSortApi.setText("API level");
+        mCheckSortApi.setSelection(true);
+
+        mCheckSortSource = new Button(mGroupOptions, SWT.RADIO);
+        mCheckSortSource.setText("Repository");
+        mCheckSortSource.setToolTipText("Sort by Repository");
+        mCheckSortSource.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                if (mCheckSortSource.getSelection()) {
+                    refreshViewerInput();
+                    copySelection(false /*toApi*/);
+                    syncViewerSelection();
+                }
+            }
+        });
+
+        // placeholder between "repository" and "deselect"
+        new Label(mGroupOptions, SWT.NONE);
+
+        Link linkDeselect = new Link(mGroupOptions, SWT.NONE);
+        linkDeselect.setText("<a>Deselect All</a>");
+        linkDeselect.setToolTipText("Deselects all the currently selected items");
+        GridDataBuilder.create(linkDeselect).hFill();
+        linkDeselect.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                super.widgetSelected(e);
+                onDeselectAll();
+            }
+        });
+
+        // placeholder between "deselect" and "delete"
+        placeholder = new Label(mGroupOptions, SWT.NONE);
+        GridDataBuilder.create(placeholder).hFill().hGrab();
+
+        mButtonDelete = new Button(mGroupOptions, SWT.NONE);
+        mButtonDelete.setText("");  //$NON-NLS-1$  placeholder, filled in updateButtonsState()
+        mButtonDelete.setToolTipText("Delete one ore more installed packages");
+        GridDataBuilder.create(mButtonDelete).vCenter().wHint(150);
+        mButtonDelete.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                onButtonDelete();  //$hide$
+            }
+        });
+    }
+
+    private PackagesPageImpl.ITreeViewerColumn wrapColumn(final TreeViewerColumn column) {
+        return new PackagesPageImpl.ITreeViewerColumn() {
+            @Override
+            public void setLabelProvider(ColumnLabelProvider labelProvider) {
+                column.setLabelProvider(labelProvider);
+            }
+        };
+    }
+
+    private Image getImage(String filename) {
+        if (mImpl.mSwtUpdaterData != null) {
+            ImageFactory imgFactory = mImpl.mSwtUpdaterData.getImageFactory();
+            if (imgFactory != null) {
+                return imgFactory.getImageByName(filename);
+            }
+        }
+        return null;
+    }
+
+
+    // -- Start of internal part ----------
+    // Hide everything down-below from SWT designer
+    //$hide>>$
+
+
+    // --- menu interactions ---
+
+    public void registerMenuAction(final MenuAction action, MenuItem item) {
+        item.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                Button button = null;
+
+                switch (action) {
+                case RELOAD:
+                    mImpl.fullReload();
+                    break;
+                case SHOW_ADDON_SITES:
+                    AddonSitesDialog d = new AddonSitesDialog(getShell(), mImpl.mSwtUpdaterData);
+                    if (d.open()) {
+                        mImpl.loadPackages();
+                    }
+                    break;
+                case TOGGLE_SHOW_ARCHIVES:
+                    mDisplayArchives = !mDisplayArchives;
+                    // Force the viewer to be refreshed
+                    ((PkgContentProvider) mTreeViewer.getContentProvider()).
+                        setDisplayArchives(mDisplayArchives);
+                    mTreeViewer.setInput(null);
+                    refreshViewerInput();
+                    syncViewerSelection();
+                    break;
+                case TOGGLE_SHOW_INSTALLED_PKG:
+                    button = mCheckFilterInstalled;
+                    break;
+                case TOGGLE_SHOW_OBSOLETE_PKG:
+                    button = mCheckFilterObsolete;
+                    break;
+                case TOGGLE_SHOW_UPDATE_NEW_PKG:
+                    button = mCheckFilterNew;
+                    break;
+                case SORT_API_LEVEL:
+                    button = mCheckSortApi;
+                    break;
+                case SORT_SOURCE:
+                    button = mCheckSortSource;
+                    break;
+                }
+
+                if (button != null && !button.isDisposed()) {
+                    // Toggle this button (radio or checkbox)
+
+                    boolean value = button.getSelection();
+
+                    // SWT doesn't automatically switch radio buttons when using the
+                    // Widget#setSelection method, so we'll do it here manually.
+                    if (!value && (button.getStyle() & SWT.RADIO) != 0) {
+                        // we'll be selecting this radio button, so deselect all ther other ones
+                        // in the parent group.
+                        for (Control child : button.getParent().getChildren()) {
+                            if (child instanceof Button &&
+                                    child != button &&
+                                    (child.getStyle() & SWT.RADIO) != 0) {
+                                ((Button) child).setSelection(value);
+                            }
+                        }
+                    }
+
+                    button.setSelection(!value);
+
+                    // SWT doesn't actually invoke the listeners when using Widget#setSelection
+                    // so let's run the actual action.
+                    button.notifyListeners(SWT.Selection, new Event());
+                }
+
+                updateMenuCheckmarks();
+            }
+        });
+
+        mMenuActions.put(action, item);
+    }
+
+    // --- internal methods ---
+
+    private void updateMenuCheckmarks() {
+
+        for (Entry<MenuAction, MenuItem> entry : mMenuActions.entrySet()) {
+            MenuAction action = entry.getKey();
+            MenuItem item = entry.getValue();
+
+            if (action.getMenuStyle() == SWT.NONE) {
+                continue;
+            }
+
+            boolean value = false;
+            Button button = null;
+
+            switch (action) {
+            case TOGGLE_SHOW_ARCHIVES:
+                value = mDisplayArchives;
+                break;
+            case TOGGLE_SHOW_INSTALLED_PKG:
+                button = mCheckFilterInstalled;
+                break;
+            case TOGGLE_SHOW_OBSOLETE_PKG:
+                button = mCheckFilterObsolete;
+                break;
+            case TOGGLE_SHOW_UPDATE_NEW_PKG:
+                button = mCheckFilterNew;
+                break;
+            case SORT_API_LEVEL:
+                button = mCheckSortApi;
+                break;
+            case SORT_SOURCE:
+                button = mCheckSortSource;
+                break;
+            case RELOAD:
+            case SHOW_ADDON_SITES:
+                // No checkmark to update
+                break;
+            }
+
+            if (button != null && !button.isDisposed()) {
+                value = button.getSelection();
+            }
+
+            if (!item.isDisposed()) {
+                item.setSelection(value);
+            }
+        }
+    }
+
+    private void postCreate() {
+        mImpl.postCreate();
+
+        if (mImpl.mSwtUpdaterData != null) {
+            mTextSdkOsPath.setText(mImpl.mSwtUpdaterData.getOsSdkRoot());
+        }
+
+        ((PkgContentProvider) mTreeViewer.getContentProvider()).setDisplayArchives(
+                mDisplayArchives);
+
+        ColumnViewerToolTipSupport.enableFor(mTreeViewer, ToolTip.NO_RECREATE);
+
+        Tree tree = mTreeViewer.getTree();
+        FontData fontData = tree.getFont().getFontData()[0];
+        fontData.setStyle(SWT.ITALIC);
+        mTreeFontItalic = new Font(tree.getDisplay(), fontData);
+
+        tree.addDisposeListener(new DisposeListener() {
+            @Override
+            public void widgetDisposed(DisposeEvent e) {
+                mTreeFontItalic.dispose();
+                mTreeFontItalic = null;
+            }
+        });
+    }
+
+    private void loadPackages(boolean useLocalCache, boolean overrideExisting) {
+        if (mImpl.mSwtUpdaterData == null) {
+            return;
+        }
+
+        // LoadPackage is synchronous but does not block the UI.
+        // Consequently it's entirely possible for the user
+        // to request the app to close whilst the packages are loading. Any
+        // action done after loadPackages must check the UI hasn't been
+        // disposed yet. Otherwise hilarity ensues.
+
+        boolean displaySortByApi = isSortByApi();
+
+        if (mTreeColumnName.isDisposed()) {
+            // If the UI got disposed, don't try to load anything since we won't be
+            // able to display it anyway.
+            return;
+        }
+
+        mTreeColumnName.setImage(getImage(
+                displaySortByApi ? PackagesPageIcons.ICON_SORT_BY_API
+                                 : PackagesPageIcons.ICON_SORT_BY_SOURCE));
+
+        mImpl.loadPackagesImpl(useLocalCache, overrideExisting);
+    }
+
+    private void refreshViewerInput() {
+        // Dynamically update the table while we load after each source.
+        // Since the official Android source gets loaded first, it makes the
+        // window look non-empty a lot sooner.
+        if (!mGroupPackages.isDisposed()) {
+            try {
+                mImpl.setViewerInput();
+            } catch (Exception ignore) {}
+
+            // set the initial expanded state
+            expandInitial(mTreeViewer.getInput());
+
+            updateButtonsState();
+            updateMenuCheckmarks();
+        }
+    }
+
+    private boolean isSortByApi() {
+        return mCheckSortApi != null && !mCheckSortApi.isDisposed() && mCheckSortApi.getSelection();
+    }
+
+    /**
+     * Decide whether to keep an item in the current tree based on user-chosen filter options.
+     */
+    private boolean filterViewerItem(Object treeElement) {
+        if (treeElement instanceof PkgCategory) {
+            PkgCategory cat = (PkgCategory) treeElement;
+
+            if (!cat.getItems().isEmpty()) {
+                // A category is hidden if all of its content is hidden.
+                // However empty categories are always visible.
+                for (PkgItem item : cat.getItems()) {
+                    if (filterViewerItem(item)) {
+                        // We found at least one element that is visible.
+                        return true;
+                    }
+                }
+                return false;
+            }
+        }
+
+        if (treeElement instanceof PkgItem) {
+            PkgItem item = (PkgItem) treeElement;
+
+            if (!mCheckFilterObsolete.getSelection()) {
+                if (item.isObsolete()) {
+                    return false;
+                }
+            }
+
+            if (!mCheckFilterInstalled.getSelection()) {
+                if (item.getState() == PkgState.INSTALLED) {
+                    return false;
+                }
+            }
+
+            if (!mCheckFilterNew.getSelection()) {
+                if (item.getState() == PkgState.NEW || item.hasUpdatePkg()) {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Performs the initial expansion of the tree. This expands categories that contain
+     * at least one installed item and collapses the ones with nothing installed.
+     *
+     * TODO: change this to only change the expanded state on categories that have not
+     * been touched by the user yet. Once we do that, call this every time a new source
+     * is added or the list is reloaded.
+     */
+    private void expandInitial(Object elem) {
+        if (elem == null) {
+            return;
+        }
+        if (mTreeViewer != null && !mTreeViewer.getTree().isDisposed()) {
+
+            boolean enablePreviews =
+                mImpl.mSwtUpdaterData.getSettingsController().getSettings().getEnablePreviews();
+
+            mTreeViewer.setExpandedState(elem, true);
+            nextCategory: for (Object pkg :
+                    ((ITreeContentProvider) mTreeViewer.getContentProvider()).
+                        getChildren(elem)) {
+                if (pkg instanceof PkgCategory) {
+                    PkgCategory cat = (PkgCategory) pkg;
+
+                    // Always expand the Tools category (and the preview one, if enabled)
+                    if (cat.getKey().equals(PkgCategoryApi.KEY_TOOLS) ||
+                            (enablePreviews &&
+                                    cat.getKey().equals(PkgCategoryApi.KEY_TOOLS_PREVIEW))) {
+                        expandInitial(pkg);
+                        continue nextCategory;
+                    }
+
+
+                    for (PkgItem item : cat.getItems()) {
+                        if (item.getState() == PkgState.INSTALLED) {
+                            expandInitial(pkg);
+                            continue nextCategory;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Handle checking and unchecking of the tree items.
+     *
+     * When unchecking, all sub-tree items checkboxes are cleared too.
+     * When checking a source, all of its packages are checked too.
+     * When checking a package, only its compatible archives are checked.
+     */
+    private void onTreeCheckStateChanged(CheckStateChangedEvent event) {
+        boolean checked = event.getChecked();
+        Object elem = event.getElement();
+
+        assert event.getSource() == mTreeViewer;
+
+        // When selecting, we want to only select compatible archives and expand the super nodes.
+        checkAndExpandItem(elem, checked, true/*fixChildren*/, true/*fixParent*/);
+        updateButtonsState();
+    }
+
+    private void onTreeDoubleClick(DoubleClickEvent event) {
+        assert event.getSource() == mTreeViewer;
+        ISelection sel = event.getSelection();
+        if (sel.isEmpty() || !(sel instanceof ITreeSelection)) {
+            return;
+        }
+        ITreeSelection tsel = (ITreeSelection) sel;
+        Object elem = tsel.getFirstElement();
+        if (elem == null) {
+            return;
+        }
+
+        ITreeContentProvider provider =
+            (ITreeContentProvider) mTreeViewer.getContentProvider();
+        Object[] children = provider.getElements(elem);
+        if (children == null) {
+            return;
+        }
+
+        if (children.length > 0) {
+            // If the element has children, expand/collapse it.
+            if (mTreeViewer.getExpandedState(elem)) {
+                mTreeViewer.collapseToLevel(elem, 1);
+            } else {
+                mTreeViewer.expandToLevel(elem, 1);
+            }
+        } else {
+            // If the element is a terminal one, select/deselect it.
+            checkAndExpandItem(
+                    elem,
+                    !mTreeViewer.getChecked(elem),
+                    false /*fixChildren*/,
+                    true /*fixParent*/);
+            updateButtonsState();
+        }
+    }
+
+    private void checkAndExpandItem(
+            Object elem,
+            boolean checked,
+            boolean fixChildren,
+            boolean fixParent) {
+        ITreeContentProvider provider =
+            (ITreeContentProvider) mTreeViewer.getContentProvider();
+
+        // fix the item itself
+        if (checked != mTreeViewer.getChecked(elem)) {
+            mTreeViewer.setChecked(elem, checked);
+        }
+        if (elem instanceof PkgItem) {
+            // update the PkgItem to reflect the selection
+            ((PkgItem) elem).setChecked(checked);
+        }
+
+        if (!checked) {
+            if (fixChildren) {
+                // when de-selecting, we deselect all children too
+                mTreeViewer.setSubtreeChecked(elem, checked);
+                for (Object child : provider.getChildren(elem)) {
+                    checkAndExpandItem(child, checked, fixChildren, false/*fixParent*/);
+                }
+            }
+
+            // fix the parent when deselecting
+            if (fixParent) {
+                Object parent = provider.getParent(elem);
+                if (parent != null && mTreeViewer.getChecked(parent)) {
+                    mTreeViewer.setChecked(parent, false);
+                }
+            }
+            return;
+        }
+
+        // When selecting, we also select sub-items (for a category)
+        if (fixChildren) {
+            if (elem instanceof PkgCategory || elem instanceof PkgItem) {
+                Object[] children = provider.getChildren(elem);
+                for (Object child : children) {
+                    checkAndExpandItem(child, true, fixChildren, false/*fixParent*/);
+                }
+                // only fix the parent once the last sub-item is set
+                if (elem instanceof PkgCategory) {
+                    if (children.length > 0) {
+                        checkAndExpandItem(
+                                children[0], true, false/*fixChildren*/, true/*fixParent*/);
+                    } else {
+                        mTreeViewer.setChecked(elem, false);
+                    }
+                }
+            } else if (elem instanceof Package) {
+                // in details mode, we auto-select compatible packages
+                selectCompatibleArchives(elem, provider);
+            }
+        }
+
+        if (fixParent && checked && elem instanceof PkgItem) {
+            Object parent = provider.getParent(elem);
+            if (!mTreeViewer.getChecked(parent)) {
+                Object[] children = provider.getChildren(parent);
+                boolean allChecked = children.length > 0;
+                for (Object e : children) {
+                    if (!mTreeViewer.getChecked(e)) {
+                        allChecked = false;
+                        break;
+                    }
+                }
+                if (allChecked) {
+                    mTreeViewer.setChecked(parent, true);
+                }
+            }
+        }
+    }
+
+    private void selectCompatibleArchives(Object pkg, ITreeContentProvider provider) {
+        for (Object archive : provider.getChildren(pkg)) {
+            if (archive instanceof Archive) {
+                mTreeViewer.setChecked(archive, ((Archive) archive).isCompatible());
+            }
+        }
+    }
+
+    /**
+     * Checks all PkgItems that are either new or have updates or select top platform
+     * for initial run.
+     */
+    private void onSelectNewUpdates(boolean selectNew, boolean selectUpdates, boolean selectTop) {
+        // This will update the tree's "selected" state and then invoke syncViewerSelection()
+        // which will in turn update tree.
+        mImpl.onSelectNewUpdates(selectNew, selectUpdates, selectTop);
+    }
+
+    /**
+     * Deselect all checked PkgItems.
+     */
+    private void onDeselectAll() {
+        // This does not update the tree itself, syncViewerSelection does it below.
+        mImpl.onDeselectAll();
+        syncViewerSelection();
+    }
+
+    /**
+     * When switching between the tree-by-api and the tree-by-source, copy the selection
+     * (aka the checked items) from one list to the other.
+     * This does not update the tree itself.
+     */
+    private void copySelection(boolean fromSourceToApi) {
+        List<PkgItem> fromItems =
+            mImpl.mDiffLogic.getAllPkgItems(!fromSourceToApi, fromSourceToApi);
+        List<PkgItem> toItems =
+            mImpl.mDiffLogic.getAllPkgItems(fromSourceToApi, !fromSourceToApi);
+
+        // deselect all targets
+        for (PkgItem item : toItems) {
+            item.setChecked(false);
+        }
+
+        // mark new one from the source
+        for (PkgItem source : fromItems) {
+            if (source.isChecked()) {
+                // There should typically be a corresponding item in the target side
+                for (PkgItem target : toItems) {
+                    if (target.isSameMainPackageAs(source.getMainPackage())) {
+                        target.setChecked(true);
+                        break;
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Synchronize the 'checked' state of PkgItems in the tree with their internal isChecked state.
+     */
+    private void syncViewerSelection() {
+        ITreeContentProvider provider = (ITreeContentProvider) mTreeViewer.getContentProvider();
+
+        Object input = mTreeViewer.getInput();
+        if (input != null) {
+            for (Object cat : provider.getElements(input)) {
+                Object[] children = provider.getElements(cat);
+                boolean allChecked = children.length > 0;
+                for (Object child : children) {
+                    if (child instanceof PkgItem) {
+                        PkgItem item = (PkgItem) child;
+                        boolean checked = item.isChecked();
+                        allChecked &= checked;
+
+                        if (checked != mTreeViewer.getChecked(item)) {
+                            if (checked) {
+                                if (!mTreeViewer.getExpandedState(cat)) {
+                                    mTreeViewer.setExpandedState(cat, true);
+                                }
+                            }
+                            checkAndExpandItem(
+                                    item,
+                                    checked,
+                                    true/*fixChildren*/,
+                                    false/*fixParent*/);
+                        }
+                    }
+                }
+
+                if (allChecked != mTreeViewer.getChecked(cat)) {
+                    mTreeViewer.setChecked(cat, allChecked);
+                }
+            }
+        }
+
+        updateButtonsState();
+    }
+
+    /**
+     * Indicate an install/delete operation is pending.
+     * This disables the install/delete buttons.
+     * Use {@link #endOperationPending()} to revert, typically in a {@code try..finally} block.
+     */
+    private void beginOperationPending() {
+        mOperationPending = true;
+        updateButtonsState();
+    }
+
+    private void endOperationPending() {
+        mOperationPending = false;
+        updateButtonsState();
+    }
+
+    /**
+     * Updates the Install and Delete Package buttons.
+     */
+    private void updateButtonsState() {
+        if (!mButtonInstall.isDisposed()) {
+            int numPackages = getArchivesForInstall(null /*archives*/);
+
+            mButtonInstall.setEnabled((numPackages > 0) && !mOperationPending);
+            mButtonInstall.setText(
+                    numPackages == 0 ? "Install packages..." :          // disabled button case
+                        numPackages == 1 ? "Install 1 package..." :
+                            String.format("Install %d packages...", numPackages));
+        }
+
+        if (!mButtonDelete.isDisposed()) {
+            // We can only delete local archives
+            int numPackages = getArchivesToDelete(null /*outMsg*/, null /*outArchives*/);
+
+            mButtonDelete.setEnabled((numPackages > 0) && !mOperationPending);
+            mButtonDelete.setText(
+                    numPackages == 0 ? "Delete packages..." :           // disabled button case
+                        numPackages == 1 ? "Delete 1 package..." :
+                            String.format("Delete %d packages...", numPackages));
+        }
+    }
+
+    /**
+     * Called when the Install Package button is selected.
+     * Collects the packages to be installed and shows the installation window.
+     */
+    private void onButtonInstall() {
+        ArrayList<Archive> archives = new ArrayList<Archive>();
+        getArchivesForInstall(archives);
+
+        if (mImpl.mSwtUpdaterData != null) {
+            boolean needsRefresh = false;
+            try {
+                beginOperationPending();
+
+                List<Archive> installed = mImpl.mSwtUpdaterData.updateOrInstallAll_WithGUI(
+                    archives,
+                    mCheckFilterObsolete.getSelection() /* includeObsoletes */,
+                    mContext == SdkInvocationContext.IDE ?
+                            SwtUpdaterData.TOOLS_MSG_UPDATED_FROM_ADT :
+                                SwtUpdaterData.TOOLS_MSG_UPDATED_FROM_SDKMAN);
+                needsRefresh = installed != null && !installed.isEmpty();
+            } finally {
+                endOperationPending();
+
+                if (needsRefresh) {
+                    // The local package list has changed, make sure to refresh it
+                    mImpl.localReload();
+                }
+            }
+        }
+    }
+
+    /**
+     * Selects the archives that can be installed.
+     * This can be used with a null {@code outArchives} just to count the number of
+     * installable archives.
+     *
+     * @param outArchives An archive list where to add the archives that can be installed.
+     *   This can be null.
+     * @return The number of archives that can be installed.
+     */
+    private int getArchivesForInstall(List<Archive> outArchives) {
+        if (mTreeViewer == null ||
+                mTreeViewer.getTree() == null ||
+                mTreeViewer.getTree().isDisposed()) {
+            return 0;
+        }
+        Object[] checked = mTreeViewer.getCheckedElements();
+        if (checked == null) {
+            return 0;
+        }
+
+        int count = 0;
+
+        // Give us a way to force install of incompatible archives.
+        boolean checkIsCompatible =
+            System.getenv(ArchiveInstaller.ENV_VAR_IGNORE_COMPAT) == null;
+
+        if (mDisplayArchives) {
+            // In detail mode, we display archives so we can install only the
+            // archives that are actually selected.
+
+            for (Object c : checked) {
+                if (c instanceof Archive) {
+                    Archive a = (Archive) c;
+                    if (a != null) {
+                        if (checkIsCompatible && !a.isCompatible()) {
+                            continue;
+                        }
+                        count++;
+                        if (outArchives != null) {
+                            outArchives.add((Archive) c);
+                        }
+                    }
+                }
+            }
+        } else {
+            // In non-detail mode, we install all the compatible archives
+            // found in the selected pkg items. We also automatically
+            // select update packages rather than the root package if any.
+
+            for (Object c : checked) {
+                Package p = null;
+                if (c instanceof Package) {
+                    // This is an update package
+                    p = (Package) c;
+                } else if (c instanceof PkgItem) {
+                    p = ((PkgItem) c).getMainPackage();
+
+                    PkgItem pi = (PkgItem) c;
+                    if (pi.getState() == PkgState.INSTALLED) {
+                        // We don't allow installing items that are already installed
+                        // unless they have a pending update.
+                        p = pi.getUpdatePkg();
+
+                    } else if (pi.getState() == PkgState.NEW) {
+                        p = pi.getMainPackage();
+                    }
+                }
+                if (p != null) {
+                    for (Archive a : p.getArchives()) {
+                        if (a != null) {
+                            if (checkIsCompatible && !a.isCompatible()) {
+                                continue;
+                            }
+                            count++;
+                            if (outArchives != null) {
+                                outArchives.add(a);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        return count;
+    }
+
+    /**
+     * Called when the Delete Package button is selected.
+     * Collects the packages to be deleted, prompt the user for confirmation
+     * and actually performs the deletion.
+     */
+    private void onButtonDelete() {
+        final String title = "Delete SDK Package";
+        StringBuilder msg = new StringBuilder("Are you sure you want to delete:");
+
+        // A list of archives to delete
+        final ArrayList<Archive> archives = new ArrayList<Archive>();
+
+        getArchivesToDelete(msg, archives);
+
+        if (!archives.isEmpty()) {
+            msg.append("\n").append("This cannot be undone.");  //$NON-NLS-1$
+            if (MessageDialog.openQuestion(getShell(), title, msg.toString())) {
+                try {
+                    beginOperationPending();
+
+                    mImpl.mSwtUpdaterData.getTaskFactory().start("Delete Package", new ITask() {
+                        @Override
+                        public void run(ITaskMonitor monitor) {
+                            monitor.setProgressMax(archives.size() + 1);
+                            for (Archive a : archives) {
+                                monitor.setDescription("Deleting '%1$s' (%2$s)",
+                                        a.getParentPackage().getShortDescription(),
+                                        a.getLocalOsPath());
+
+                                // Delete the actual package
+                                a.deleteLocal();
+
+                                monitor.incProgress(1);
+                                if (monitor.isCancelRequested()) {
+                                    break;
+                                }
+                            }
+
+                            monitor.incProgress(1);
+                            monitor.setDescription("Done");
+                        }
+                    });
+                } finally {
+                    endOperationPending();
+
+                    // The local package list has changed, make sure to refresh it
+                    mImpl.localReload();
+                }
+            }
+        }
+    }
+
+    /**
+     * Selects the archives that can be deleted and collect their names.
+     * This can be used with a null {@code outArchives} and a null {@code outMsg}
+     * just to count the number of archives to be deleted.
+     *
+     * @param outMsg A StringBuilder where the names of the packages to be deleted is
+     *   accumulated. This is used to confirm deletion with the user.
+     * @param outArchives An archive list where to add the archives that can be installed.
+     *   This can be null.
+     * @return The number of archives that can be deleted.
+     */
+    private int getArchivesToDelete(StringBuilder outMsg, List<Archive> outArchives) {
+        if (mTreeViewer == null ||
+                mTreeViewer.getTree() == null ||
+                mTreeViewer.getTree().isDisposed()) {
+            return 0;
+        }
+        Object[] checked = mTreeViewer.getCheckedElements();
+        if (checked == null) {
+            // This should not happen since the button should be disabled
+            return 0;
+        }
+
+        int count = 0;
+
+        if (mDisplayArchives) {
+            // In detail mode, select archives that can be deleted
+
+            for (Object c : checked) {
+                if (c instanceof Archive) {
+                    Archive a = (Archive) c;
+                    if (a != null && a.isLocal()) {
+                        count++;
+                        if (outMsg != null) {
+                            String osPath = a.getLocalOsPath();
+                            File dir = new File(osPath);
+                            Package p = a.getParentPackage();
+                            if (p != null && dir.isDirectory()) {
+                                outMsg.append("\n - ")    //$NON-NLS-1$
+                                      .append(p.getShortDescription());
+                            }
+                        }
+                        if (outArchives != null) {
+                            outArchives.add(a);
+                        }
+                    }
+                }
+            }
+        } else {
+            // In non-detail mode, select archives of selected packages that can be deleted.
+
+            for (Object c : checked) {
+                if (c instanceof PkgItem) {
+                    PkgItem pi = (PkgItem) c;
+                    PkgState state = pi.getState();
+                    if (state == PkgState.INSTALLED) {
+                        Package p = pi.getMainPackage();
+
+                        for (Archive a : p.getArchives()) {
+                            if (a != null && a.isLocal()) {
+                                count++;
+                                if (outMsg != null) {
+                                    String osPath = a.getLocalOsPath();
+                                    File dir = new File(osPath);
+                                    if (dir.isDirectory()) {
+                                        outMsg.append("\n - ")    //$NON-NLS-1$
+                                              .append(p.getShortDescription());
+                                    }
+                                }
+                                if (outArchives != null) {
+                                    outArchives.add(a);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        return count;
+    }
+
+    // ----------------------
+
+
+    // --- Implementation of ISdkChangeListener ---
+
+    @Override
+    public void onSdkLoaded() {
+        onSdkReload();
+    }
+
+    @Override
+    public void onSdkReload() {
+        // The sdkmanager finished reloading its data. We must not call localReload() from here
+        // since we don't want to alter the sdkmanager's data that just finished loading.
+        mImpl.loadPackages();
+    }
+
+    @Override
+    public void preInstallHook() {
+        // nothing to be done for now.
+    }
+
+    @Override
+    public void postInstallHook() {
+        // nothing to be done for now.
+    }
+
+
+    // --- End of hiding from SWT Designer ---
+    //$hide<<$
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPageIcons.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPageIcons.java
new file mode 100755
index 0000000..4fe8fca
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPageIcons.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository.ui;
+
+
+/**
+ * Icons used by {@link PackagesPage}.
+ */
+public class PackagesPageIcons {
+
+    public static final String ICON_CAT_OTHER      = "pkgcat_other_16.png";    //$NON-NLS-1$
+    public static final String ICON_CAT_PLATFORM   = "pkgcat_16.png";          //$NON-NLS-1$
+    public static final String ICON_SORT_BY_SOURCE = "source_icon16.png";      //$NON-NLS-1$
+    public static final String ICON_SORT_BY_API    = "platform_pkg_16.png";    //$NON-NLS-1$
+    public static final String ICON_PKG_NEW        = "pkg_new_16.png";         //$NON-NLS-1$
+    public static final String ICON_PKG_INCOMPAT   = "pkg_incompat_16.png";    //$NON-NLS-1$
+    public static final String ICON_PKG_UPDATE     = "pkg_update_16.png";      //$NON-NLS-1$
+    public static final String ICON_PKG_INSTALLED  = "pkg_installed_16.png";   //$NON-NLS-1$
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPageImpl.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPageImpl.java
new file mode 100755
index 0000000..5e6ac7f
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPageImpl.java
@@ -0,0 +1,574 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository.ui;
+
+import com.android.SdkConstants;
+import com.android.sdklib.internal.repository.DownloadCache;
+import com.android.sdklib.internal.repository.DownloadCache.Strategy;
+import com.android.sdklib.internal.repository.IDescription;
+import com.android.sdklib.internal.repository.archives.Archive;
+import com.android.sdklib.internal.repository.packages.Package;
+import com.android.sdklib.internal.repository.sources.SdkSource;
+import com.android.sdklib.internal.repository.updater.PackageLoader;
+import com.android.sdklib.internal.repository.updater.PkgItem;
+import com.android.sdklib.internal.repository.updater.PackageLoader.ISourceLoadedCallback;
+import com.android.sdklib.internal.repository.updater.PkgItem.PkgState;
+import com.android.sdkuilib.internal.repository.SwtUpdaterData;
+import com.android.sdkuilib.internal.repository.core.PackagesDiffLogic;
+import com.android.sdkuilib.internal.repository.core.PkgCategory;
+import com.android.sdkuilib.internal.repository.core.PkgCategoryApi;
+import com.android.sdkuilib.internal.repository.core.PkgContentProvider;
+import com.android.sdkuilib.internal.repository.icons.ImageFactory;
+
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.IInputProvider;
+import org.eclipse.jface.viewers.ITableFontProvider;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.List;
+
+/**
+ * Base class for {@link PackagesPage} that holds most of the logic to display
+ * the tree/list of packages. This class holds most of the logic and {@link PackagesPage}
+ * holds most of the UI (creating the UI, dealing with menus and buttons and tree
+ * selection.) This makes it easier to test the functionality by mocking only a
+ * subset of the UI.
+ */
+abstract class PackagesPageImpl {
+
+    final SwtUpdaterData mSwtUpdaterData;
+    final PackagesDiffLogic mDiffLogic;
+
+    private ICheckboxTreeViewer mITreeViewer;
+    private ITreeViewerColumn   mIColumnName;
+    private ITreeViewerColumn   mIColumnApi;
+    private ITreeViewerColumn   mIColumnRevision;
+    private ITreeViewerColumn   mIColumnStatus;
+
+    PackagesPageImpl(SwtUpdaterData swtUpdaterData) {
+        mSwtUpdaterData = swtUpdaterData;
+        mDiffLogic = new PackagesDiffLogic(swtUpdaterData);
+    }
+
+    /**
+     * Utility method that derived classes can override to check whether the UI is disposed.
+     * When the UI is disposed, most operations that affect the UI will be bypassed.
+     * @return True if UI is not available and should not be touched.
+     */
+    abstract protected boolean isUiDisposed();
+
+    /**
+     * Utility method to execute a runnable on the main UI thread.
+     * Will do nothing if {@link #isUiDisposed()} returns false.
+     * @param runnable The runnable to execute on the main UI thread.
+     */
+    abstract protected void syncExec(Runnable runnable);
+
+    /**
+     * Synchronizes the 'checked' state of PkgItems in the tree with their internal isChecked state.
+     */
+    abstract protected void syncViewerSelection();
+
+    void performFirstLoad() {
+        // First a package loader is created that only checks
+        // the local cache xml files. It populates the package
+        // list based on what the client got last, essentially.
+        loadPackages(true /*useLocalCache*/, false /*overrideExisting*/);
+
+        // Next a regular package loader is created that will
+        // respect the expiration and refresh parameters of the
+        // download cache.
+        loadPackages(false /*useLocalCache*/, true /*overrideExisting*/);
+    }
+
+    public void setITreeViewer(ICheckboxTreeViewer iTreeViewer) {
+        mITreeViewer = iTreeViewer;
+    }
+
+    public void setIColumns(
+            ITreeViewerColumn columnName,
+            ITreeViewerColumn columnApi,
+            ITreeViewerColumn columnRevision,
+            ITreeViewerColumn columnStatus) {
+        mIColumnName = columnName;
+        mIColumnApi = columnApi;
+        mIColumnRevision = columnRevision;
+        mIColumnStatus = columnStatus;
+    }
+
+    void postCreate() {
+        // Caller needs to call setITreeViewer before this.
+        assert mITreeViewer     != null;
+        // Caller needs to call setIColumns before this.
+        assert mIColumnApi      != null;
+        assert mIColumnName     != null;
+        assert mIColumnStatus   != null;
+        assert mIColumnRevision != null;
+
+        mITreeViewer.setContentProvider(new PkgContentProvider(mITreeViewer));
+
+        mIColumnApi.setLabelProvider(
+                new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mIColumnApi)));
+        mIColumnName.setLabelProvider(
+                new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mIColumnName)));
+        mIColumnStatus.setLabelProvider(
+                new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mIColumnStatus)));
+        mIColumnRevision.setLabelProvider(
+                new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mIColumnRevision)));
+    }
+
+    /**
+     * Performs a full reload by removing all cached packages data, including the platforms
+     * and addons from the sdkmanager instance. This will perform a full local parsing
+     * as well as a full reload of the remote data (by fetching all sources again.)
+     */
+    void fullReload() {
+        // Clear all source information, forcing them to be refreshed.
+        mSwtUpdaterData.getSources().clearAllPackages();
+        // Clear and reload all local data too.
+        localReload();
+    }
+
+    /**
+     * Performs a full reload of all the local package information, including the platforms
+     * and addons from the sdkmanager instance. This will perform a full local parsing.
+     * <p/>
+     * This method does NOT force a new fetch of the remote sources.
+     *
+     * @see #fullReload()
+     */
+    void localReload() {
+        // Clear all source caches, otherwise loading will use the cached data
+        mSwtUpdaterData.getLocalSdkParser().clearPackages();
+        mSwtUpdaterData.getSdkManager().reloadSdk(mSwtUpdaterData.getSdkLog());
+        loadPackages();
+    }
+
+    /**
+     * Performs a "normal" reload of the package information, use the default download
+     * cache and refreshing strategy as needed.
+     */
+    void loadPackages() {
+        loadPackages(false /*useLocalCache*/, false /*overrideExisting*/);
+    }
+
+    /**
+     * Performs a reload of the package information.
+     *
+     * @param useLocalCache When true, the {@link PackageLoader} is switched to use
+     *  a specific {@link DownloadCache} using the {@link Strategy#ONLY_CACHE}, meaning
+     *  it will only use data from the local cache. It will not try to fetch or refresh
+     *  manifests. This is used once the very first time the sdk manager window opens
+     *  and is typically followed by a regular load with refresh.
+     */
+    abstract protected void loadPackages(boolean useLocalCache, boolean overrideExisting);
+
+    /**
+     * Actual implementation of {@link #loadPackages(boolean, boolean)}.
+     * Derived implementations must call this to do the actual work after setting up the UI.
+     */
+    void loadPackagesImpl(final boolean useLocalCache, final boolean overrideExisting) {
+        if (mSwtUpdaterData == null) {
+            return;
+        }
+
+        final boolean displaySortByApi = isSortByApi();
+
+        PackageLoader packageLoader = getPackageLoader(useLocalCache);
+        assert packageLoader != null;
+
+        mDiffLogic.updateStart();
+        packageLoader.loadPackages(overrideExisting, new ISourceLoadedCallback() {
+            @Override
+            public boolean onUpdateSource(SdkSource source, Package[] newPackages) {
+                // This runs in a thread and must not access UI directly.
+                final boolean changed = mDiffLogic.updateSourcePackages(
+                        displaySortByApi, source, newPackages);
+
+                syncExec(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (changed ||
+                            mITreeViewer.getInput() != mDiffLogic.getCategories(isSortByApi())) {
+                                refreshViewerInput();
+                        }
+                    }
+                });
+
+                // Return true to tell the loader to continue with the next source.
+                // Return false to stop the loader if any UI has been disposed, which can
+                // happen if the user is trying to close the window during the load operation.
+                return !isUiDisposed();
+            }
+
+            @Override
+            public void onLoadCompleted() {
+                // This runs in a thread and must not access UI directly.
+                final boolean changed = mDiffLogic.updateEnd(displaySortByApi);
+
+                syncExec(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (changed ||
+                            mITreeViewer.getInput() != mDiffLogic.getCategories(isSortByApi())) {
+                            try {
+                                refreshViewerInput();
+                            } catch (Exception ignore) {}
+                        }
+
+                        if (!useLocalCache &&
+                                mDiffLogic.isFirstLoadComplete() &&
+                                !isUiDisposed()) {
+                            // At the end of the first load, if nothing is selected then
+                            // automatically select all new and update packages.
+                            Object[] checked = mITreeViewer.getCheckedElements();
+                            if (checked == null || checked.length == 0) {
+                                onSelectNewUpdates(
+                                        false, //selectNew
+                                        true,  //selectUpdates,
+                                        true); //selectTop
+                            }
+                        }
+                    }
+                });
+            }
+        });
+    }
+
+    /**
+     * Used by {@link #loadPackagesImpl(boolean, boolean)} to get the package
+     * loader for the first or second pass update. When starting the manager
+     * starts with a first pass that reads only from the local cache, with no
+     * extra network access. That's {@code useLocalCache} being true.
+     * <p/>
+     * Leter it does a second pass with {@code useLocalCache} set to false
+     * and actually uses the download cache specified in {@link SwtUpdaterData}.
+     *
+     * This is extracted so that we can control this cache via unit tests.
+     */
+    protected PackageLoader getPackageLoader(boolean useLocalCache) {
+        if (useLocalCache) {
+            return new PackageLoader(mSwtUpdaterData, new DownloadCache(Strategy.ONLY_CACHE));
+        } else {
+            return mSwtUpdaterData.getPackageLoader();
+        }
+    }
+
+    /**
+     * Overridden by the UI to respond to a request to refresh the tree viewer
+     * when the input has changed.
+     * The implementation must call {@link #setViewerInput()} somehow and will
+     * also need to adjust the expand state of the tree items and/or update
+     * some buttons or other state.
+     */
+    abstract protected void refreshViewerInput();
+
+    /**
+     * Invoked from {@link #refreshViewerInput()} to actually either set the
+     * input of the tree viewer or refresh it if it's the <em>same</em> input
+     * object.
+     */
+    protected void setViewerInput() {
+        List<PkgCategory> cats = mDiffLogic.getCategories(isSortByApi());
+        if (mITreeViewer.getInput() != cats) {
+            // set initial input
+            mITreeViewer.setInput(cats);
+        } else {
+            // refresh existing, which preserves the expanded state, the selection
+            // and the checked state.
+            mITreeViewer.refresh();
+        }
+    }
+
+    /**
+     * Overridden by the UI to determine if the tree should display packages sorted
+     * by API (returns true) or by repository source (returns false.)
+     */
+    abstract protected boolean isSortByApi();
+
+    /**
+     * Checks all PkgItems that are either new or have updates or select top platform
+     * for initial run.
+     */
+    void onSelectNewUpdates(boolean selectNew, boolean selectUpdates, boolean selectTop) {
+        // This does not update the tree itself, syncViewerSelection does it in the caller.
+        mDiffLogic.checkNewUpdateItems(
+                selectNew,
+                selectUpdates,
+                selectTop,
+                SdkConstants.CURRENT_PLATFORM);
+        syncViewerSelection();
+    }
+
+    /**
+     * Deselect all checked PkgItems.
+     */
+    void onDeselectAll() {
+        // This does not update the tree itself, syncViewerSelection does it in the caller.
+        mDiffLogic.uncheckAllItems();
+    }
+
+    // ----------------------
+
+    abstract protected Font getTreeFontItalic();
+
+    class PkgCellLabelProvider extends ColumnLabelProvider implements ITableFontProvider {
+
+        private final ITreeViewerColumn mColumn;
+
+        public PkgCellLabelProvider(ITreeViewerColumn column) {
+            super();
+            mColumn = column;
+        }
+
+        @Override
+        public String getText(Object element) {
+
+            if (mColumn == mIColumnName) {
+                if (element instanceof PkgCategory) {
+                    return ((PkgCategory) element).getLabel();
+                } else if (element instanceof PkgItem) {
+                    return getPkgItemName((PkgItem) element);
+                } else if (element instanceof IDescription) {
+                    return ((IDescription) element).getShortDescription();
+                }
+
+            } else if (mColumn == mIColumnApi) {
+                int api = -1;
+                if (element instanceof PkgItem) {
+                    api = ((PkgItem) element).getApi();
+                }
+                if (api >= 1) {
+                    return Integer.toString(api);
+                }
+
+            } else if (mColumn == mIColumnRevision) {
+                if (element instanceof PkgItem) {
+                    PkgItem pkg = (PkgItem) element;
+                    return pkg.getRevision().toShortString();
+                }
+
+            } else if (mColumn == mIColumnStatus) {
+                if (element instanceof PkgItem) {
+                    PkgItem pkg = (PkgItem) element;
+
+                    switch(pkg.getState()) {
+                    case INSTALLED:
+                        Package update = pkg.getUpdatePkg();
+                        if (update != null) {
+                            return String.format(
+                                    "Update available: rev. %1$s",
+                                    update.getRevision().toShortString());
+                        }
+                        return "Installed";
+
+                    case NEW:
+                        Package p = pkg.getMainPackage();
+                        if (p != null && p.hasCompatibleArchive()) {
+                            return "Not installed";
+                        } else {
+                            return String.format("Not compatible with %1$s",
+                                    SdkConstants.currentPlatformName());
+                        }
+                    }
+                    return pkg.getState().toString();
+
+                } else if (element instanceof Package) {
+                    // This is an update package.
+                    return "New revision " + ((Package) element).getRevision().toShortString();
+                }
+            }
+
+            return ""; //$NON-NLS-1$
+        }
+
+        private String getPkgItemName(PkgItem item) {
+            String name = item.getName().trim();
+
+            if (isSortByApi()) {
+                // When sorting by API, the package name might contains the API number
+                // or the platform name at the end. If we find it, cut it out since it's
+                // redundant.
+
+                PkgCategoryApi cat = (PkgCategoryApi) findCategoryForItem(item);
+                String apiLabel = cat.getApiLabel();
+                String platLabel = cat.getPlatformName();
+
+                if (platLabel != null && name.endsWith(platLabel)) {
+                    return name.substring(0, name.length() - platLabel.length());
+
+                } else if (apiLabel != null && name.endsWith(apiLabel)) {
+                    return name.substring(0, name.length() - apiLabel.length());
+
+                } else if (platLabel != null && item.isObsolete() && name.indexOf(platLabel) > 0) {
+                    // For obsolete items, the format is "<base name> <platform name> (Obsolete)"
+                    // so in this case only accept removing a platform name that is not at
+                    // the end.
+                    name = name.replace(platLabel, ""); //$NON-NLS-1$
+                }
+            }
+
+            // Collapse potential duplicated spacing
+            name = name.replaceAll(" +", " "); //$NON-NLS-1$ //$NON-NLS-2$
+
+            return name;
+        }
+
+        private PkgCategory findCategoryForItem(PkgItem item) {
+            List<PkgCategory> cats = mDiffLogic.getCategories(isSortByApi());
+            for (PkgCategory cat : cats) {
+                for (PkgItem i : cat.getItems()) {
+                    if (i == item) {
+                        return cat;
+                    }
+                }
+            }
+
+            return null;
+        }
+
+        @Override
+        public Image getImage(Object element) {
+            ImageFactory imgFactory = mSwtUpdaterData.getImageFactory();
+
+            if (imgFactory != null) {
+                if (mColumn == mIColumnName) {
+                    if (element instanceof PkgCategory) {
+                        return imgFactory.getImageForObject(((PkgCategory) element).getIconRef());
+                    } else if (element instanceof PkgItem) {
+                        return imgFactory.getImageForObject(((PkgItem) element).getMainPackage());
+                    }
+                    return imgFactory.getImageForObject(element);
+
+                } else if (mColumn == mIColumnStatus && element instanceof PkgItem) {
+                    PkgItem pi = (PkgItem) element;
+                    switch(pi.getState()) {
+                    case INSTALLED:
+                        if (pi.hasUpdatePkg()) {
+                            return imgFactory.getImageByName(PackagesPageIcons.ICON_PKG_UPDATE);
+                        } else {
+                            return imgFactory.getImageByName(PackagesPageIcons.ICON_PKG_INSTALLED);
+                        }
+                    case NEW:
+                        Package p = pi.getMainPackage();
+                        if (p != null && p.hasCompatibleArchive()) {
+                            return imgFactory.getImageByName(PackagesPageIcons.ICON_PKG_NEW);
+                        } else {
+                            return imgFactory.getImageByName(PackagesPageIcons.ICON_PKG_INCOMPAT);
+                        }
+                    }
+                }
+            }
+            return super.getImage(element);
+        }
+
+        // -- ITableFontProvider
+
+        @Override
+        public Font getFont(Object element, int columnIndex) {
+            if (element instanceof PkgItem) {
+                if (((PkgItem) element).getState() == PkgState.NEW) {
+                    return getTreeFontItalic();
+                }
+            } else if (element instanceof Package) {
+                // update package
+                return getTreeFontItalic();
+            }
+            return super.getFont(element);
+        }
+
+        // -- Tooltip support
+
+        @Override
+        public String getToolTipText(Object element) {
+            PkgItem pi = element instanceof PkgItem ? (PkgItem) element : null;
+            if (pi != null) {
+                element = pi.getMainPackage();
+            }
+            if (element instanceof IDescription) {
+                String s = getTooltipDescription((IDescription) element);
+
+                if (pi != null && pi.hasUpdatePkg()) {
+                    s += "\n-----------------" +        //$NON-NLS-1$
+                         "\nUpdate Available:\n" +      //$NON-NLS-1$
+                         getTooltipDescription(pi.getUpdatePkg());
+                }
+
+                return s;
+            }
+            return super.getToolTipText(element);
+        }
+
+        private String getTooltipDescription(IDescription element) {
+            String s = element.getLongDescription();
+            if (element instanceof Package) {
+                Package p = (Package) element;
+
+                if (!p.isLocal()) {
+                    // For non-installed item, try to find a download size
+                    for (Archive a : p.getArchives()) {
+                        if (!a.isLocal() && a.isCompatible()) {
+                            s += '\n' + a.getSizeDescription();
+                            break;
+                        }
+                    }
+                }
+
+                // Display info about where this package comes/came from
+                SdkSource src = p.getParentSource();
+                if (src != null) {
+                    try {
+                        URL url = new URL(src.getUrl());
+                        String host = url.getHost();
+                        if (p.isLocal()) {
+                            s += String.format("\nInstalled from %1$s", host);
+                        } else {
+                            s += String.format("\nProvided by %1$s", host);
+                        }
+                    } catch (MalformedURLException ignore) {
+                    }
+                }
+            }
+            return s;
+        }
+
+        @Override
+        public Point getToolTipShift(Object object) {
+            return new Point(15, 5);
+        }
+
+        @Override
+        public int getToolTipDisplayDelayTime(Object object) {
+            return 500;
+        }
+    }
+
+    interface ICheckboxTreeViewer extends IInputProvider {
+        void setContentProvider(PkgContentProvider pkgContentProvider);
+        void refresh();
+        void setInput(List<PkgCategory> cats);
+        Object[] getCheckedElements();
+    }
+
+    interface ITreeViewerColumn {
+        void setLabelProvider(ColumnLabelProvider labelProvider);
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PkgTreeColumnViewerLabelProvider.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PkgTreeColumnViewerLabelProvider.java
new file mode 100755
index 0000000..3323104
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PkgTreeColumnViewerLabelProvider.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository.ui;
+
+import org.eclipse.jface.viewers.CellLabelProvider;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.TreeColumnViewerLabelProvider;
+import org.eclipse.jface.viewers.TreePath;
+import org.eclipse.jface.viewers.TreeViewerColumn;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+
+/**
+ * A custom version of {@link TreeColumnViewerLabelProvider} which
+ * handles {@link TreePath}s and delegates content to the given
+ * {@link ColumnLabelProvider} for a given {@link TreeViewerColumn}.
+ * <p/>
+ * The implementation handles a variety of providers (table label, table
+ * color, table font) but does not implement a tooltip provider, so we
+ * delegate the calls here to the appropriate {@link ColumnLabelProvider}.
+ * <p/>
+ * Only {@link #getToolTipText(Object)} is really useful for us but we
+ * delegate all the tooltip calls for completeness and avoid surprises later
+ * if we ever decide to override more things in the label provider.
+ */
+class PkgTreeColumnViewerLabelProvider extends TreeColumnViewerLabelProvider {
+
+    private CellLabelProvider mTooltipProvider;
+
+    public PkgTreeColumnViewerLabelProvider(ColumnLabelProvider columnLabelProvider) {
+        super(columnLabelProvider);
+    }
+
+    @Override
+    public void setProviders(Object provider) {
+        super.setProviders(provider);
+        if (provider instanceof CellLabelProvider) {
+            mTooltipProvider = (CellLabelProvider) provider;
+        }
+    }
+
+    @Override
+    public Image getToolTipImage(Object object) {
+        if (mTooltipProvider != null) {
+            return mTooltipProvider.getToolTipImage(object);
+        }
+        return super.getToolTipImage(object);
+    }
+
+    @Override
+    public String getToolTipText(Object element) {
+        if (mTooltipProvider != null) {
+            return mTooltipProvider.getToolTipText(element);
+        }
+        return super.getToolTipText(element);
+    }
+
+    @Override
+    public Color getToolTipBackgroundColor(Object object) {
+        if (mTooltipProvider != null) {
+            return mTooltipProvider.getToolTipBackgroundColor(object);
+        }
+        return super.getToolTipBackgroundColor(object);
+    }
+
+    @Override
+    public Color getToolTipForegroundColor(Object object) {
+        if (mTooltipProvider != null) {
+            return mTooltipProvider.getToolTipForegroundColor(object);
+        }
+        return super.getToolTipForegroundColor(object);
+    }
+
+    @Override
+    public Font getToolTipFont(Object object) {
+        if (mTooltipProvider != null) {
+            return mTooltipProvider.getToolTipFont(object);
+        }
+        return super.getToolTipFont(object);
+    }
+
+    @Override
+    public Point getToolTipShift(Object object) {
+        if (mTooltipProvider != null) {
+            return mTooltipProvider.getToolTipShift(object);
+        }
+        return super.getToolTipShift(object);
+    }
+
+    @Override
+    public boolean useNativeToolTip(Object object) {
+        if (mTooltipProvider != null) {
+            return mTooltipProvider.useNativeToolTip(object);
+        }
+        return super.useNativeToolTip(object);
+    }
+
+    @Override
+    public int getToolTipTimeDisplayed(Object object) {
+        if (mTooltipProvider != null) {
+            return mTooltipProvider.getToolTipTimeDisplayed(object);
+        }
+        return super.getToolTipTimeDisplayed(object);
+    }
+
+    @Override
+    public int getToolTipDisplayDelayTime(Object object) {
+        if (mTooltipProvider != null) {
+            return mTooltipProvider.getToolTipDisplayDelayTime(object);
+        }
+        return super.getToolTipDisplayDelayTime(object);
+    }
+
+    @Override
+    public int getToolTipStyle(Object object) {
+        if (mTooltipProvider != null) {
+            return mTooltipProvider.getToolTipStyle(object);
+        }
+        return super.getToolTipStyle(object);
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/SdkUpdaterWindowImpl2.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/SdkUpdaterWindowImpl2.java
new file mode 100755
index 0000000..3b0801b
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/SdkUpdaterWindowImpl2.java
@@ -0,0 +1,590 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository.ui;
+
+
+import com.android.SdkConstants;
+import com.android.sdklib.internal.repository.ITaskFactory;
+import com.android.sdklib.internal.repository.sources.SdkSourceProperties;
+import com.android.sdklib.internal.repository.updater.SettingsController;
+import com.android.sdklib.internal.repository.updater.SettingsController.Settings;
+import com.android.sdklib.repository.ISdkChangeListener;
+import com.android.sdkuilib.internal.repository.AboutDialog;
+import com.android.sdkuilib.internal.repository.ISdkUpdaterWindow;
+import com.android.sdkuilib.internal.repository.MenuBarWrapper;
+import com.android.sdkuilib.internal.repository.SettingsDialog;
+import com.android.sdkuilib.internal.repository.SwtUpdaterData;
+import com.android.sdkuilib.internal.repository.icons.ImageFactory;
+import com.android.sdkuilib.internal.repository.ui.PackagesPage.MenuAction;
+import com.android.sdkuilib.internal.tasks.ILogUiProvider;
+import com.android.sdkuilib.internal.tasks.ProgressView;
+import com.android.sdkuilib.internal.tasks.ProgressViewFactory;
+import com.android.sdkuilib.internal.widgets.ImgDisabledButton;
+import com.android.sdkuilib.internal.widgets.ToggleButton;
+import com.android.sdkuilib.repository.AvdManagerWindow.AvdInvocationContext;
+import com.android.sdkuilib.repository.SdkUpdaterWindow.SdkInvocationContext;
+import com.android.utils.ILogger;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
+import org.eclipse.swt.widgets.ProgressBar;
+import org.eclipse.swt.widgets.Shell;
+
+/**
+ * This is the private implementation of the UpdateWindow
+ * for the second version of the SDK Manager.
+ * <p/>
+ * This window features only one embedded page, the combined installed+available package list.
+ */
+public class SdkUpdaterWindowImpl2 implements ISdkUpdaterWindow {
+
+    public static final String APP_NAME = "Android SDK Manager";
+    private static final String SIZE_POS_PREFIX = "sdkman2"; //$NON-NLS-1$
+
+    private final Shell mParentShell;
+    private final SdkInvocationContext mContext;
+    /** Internal data shared between the window and its pages. */
+    private final SwtUpdaterData mSwtUpdaterData;
+
+    // --- UI members ---
+
+    protected Shell mShell;
+    private PackagesPage mPkgPage;
+    private ProgressBar mProgressBar;
+    private Label mStatusText;
+    private ImgDisabledButton mButtonStop;
+    private ToggleButton mButtonShowLog;
+    private SettingsController mSettingsController;
+    private LogWindow mLogWindow;
+
+    /**
+     * Creates a new window. Caller must call open(), which will block.
+     *
+     * @param parentShell Parent shell.
+     * @param sdkLog Logger. Cannot be null.
+     * @param osSdkRoot The OS path to the SDK root.
+     * @param context The {@link SdkInvocationContext} to change the behavior depending on who's
+     *  opening the SDK Manager.
+     */
+    public SdkUpdaterWindowImpl2(
+            Shell parentShell,
+            ILogger sdkLog,
+            String osSdkRoot,
+            SdkInvocationContext context) {
+        mParentShell = parentShell;
+        mContext = context;
+        mSwtUpdaterData = new SwtUpdaterData(osSdkRoot, sdkLog);
+    }
+
+    /**
+     * Creates a new window. Caller must call open(), which will block.
+     * <p/>
+     * This is to be used when the window is opened from {@link AvdManagerWindowImpl1}
+     * to share the same {@link SwtUpdaterData} structure.
+     *
+     * @param parentShell Parent shell.
+     * @param swtUpdaterData The parent's updater data.
+     * @param context The {@link SdkInvocationContext} to change the behavior depending on who's
+     *  opening the SDK Manager.
+     */
+    public SdkUpdaterWindowImpl2(
+            Shell parentShell,
+            SwtUpdaterData swtUpdaterData,
+            SdkInvocationContext context) {
+        mParentShell = parentShell;
+        mContext = context;
+        mSwtUpdaterData = swtUpdaterData;
+    }
+
+    /**
+     * Opens the window.
+     * @wbp.parser.entryPoint
+     */
+    @Override
+    public void open() {
+        if (mParentShell == null) {
+            Display.setAppName(APP_NAME); //$hide$ (hide from SWT designer)
+        }
+
+        createShell();
+        preCreateContent();
+        createContents();
+        createMenuBar();
+        createLogWindow();
+        mShell.open();
+        mShell.layout();
+
+        if (postCreateContent()) {    //$hide$ (hide from SWT designer)
+            Display display = Display.getDefault();
+            while (!mShell.isDisposed()) {
+                if (!display.readAndDispatch()) {
+                    display.sleep();
+                }
+            }
+        }
+
+        SdkSourceProperties p = new SdkSourceProperties();
+        p.save();
+
+        dispose();  //$hide$
+    }
+
+    private void createShell() {
+        // The SDK Manager must use a shell trim when standalone
+        // or a dialog trim when invoked from somewhere else.
+        int style = SWT.SHELL_TRIM;
+        if (mContext != SdkInvocationContext.STANDALONE) {
+            style |= SWT.APPLICATION_MODAL;
+        }
+
+        mShell = new Shell(mParentShell, style);
+        mShell.addDisposeListener(new DisposeListener() {
+            @Override
+            public void widgetDisposed(DisposeEvent e) {
+                ShellSizeAndPos.saveSizeAndPos(mShell, SIZE_POS_PREFIX);
+                onAndroidSdkUpdaterDispose();    //$hide$ (hide from SWT designer)
+            }
+        });
+
+        GridLayout glShell = new GridLayout(2, false);
+        glShell.verticalSpacing = 0;
+        glShell.horizontalSpacing = 0;
+        glShell.marginWidth = 0;
+        glShell.marginHeight = 0;
+        mShell.setLayout(glShell);
+
+        mShell.setMinimumSize(new Point(600, 300));
+        mShell.setSize(700, 500);
+        mShell.setText(APP_NAME);
+
+        ShellSizeAndPos.loadSizeAndPos(mShell, SIZE_POS_PREFIX);
+    }
+
+    private void createContents() {
+        mPkgPage = new PackagesPage(mShell, SWT.NONE, mSwtUpdaterData, mContext);
+        mPkgPage.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1));
+
+        Composite composite1 = new Composite(mShell, SWT.NONE);
+        composite1.setLayout(new GridLayout(1, false));
+        composite1.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+
+        mProgressBar = new ProgressBar(composite1, SWT.NONE);
+        mProgressBar.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+
+        mStatusText = new Label(composite1, SWT.NONE);
+        mStatusText.setText("Status Placeholder");  //$NON-NLS-1$ placeholder
+        mStatusText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+
+        Composite composite2 = new Composite(mShell, SWT.NONE);
+        composite2.setLayout(new GridLayout(2, false));
+
+        mButtonStop = new ImgDisabledButton(composite2, SWT.NONE,
+                getImage("stop_enabled_16.png"),    //$NON-NLS-1$
+                getImage("stop_disabled_16.png"),   //$NON-NLS-1$
+                "Click to abort the current task",
+                "");                                //$NON-NLS-1$ nothing to abort
+        mButtonStop.addListener(SWT.Selection, new Listener() {
+            @Override
+            public void handleEvent(Event event) {
+                onStopSelected();
+            }
+        });
+
+        mButtonShowLog = new ToggleButton(composite2, SWT.NONE,
+                getImage("log_off_16.png"),         //$NON-NLS-1$
+                getImage("log_on_16.png"),          //$NON-NLS-1$
+                "Click to show the log window",     // tooltip for state hidden=>shown
+                "Click to hide the log window");    // tooltip for state shown=>hidden
+        mButtonShowLog.addListener(SWT.Selection, new Listener() {
+            @Override
+            public void handleEvent(Event event) {
+                onToggleLogWindow();
+            }
+        });
+    }
+
+    @SuppressWarnings("unused") // MenuItem works using side effects
+    private void createMenuBar() {
+
+        Menu menuBar = new Menu(mShell, SWT.BAR);
+        mShell.setMenuBar(menuBar);
+
+        MenuItem menuBarPackages = new MenuItem(menuBar, SWT.CASCADE);
+        menuBarPackages.setText("Packages");
+
+        Menu menuPkgs = new Menu(menuBarPackages);
+        menuBarPackages.setMenu(menuPkgs);
+
+        MenuItem showUpdatesNew = new MenuItem(menuPkgs,
+                MenuAction.TOGGLE_SHOW_UPDATE_NEW_PKG.getMenuStyle());
+        showUpdatesNew.setText(
+                MenuAction.TOGGLE_SHOW_UPDATE_NEW_PKG.getMenuTitle());
+        mPkgPage.registerMenuAction(
+                MenuAction.TOGGLE_SHOW_UPDATE_NEW_PKG, showUpdatesNew);
+
+        MenuItem showInstalled = new MenuItem(menuPkgs,
+                MenuAction.TOGGLE_SHOW_INSTALLED_PKG.getMenuStyle());
+        showInstalled.setText(
+                MenuAction.TOGGLE_SHOW_INSTALLED_PKG.getMenuTitle());
+        mPkgPage.registerMenuAction(
+                MenuAction.TOGGLE_SHOW_INSTALLED_PKG, showInstalled);
+
+        MenuItem showObsoletePackages = new MenuItem(menuPkgs,
+                MenuAction.TOGGLE_SHOW_OBSOLETE_PKG.getMenuStyle());
+        showObsoletePackages.setText(
+                MenuAction.TOGGLE_SHOW_OBSOLETE_PKG.getMenuTitle());
+        mPkgPage.registerMenuAction(
+                MenuAction.TOGGLE_SHOW_OBSOLETE_PKG, showObsoletePackages);
+
+        MenuItem showArchives = new MenuItem(menuPkgs,
+                MenuAction.TOGGLE_SHOW_ARCHIVES.getMenuStyle());
+        showArchives.setText(
+                MenuAction.TOGGLE_SHOW_ARCHIVES.getMenuTitle());
+        mPkgPage.registerMenuAction(
+                MenuAction.TOGGLE_SHOW_ARCHIVES, showArchives);
+
+        new MenuItem(menuPkgs, SWT.SEPARATOR);
+
+        MenuItem sortByApi = new MenuItem(menuPkgs,
+                MenuAction.SORT_API_LEVEL.getMenuStyle());
+        sortByApi.setText(
+                MenuAction.SORT_API_LEVEL.getMenuTitle());
+        mPkgPage.registerMenuAction(
+                MenuAction.SORT_API_LEVEL, sortByApi);
+
+        MenuItem sortBySource = new MenuItem(menuPkgs,
+                MenuAction.SORT_SOURCE.getMenuStyle());
+        sortBySource.setText(
+                MenuAction.SORT_SOURCE.getMenuTitle());
+        mPkgPage.registerMenuAction(
+                MenuAction.SORT_SOURCE, sortBySource);
+
+        new MenuItem(menuPkgs, SWT.SEPARATOR);
+
+        MenuItem reload = new MenuItem(menuPkgs,
+                MenuAction.RELOAD.getMenuStyle());
+        reload.setText(
+                MenuAction.RELOAD.getMenuTitle());
+        mPkgPage.registerMenuAction(
+                MenuAction.RELOAD, reload);
+
+        MenuItem menuBarTools = new MenuItem(menuBar, SWT.CASCADE);
+        menuBarTools.setText("Tools");
+
+        Menu menuTools = new Menu(menuBarTools);
+        menuBarTools.setMenu(menuTools);
+
+        if (mContext == SdkInvocationContext.STANDALONE) {
+            MenuItem manageAvds = new MenuItem(menuTools, SWT.NONE);
+            manageAvds.setText("Manage AVDs...");
+            manageAvds.addSelectionListener(new SelectionAdapter() {
+                @Override
+                public void widgetSelected(SelectionEvent event) {
+                    onAvdManager();
+                }
+            });
+        }
+
+        MenuItem manageSources = new MenuItem(menuTools,
+                MenuAction.SHOW_ADDON_SITES.getMenuStyle());
+        manageSources.setText(
+                MenuAction.SHOW_ADDON_SITES.getMenuTitle());
+        mPkgPage.registerMenuAction(
+                MenuAction.SHOW_ADDON_SITES, manageSources);
+
+        if (mContext == SdkInvocationContext.STANDALONE || mContext == SdkInvocationContext.IDE) {
+            try {
+                new MenuBarWrapper(APP_NAME, menuTools) {
+                    @Override
+                    public void onPreferencesMenuSelected() {
+
+                        // capture a copy of the initial settings
+                        Settings settings1 = new Settings(mSettingsController.getSettings());
+
+                        // open the dialog and wait for it to close
+                        SettingsDialog sd = new SettingsDialog(mShell, mSwtUpdaterData);
+                        sd.open();
+
+                        // get the new settings
+                        Settings settings2 = mSettingsController.getSettings();
+
+                        // We need to reload the package list if the http mode or the preview
+                        // modes have changed.
+                        if (settings1.getForceHttp() != settings2.getForceHttp() ||
+                                settings1.getEnablePreviews() != settings2.getEnablePreviews()) {
+                            mPkgPage.onSdkReload();
+                        }
+                    }
+
+                    @Override
+                    public void onAboutMenuSelected() {
+                        AboutDialog ad = new AboutDialog(mShell, mSwtUpdaterData);
+                        ad.open();
+                    }
+
+                    @Override
+                    public void printError(String format, Object... args) {
+                        if (mSwtUpdaterData != null) {
+                            mSwtUpdaterData.getSdkLog().error(null, format, args);
+                        }
+                    }
+                };
+            } catch (Throwable e) {
+                mSwtUpdaterData.getSdkLog().error(e, "Failed to setup menu bar");
+                e.printStackTrace();
+            }
+        }
+    }
+
+    private Image getImage(String filename) {
+        if (mSwtUpdaterData != null) {
+            ImageFactory imgFactory = mSwtUpdaterData.getImageFactory();
+            if (imgFactory != null) {
+                return imgFactory.getImageByName(filename);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Creates the log window.
+     * <p/>
+     * If this is invoked from an IDE, we also define a secondary logger so that all
+     * messages flow to the IDE log. This may or may not be what we want in the end
+     * (e.g. a middle ground would be to repeat error, and ignore normal/verbose)
+     */
+    private void createLogWindow() {
+        mLogWindow = new LogWindow(mShell,
+                mContext == SdkInvocationContext.IDE ? mSwtUpdaterData.getSdkLog() : null);
+        mLogWindow.open();
+    }
+
+
+    // -- Start of internal part ----------
+    // Hide everything down-below from SWT designer
+    //$hide>>$
+
+    // --- Public API -----------
+
+    /**
+     * Adds a new listener to be notified when a change is made to the content of the SDK.
+     */
+    @Override
+    public void addListener(ISdkChangeListener listener) {
+        mSwtUpdaterData.addListeners(listener);
+    }
+
+    /**
+     * Removes a new listener to be notified anymore when a change is made to the content of
+     * the SDK.
+     */
+    @Override
+    public void removeListener(ISdkChangeListener listener) {
+        mSwtUpdaterData.removeListener(listener);
+    }
+
+    // --- Internals & UI Callbacks -----------
+
+    /**
+     * Called before the UI is created.
+     */
+    private void preCreateContent() {
+        mSwtUpdaterData.setWindowShell(mShell);
+        // We need the UI factory to create the UI
+        mSwtUpdaterData.setImageFactory(new ImageFactory(mShell.getDisplay()));
+        // Note: we can't create the TaskFactory yet because we need the UI
+        // to be created first, so this is done in postCreateContent().
+    }
+
+    /**
+     * Once the UI has been created, initializes the content.
+     * This creates the pages, selects the first one, setups sources and scans for local folders.
+     *
+     * Returns true if we should show the window.
+     */
+    private boolean postCreateContent() {
+        ProgressViewFactory factory = new ProgressViewFactory();
+
+        // This class delegates all logging to the mLogWindow window
+        // and filters errors to make sure the window is visible when
+        // an error is logged.
+        ILogUiProvider logAdapter = new ILogUiProvider() {
+            @Override
+            public void setDescription(String description) {
+                mLogWindow.setDescription(description);
+            }
+
+            @Override
+            public void log(String log) {
+                mLogWindow.log(log);
+            }
+
+            @Override
+            public void logVerbose(String log) {
+                mLogWindow.logVerbose(log);
+            }
+
+            @Override
+            public void logError(String log) {
+                mLogWindow.logError(log);
+
+                // Run the window visibility check/toggle on the UI thread.
+                // Note: at least on Windows, it seems ok to check for the window visibility
+                // on a sub-thread but that doesn't seem cross-platform safe. We shouldn't
+                // have a lot of error logging, so this should be acceptable. If not, we could
+                // cache the visibility state.
+                if (mShell != null && !mShell.isDisposed()) {
+                    mShell.getDisplay().syncExec(new Runnable() {
+                        @Override
+                        public void run() {
+                            if (!mLogWindow.isVisible()) {
+                                // Don't toggle the window visibility directly.
+                                // Instead use the same action as the log-toggle button
+                                // so that the button's state be kept in sync.
+                                onToggleLogWindow();
+                            }
+                        }
+                    });
+                }
+            }
+        };
+
+        factory.setProgressView(
+                new ProgressView(mStatusText, mProgressBar, mButtonStop, logAdapter));
+        mSwtUpdaterData.setTaskFactory(factory);
+
+        setWindowImage(mShell);
+
+        setupSources();
+        initializeSettings();
+
+        if (mSwtUpdaterData.checkIfInitFailed()) {
+            return false;
+        }
+
+        mSwtUpdaterData.broadcastOnSdkLoaded();
+
+        // Tell the one page its the selected one
+        mPkgPage.performFirstLoad();
+
+        return true;
+    }
+
+    /**
+     * Creates the icon of the window shell.
+     *
+     * @param shell The shell on which to put the icon
+     */
+    private void setWindowImage(Shell shell) {
+        String imageName = "android_icon_16.png"; //$NON-NLS-1$
+        if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_DARWIN) {
+            imageName = "android_icon_128.png"; //$NON-NLS-1$
+        }
+
+        if (mSwtUpdaterData != null) {
+            ImageFactory imgFactory = mSwtUpdaterData.getImageFactory();
+            if (imgFactory != null) {
+                shell.setImage(imgFactory.getImageByName(imageName));
+            }
+        }
+    }
+
+    /**
+     * Called by the main loop when the window has been disposed.
+     */
+    private void dispose() {
+        mLogWindow.close();
+        mSwtUpdaterData.getSources().saveUserAddons(mSwtUpdaterData.getSdkLog());
+    }
+
+    /**
+     * Callback called when the window shell is disposed.
+     */
+    private void onAndroidSdkUpdaterDispose() {
+        if (mSwtUpdaterData != null) {
+            ImageFactory imgFactory = mSwtUpdaterData.getImageFactory();
+            if (imgFactory != null) {
+                imgFactory.dispose();
+            }
+        }
+    }
+
+    /**
+     * Used to initialize the sources.
+     */
+    private void setupSources() {
+        mSwtUpdaterData.setupDefaultSources();
+    }
+
+    /**
+     * Initializes settings.
+     * This must be called after addExtraPages(), which created a settings page.
+     * Iterate through all the pages to find the first (and supposedly unique) setting page,
+     * and use it to load and apply these settings.
+     */
+    private void initializeSettings() {
+        mSettingsController = mSwtUpdaterData.getSettingsController();
+        mSettingsController.loadSettings();
+        mSettingsController.applySettings();
+    }
+
+    private void onToggleLogWindow() {
+        // toggle visibility
+        if (!mButtonShowLog.isDisposed()) {
+            mLogWindow.setVisible(!mLogWindow.isVisible());
+            mButtonShowLog.setState(mLogWindow.isVisible() ? 1 : 0);
+        }
+    }
+
+    private void onStopSelected() {
+        // TODO
+    }
+
+    private void onAvdManager() {
+        ITaskFactory oldFactory = mSwtUpdaterData.getTaskFactory();
+
+        try {
+            AvdManagerWindowImpl1 win = new AvdManagerWindowImpl1(
+                    mShell,
+                    mSwtUpdaterData,
+                    AvdInvocationContext.DIALOG);
+
+            win.open();
+        } catch (Exception e) {
+            mSwtUpdaterData.getSdkLog().error(e, "AVD Manager window error");
+        } finally {
+            mSwtUpdaterData.setTaskFactory(oldFactory);
+        }
+    }
+
+    // End of hiding from SWT Designer
+    //$hide<<$
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/ShellSizeAndPos.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/ShellSizeAndPos.java
new file mode 100755
index 0000000..4921ba0
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/ShellSizeAndPos.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository.ui;
+
+
+import com.android.prefs.AndroidLocation;
+
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Monitor;
+import org.eclipse.swt.widgets.Shell;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Properties;
+
+/**
+ * Utility to save & restore the size and position on a window
+ * using a common config file.
+ */
+public class ShellSizeAndPos {
+
+    private static final String SETTINGS_FILENAME = "androidwin.cfg";   //$NON-NLS-1$
+    private static final String PX = "_px";                             //$NON-NLS-1$
+    private static final String PY = "_py";                             //$NON-NLS-1$
+    private static final String SX = "_sx";                             //$NON-NLS-1$
+    private static final String SY = "_sy";                             //$NON-NLS-1$
+
+    public static void loadSizeAndPos(Shell shell, String prefix) {
+        Properties props = loadProperties();
+
+        try {
+            int px = Integer.parseInt(props.getProperty(prefix + PX));
+            int py = Integer.parseInt(props.getProperty(prefix + PY));
+            int sx = Integer.parseInt(props.getProperty(prefix + SX));
+            int sy = Integer.parseInt(props.getProperty(prefix + SY));
+
+            Point p1 = new Point(px, py);
+            Point p2 = new Point(px + sx, py + sy);
+            Rectangle r = new Rectangle(px, py, sy, sy);
+
+            Monitor bestMatch = null;
+            int bestSurface = -1;
+            for (Monitor monitor : shell.getDisplay().getMonitors()) {
+                Rectangle area = monitor.getClientArea();
+                if (area.contains(p1) && area.contains(p2)) {
+                    // The shell is fully visible on this monitor. Just use that.
+                    bestMatch = monitor;
+                    bestSurface = Integer.MAX_VALUE;
+                    break;
+                } else {
+                    // Find which monitor displays the largest surface of the window.
+                    // We'll use this one to center the window there, to make sure we're not
+                    // starting split between several monitors.
+                    Rectangle i = area.intersection(r);
+                    int surface = i.width * i.height;
+                    if (surface > bestSurface) {
+                        bestSurface = surface;
+                        bestMatch = monitor;
+                    }
+                }
+            }
+
+            if (bestMatch != null && bestSurface != Integer.MAX_VALUE) {
+                // Recenter the window on this monitor and make sure it fits
+                Rectangle area = bestMatch.getClientArea();
+
+                sx = Math.min(sx, area.width);
+                sy = Math.min(sy, area.height);
+                px = area.x + (area.width - sx) / 2;
+                py = area.y + (area.height - sy) / 2;
+            }
+
+            shell.setLocation(px, py);
+            shell.setSize(sx, sy);
+
+        } catch ( Exception e) {
+            // Ignore exception. We could typically get NPE from the getProperty
+            // or NumberFormatException from parseInt calls. Either way, do
+            // nothing if anything goes wrong.
+        }
+    }
+
+    public static void saveSizeAndPos(Shell shell, String prefix) {
+        Properties props = loadProperties();
+
+        Point loc = shell.getLocation();
+        Point size = shell.getSize();
+
+        props.setProperty(prefix + PX, Integer.toString(loc.x));
+        props.setProperty(prefix + PY, Integer.toString(loc.y));
+        props.setProperty(prefix + SX, Integer.toString(size.x));
+        props.setProperty(prefix + SY, Integer.toString(size.y));
+
+        saveProperties(props);
+    }
+
+    /**
+     * Load properties saved in {@link #SETTINGS_FILENAME}.
+     * If the file does not exists or doesn't load properly, just return an
+     * empty set of properties.
+     */
+    private static Properties loadProperties() {
+        Properties props = new Properties();
+        FileInputStream fis = null;
+
+        try {
+            String folder = AndroidLocation.getFolder();
+            File f = new File(folder, SETTINGS_FILENAME);
+            if (f.exists()) {
+                fis = new FileInputStream(f);
+
+                props.load(fis);
+            }
+        } catch (Exception e) {
+            // Ignore
+        } finally {
+            if (fis != null) {
+                try {
+                    fis.close();
+                } catch (IOException e) {
+                }
+            }
+        }
+
+        return props;
+    }
+
+    private static void saveProperties(Properties props) {
+        FileOutputStream fos = null;
+
+        try {
+            String folder = AndroidLocation.getFolder();
+            File f = new File(folder, SETTINGS_FILENAME);
+            fos = new FileOutputStream(f);
+
+            props.store(fos, "## Size and Pos for SDK Manager Windows");  //$NON-NLS-1$
+
+        } catch (Exception e) {
+            // ignore
+        } finally {
+            if (fos != null) {
+                try {
+                    fos.close();
+                } catch (IOException e) {
+                }
+            }
+        }
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ILogUiProvider.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ILogUiProvider.java
new file mode 100755
index 0000000..8f77b7a
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ILogUiProvider.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.tasks;
+
+
+/**
+ * Interface for a user interface that displays the log from a task monitor.
+ */
+public interface ILogUiProvider {
+
+    /**
+     * Sets the description in the current task dialog.
+     * This method can be invoked from a non-UI thread.
+     */
+    public abstract void setDescription(String description);
+
+    /**
+     * Logs a "normal" information line.
+     * This method can be invoked from a non-UI thread.
+     */
+    public abstract void log(String log);
+
+    /**
+     * Logs an "error" information line.
+     * This method can be invoked from a non-UI thread.
+     */
+    public abstract void logError(String log);
+
+    /**
+     * Logs a "verbose" information line, that is extra details which are typically
+     * not that useful for the end-user and might be hidden until explicitly shown.
+     * This method can be invoked from a non-UI thread.
+     */
+    public abstract void logVerbose(String log);
+
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/IProgressUiProvider.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/IProgressUiProvider.java
new file mode 100755
index 0000000..4e2c131
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/IProgressUiProvider.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.tasks;
+
+import com.android.sdklib.internal.repository.ITaskMonitor;
+import com.android.sdklib.internal.repository.UserCredentials;
+
+import org.eclipse.swt.widgets.ProgressBar;
+
+/**
+ * Interface for a user interface that displays both a task status
+ * (e.g. via an {@link ITaskMonitor}) and the progress state of the
+ * task (e.g. via a progress bar.)
+ * <p/>
+ * See {@link ITaskMonitor} for details on how a monitor expects to
+ * be displayed.
+ */
+interface IProgressUiProvider extends ILogUiProvider {
+
+    public abstract boolean isCancelRequested();
+
+    /**
+     * Sets the description in the current task dialog.
+     * This method can be invoked from a non-UI thread.
+     */
+    @Override
+    public abstract void setDescription(String description);
+
+    /**
+     * Sets the max value of the progress bar.
+     * This method can be invoked from a non-UI thread.
+     *
+     * @see ProgressBar#setMaximum(int)
+     */
+    public abstract void setProgressMax(int max);
+
+    /**
+     * Sets the current value of the progress bar.
+     * This method can be invoked from a non-UI thread.
+     */
+    public abstract void setProgress(int value);
+
+    /**
+     * Returns the current value of the progress bar,
+     * between 0 and up to {@link #setProgressMax(int)} - 1.
+     * This method can be invoked from a non-UI thread.
+     */
+    public abstract int getProgress();
+
+    /**
+     * Display a yes/no question dialog box.
+     *
+     * This implementation allow this to be called from any thread, it
+     * makes sure the dialog is opened synchronously in the ui thread.
+     *
+     * @param title The title of the dialog box
+     * @param message The error message
+     * @return true if YES was clicked.
+     */
+    public abstract boolean displayPrompt(String title, String message);
+
+    /**
+     * Launch an interface which asks for login credentials. Implementations
+     * MUST allow this to be called from any thread, e.g. by making sure the
+     * dialog is opened synchronously in the UI thread.
+     *
+     * @param title The title of the dialog box.
+     * @param message The message to be displayed as an instruction.
+     * @return Returns user provided credentials
+     */
+    public UserCredentials displayLoginCredentialsPrompt(String title, String message);
+
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTask.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTask.java
new file mode 100755
index 0000000..d5404ae
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTask.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.tasks;
+
+import com.android.sdklib.internal.repository.ITask;
+import com.android.sdklib.internal.repository.ITaskMonitor;
+
+import org.eclipse.swt.widgets.Shell;
+
+
+/**
+ * An {@link ITaskMonitor} that displays a {@link ProgressTaskDialog}.
+ */
+public final class ProgressTask extends TaskMonitorImpl {
+
+    private final String mTitle;
+    private final ProgressTaskDialog mDialog;
+    private volatile boolean mAutoClose = true;
+
+
+    /**
+     * Creates a new {@link ProgressTask} with the given title.
+     * This does NOT start the task. The caller must invoke {@link #start(ITask)}.
+     */
+    public ProgressTask(Shell parent, String title) {
+        super(new ProgressTaskDialog(parent));
+        mTitle = title;
+        mDialog = (ProgressTaskDialog) getUiProvider();
+        mDialog.setText(mTitle);
+    }
+
+    /**
+     * Execute the given task in a separate thread (not the UI thread).
+     * This blocks till the thread ends.
+     * <p/>
+     * The {@link ProgressTask} must not be reused after this call.
+     */
+    public void start(ITask task) {
+        assert mDialog != null;
+        mDialog.open(createTaskThread(mTitle, task));
+    }
+
+    /**
+     * Changes the auto-close behavior of the dialog on task completion.
+     *
+     * @param autoClose True if the dialog should be closed automatically when the task
+     *   has completed.
+     */
+    public void setAutoClose(boolean autoClose) {
+        if (autoClose != mAutoClose) {
+            if (autoClose) {
+                mDialog.setAutoCloseRequested();
+            } else {
+                mDialog.setManualCloseRequested();
+            }
+            mAutoClose = autoClose;
+        }
+    }
+
+    /**
+     * Creates a thread to run the task. The thread has not been started yet.
+     * When the task completes, requests to close the dialog.
+     *
+     * @return A new thread that will run the task. The thread has not been started yet.
+     */
+    private Thread createTaskThread(String title, final ITask task) {
+        if (task != null) {
+            return new Thread(title) {
+                @Override
+                public void run() {
+                    task.run(ProgressTask.this);
+                    if (mAutoClose) {
+                        mDialog.setAutoCloseRequested();
+                    } else {
+                        mDialog.setManualCloseRequested();
+                    }
+                }
+            };
+        }
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p/>
+     * Sets the dialog to not auto-close since we want the user to see the error
+     * (this is equivalent to calling {@code setAutoClose(false)}).
+     */
+    @Override
+    public void logError(String format, Object...args) {
+        setAutoClose(false);
+        super.logError(format, args);
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskDialog.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskDialog.java
new file mode 100755
index 0000000..50f1e57
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskDialog.java
@@ -0,0 +1,520 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.tasks;
+
+import com.android.SdkConstants;
+import com.android.sdklib.internal.repository.ITaskMonitor;
+import com.android.sdklib.internal.repository.UserCredentials;
+import com.android.sdkuilib.ui.AuthenticationDialog;
+import com.android.sdkuilib.ui.GridDialog;
+import com.android.utils.Pair;
+
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.ShellAdapter;
+import org.eclipse.swt.events.ShellEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.ProgressBar;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+
+/**
+ * Implements a {@link ProgressTaskDialog}, used by the {@link ProgressTask} class.
+ * This separates the dialog UI from the task logic.
+ *
+ * Note: this does not implement the {@link ITaskMonitor} interface to avoid confusing
+ * SWT Designer.
+ */
+final class ProgressTaskDialog extends Dialog implements IProgressUiProvider {
+
+    /**
+     * Min Y location for dialog. Need to deal with the menu bar on mac os.
+     */
+    private final static int MIN_Y = SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN ?
+            20 : 0;
+
+    private static enum CancelMode {
+        /** Cancel button says "Cancel" and is enabled. Waiting for user to cancel. */
+        ACTIVE,
+        /** Cancel button has been clicked. Waiting for thread to finish. */
+        CANCEL_PENDING,
+        /** Close pending. Close button clicked or thread finished but there were some
+         * messages so the user needs to manually close. */
+        CLOSE_MANUAL,
+        /** Close button clicked or thread finished. The window will automatically close. */
+        CLOSE_AUTO
+    }
+
+    /** The current mode of operation of the dialog. */
+    private CancelMode mCancelMode = CancelMode.ACTIVE;
+
+    /** Last dialog size for this session. */
+    private static Point sLastSize;
+
+
+    // UI fields
+    private Shell mDialogShell;
+    private Composite mRootComposite;
+    private Label mLabel;
+    private ProgressBar mProgressBar;
+    private Button mCancelButton;
+    private Text mResultText;
+
+
+    /**
+     * Create the dialog.
+     * @param parent Parent container
+     */
+    public ProgressTaskDialog(Shell parent) {
+        super(parent, SWT.APPLICATION_MODAL);
+    }
+
+    /**
+     * Open the dialog and blocks till it gets closed
+     * @param taskThread The thread to run the task. Cannot be null.
+     */
+    public void open(Thread taskThread) {
+        createContents();
+        positionShell();                        //$hide$ (hide from SWT designer)
+        mDialogShell.open();
+        mDialogShell.layout();
+
+        startThread(taskThread);                //$hide$ (hide from SWT designer)
+
+        Display display = getParent().getDisplay();
+        while (!mDialogShell.isDisposed() && mCancelMode != CancelMode.CLOSE_AUTO) {
+            if (!display.readAndDispatch()) {
+                display.sleep();
+            }
+        }
+
+        setCancelRequested();       //$hide$ (hide from SWT designer)
+
+        if (!mDialogShell.isDisposed()) {
+            sLastSize = mDialogShell.getSize();
+            mDialogShell.close();
+        }
+    }
+
+    /**
+     * Create contents of the dialog.
+     */
+    private void createContents() {
+        mDialogShell = new Shell(getParent(), SWT.DIALOG_TRIM | SWT.RESIZE);
+        mDialogShell.addShellListener(new ShellAdapter() {
+            @Override
+            public void shellClosed(ShellEvent e) {
+                onShellClosed(e);
+            }
+        });
+        mDialogShell.setLayout(new GridLayout(1, false));
+        mDialogShell.setSize(450, 300);
+        mDialogShell.setText(getText());
+
+        mRootComposite = new Composite(mDialogShell, SWT.NONE);
+        mRootComposite.setLayout(new GridLayout(2, false));
+        mRootComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
+
+        mLabel = new Label(mRootComposite, SWT.NONE);
+        mLabel.setText("Task");
+        mLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1));
+
+        mProgressBar = new ProgressBar(mRootComposite, SWT.NONE);
+        mProgressBar.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+        mCancelButton = new Button(mRootComposite, SWT.NONE);
+        mCancelButton.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
+        mCancelButton.setText("Cancel");
+
+        mCancelButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                onCancelSelected();  //$hide$
+            }
+        });
+
+        mResultText = new Text(mRootComposite,
+                SWT.BORDER | SWT.READ_ONLY | SWT.WRAP |
+                SWT.H_SCROLL | SWT.V_SCROLL | SWT.CANCEL | SWT.MULTI);
+        mResultText.setEditable(true);
+        mResultText.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1));
+    }
+
+    // -- End of UI, Start of internal logic ----------
+    // Hide everything down-below from SWT designer
+    //$hide>>$
+
+    @Override
+    public boolean isCancelRequested() {
+        return mCancelMode != CancelMode.ACTIVE;
+    }
+
+    /**
+     * Sets the mode to cancel pending.
+     * The first time this grays the cancel button, to let the user know that the
+     * cancel operation is pending.
+     */
+    public void setCancelRequested() {
+        if (!mDialogShell.isDisposed()) {
+            // The dialog is not disposed, make sure to run all this in the UI thread
+            // and lock on the cancel button mode.
+            mDialogShell.getDisplay().syncExec(new Runnable() {
+
+                @Override
+                public void run() {
+                    synchronized (mCancelMode) {
+                        if (mCancelMode == CancelMode.ACTIVE) {
+                            mCancelMode = CancelMode.CANCEL_PENDING;
+
+                            if (!mCancelButton.isDisposed()) {
+                                mCancelButton.setEnabled(false);
+                            }
+                        }
+                    }
+                }
+            });
+        } else {
+            // The dialog is disposed. Just set the boolean. We shouldn't be here.
+            if (mCancelMode == CancelMode.ACTIVE) {
+                mCancelMode = CancelMode.CANCEL_PENDING;
+            }
+        }
+    }
+
+    /**
+     * Sets the mode to close manual.
+     * The first time, this also ungrays the pause button and converts it to a close button.
+     */
+    public void setManualCloseRequested() {
+        if (!mDialogShell.isDisposed()) {
+            // The dialog is not disposed, make sure to run all this in the UI thread
+            // and lock on the cancel button mode.
+            mDialogShell.getDisplay().syncExec(new Runnable() {
+
+                @Override
+                public void run() {
+                    synchronized (mCancelMode) {
+                        if (mCancelMode != CancelMode.CLOSE_MANUAL &&
+                                mCancelMode != CancelMode.CLOSE_AUTO) {
+                            mCancelMode = CancelMode.CLOSE_MANUAL;
+
+                            if (!mCancelButton.isDisposed()) {
+                                mCancelButton.setEnabled(true);
+                                mCancelButton.setText("Close");
+                            }
+                        }
+                    }
+                }
+            });
+        } else {
+            // The dialog is disposed. Just set the booleans. We shouldn't be here.
+            if (mCancelMode != CancelMode.CLOSE_MANUAL &&
+                    mCancelMode != CancelMode.CLOSE_AUTO) {
+                mCancelMode = CancelMode.CLOSE_MANUAL;
+            }
+        }
+    }
+
+    /**
+     * Sets the mode to close auto.
+     * The main loop will just exit and close the shell at the first opportunity.
+     */
+    public void setAutoCloseRequested() {
+        synchronized (mCancelMode) {
+            if (mCancelMode != CancelMode.CLOSE_AUTO) {
+                mCancelMode = CancelMode.CLOSE_AUTO;
+            }
+        }
+    }
+
+    /**
+     * Callback invoked when the cancel button is selected.
+     * When in closing mode, this simply closes the shell. Otherwise triggers a cancel.
+     */
+    private void onCancelSelected() {
+        if (mCancelMode == CancelMode.CLOSE_MANUAL) {
+            setAutoCloseRequested();
+        } else {
+            setCancelRequested();
+        }
+    }
+
+    /**
+     * Callback invoked when the shell is closed either by clicking the close button
+     * on by calling shell.close().
+     * This does the same thing as clicking the cancel/close button unless the mode is
+     * to auto close in which case we should do nothing to let the shell close normally.
+     */
+    private void onShellClosed(ShellEvent e) {
+        if (mCancelMode != CancelMode.CLOSE_AUTO) {
+            e.doit = false; // don't close directly
+            onCancelSelected();
+        }
+    }
+
+    /**
+     * Sets the description in the current task dialog.
+     * This method can be invoked from a non-UI thread.
+     */
+    @Override
+    public void setDescription(final String description) {
+        mDialogShell.getDisplay().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                if (!mLabel.isDisposed()) {
+                    mLabel.setText(description);
+                }
+            }
+        });
+    }
+
+    /**
+     * Adds to the log in the current task dialog.
+     * This method can be invoked from a non-UI thread.
+     */
+    @Override
+    public void log(final String info) {
+        if (!mDialogShell.isDisposed()) {
+            mDialogShell.getDisplay().syncExec(new Runnable() {
+                @Override
+                public void run() {
+                    if (!mResultText.isDisposed()) {
+                        mResultText.setVisible(true);
+                        String lastText = mResultText.getText();
+                        if (lastText != null &&
+                                lastText.length() > 0 &&
+                                !lastText.endsWith("\n") &&     //$NON-NLS-1$
+                                !info.startsWith("\n")) {       //$NON-NLS-1$
+                            mResultText.append("\n");           //$NON-NLS-1$
+                        }
+                        mResultText.append(info);
+                    }
+                }
+            });
+        }
+    }
+
+    @Override
+    public void logError(String info) {
+        log(info);
+    }
+
+    @Override
+    public void logVerbose(String info) {
+        log(info);
+    }
+
+    /**
+     * Sets the max value of the progress bar.
+     * This method can be invoked from a non-UI thread.
+     *
+     * @see ProgressBar#setMaximum(int)
+     */
+    @Override
+    public void setProgressMax(final int max) {
+        if (!mDialogShell.isDisposed()) {
+            mDialogShell.getDisplay().syncExec(new Runnable() {
+                @Override
+                public void run() {
+                    if (!mProgressBar.isDisposed()) {
+                        mProgressBar.setMaximum(max);
+                    }
+                }
+            });
+        }
+    }
+
+    /**
+     * Sets the current value of the progress bar.
+     * This method can be invoked from a non-UI thread.
+     */
+    @Override
+    public void setProgress(final int value) {
+        if (!mDialogShell.isDisposed()) {
+            mDialogShell.getDisplay().syncExec(new Runnable() {
+                @Override
+                public void run() {
+                    if (!mProgressBar.isDisposed()) {
+                        mProgressBar.setSelection(value);
+                    }
+                }
+            });
+        }
+    }
+
+    /**
+     * Returns the current value of the progress bar,
+     * between 0 and up to {@link #setProgressMax(int)} - 1.
+     * This method can be invoked from a non-UI thread.
+     */
+    @Override
+    public int getProgress() {
+        final int[] result = new int[] { 0 };
+
+        if (!mDialogShell.isDisposed()) {
+            mDialogShell.getDisplay().syncExec(new Runnable() {
+                @Override
+                public void run() {
+                    if (!mProgressBar.isDisposed()) {
+                        result[0] = mProgressBar.getSelection();
+                    }
+                }
+            });
+        }
+
+        return result[0];
+    }
+
+    /**
+     * Display a yes/no question dialog box.
+     *
+     * This implementation allow this to be called from any thread, it
+     * makes sure the dialog is opened synchronously in the ui thread.
+     *
+     * @param title The title of the dialog box
+     * @param message The error message
+     * @return true if YES was clicked.
+     */
+    @Override
+    public boolean displayPrompt(final String title, final String message) {
+        Display display = mDialogShell.getDisplay();
+
+        // we need to ask the user what he wants to do.
+        final boolean[] result = new boolean[] { false };
+        display.syncExec(new Runnable() {
+            @Override
+            public void run() {
+                result[0] = MessageDialog.openQuestion(mDialogShell, title, message);
+            }
+        });
+        return result[0];
+    }
+
+    /**
+     * This method opens a pop-up window which requests for User Login and
+     * password.
+     *
+     * @param title The title of the window.
+     * @param message The message to displayed in the login/password window.
+     * @return Returns a {@link Pair} holding the entered login and password.
+     *         The information must always be in the following order:
+     *         Login,Password. So in order to retrieve the <b>login</b> callers
+     *         should retrieve the first element, and the second value for the
+     *         <b>password</b>.
+     *         If operation is <b>canceled</b> by user the return value must be <b>null</b>.
+     * @see ITaskMonitor#displayLoginCredentialsPrompt(String, String)
+     */
+    @Override
+    public UserCredentials displayLoginCredentialsPrompt(
+            final String title, final String message) {
+        Display display = mDialogShell.getDisplay();
+
+        // open dialog and request login and password
+        GetUserCredentialsTask task = new GetUserCredentialsTask(mDialogShell, title, message);
+        display.syncExec(task);
+
+        return task.getUserCredentials();
+    }
+
+    private static class GetUserCredentialsTask implements Runnable {
+        private UserCredentials mResult = null;
+
+        private Shell mShell;
+        private String mTitle;
+        private String mMessage;
+
+        public GetUserCredentialsTask(Shell shell, String title, String message) {
+            mShell = shell;
+            mTitle = title;
+            mMessage = message;
+        }
+
+        @Override
+        public void run() {
+            AuthenticationDialog authenticationDialog = new AuthenticationDialog(mShell,
+                        mTitle, mMessage);
+            int dlgResult= authenticationDialog.open();
+            if(dlgResult == GridDialog.OK) {
+                mResult = new UserCredentials(
+                        authenticationDialog.getLogin(),
+                        authenticationDialog.getPassword(),
+                        authenticationDialog.getWorkstation(),
+                        authenticationDialog.getDomain());
+            }
+        }
+
+        public UserCredentials getUserCredentials() {
+            return mResult;
+        }
+    }
+
+    /**
+     * Starts the thread that runs the task.
+     * This is deferred till the UI is created.
+     */
+    private void startThread(Thread taskThread) {
+        if (taskThread != null) {
+            taskThread.start();
+        }
+    }
+
+    /**
+     * Centers the dialog in its parent shell.
+     */
+    private void positionShell() {
+        // Centers the dialog in its parent shell
+        Shell child = mDialogShell;
+        Shell parent = getParent();
+        if (child != null && parent != null) {
+
+            // get the parent client area with a location relative to the display
+            Rectangle parentArea = parent.getClientArea();
+            Point parentLoc = parent.getLocation();
+            int px = parentLoc.x;
+            int py = parentLoc.y;
+            int pw = parentArea.width;
+            int ph = parentArea.height;
+
+            // Reuse the last size if there's one, otherwise use the default
+            Point childSize = sLastSize != null ? sLastSize : child.getSize();
+            int cw = childSize.x;
+            int ch = childSize.y;
+
+            int x = px + (pw - cw) / 2;
+            if (x < 0) x = 0;
+
+            int y = py + (ph - ch) / 2;
+            if (y < MIN_Y) y = MIN_Y;
+
+            child.setLocation(x, y);
+            child.setSize(cw, ch);
+        }
+    }
+
+    // End of hiding from SWT Designer
+    //$hide<<$
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskFactory.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskFactory.java
new file mode 100755
index 0000000..17cba7a
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskFactory.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.tasks;
+
+import com.android.sdklib.internal.repository.ITask;
+import com.android.sdklib.internal.repository.ITaskFactory;
+import com.android.sdklib.internal.repository.ITaskMonitor;
+
+import org.eclipse.swt.widgets.Shell;
+
+/**
+ * An {@link ITaskFactory} that creates a new {@link ProgressTask} dialog
+ * for each new task.
+ */
+public final class ProgressTaskFactory implements ITaskFactory {
+
+    private final Shell mShell;
+
+    public ProgressTaskFactory(Shell shell) {
+        mShell = shell;
+    }
+
+    @Override
+    public void start(String title, ITask task) {
+        start(title, null /*parentMonitor*/, task);
+    }
+
+    @Override
+    public void start(String title, ITaskMonitor parentMonitor, ITask task) {
+
+        if (parentMonitor == null) {
+            ProgressTask p = new ProgressTask(mShell, title);
+            p.start(task);
+        } else {
+            // Use all the reminder of the parent monitor.
+            if (parentMonitor.getProgressMax() == 0) {
+                parentMonitor.setProgressMax(1);
+            }
+
+            ITaskMonitor sub = parentMonitor.createSubMonitor(
+                    parentMonitor.getProgressMax() - parentMonitor.getProgress());
+            try {
+                task.run(sub);
+            } finally {
+                int delta =
+                    sub.getProgressMax() - sub.getProgress();
+                if (delta > 0) {
+                    sub.incProgress(delta);
+                }
+            }
+        }
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressView.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressView.java
new file mode 100755
index 0000000..8987351
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressView.java
@@ -0,0 +1,376 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.tasks;
+
+import com.android.sdklib.internal.repository.ITask;
+import com.android.sdklib.internal.repository.ITaskMonitor;
+import com.android.sdklib.internal.repository.UserCredentials;
+import com.android.sdkuilib.ui.AuthenticationDialog;
+import com.android.sdkuilib.ui.GridDialog;
+
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.ProgressBar;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Widget;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+
+/**
+ * Implements a "view" that uses an existing progress bar, status button and
+ * status text to display a {@link ITaskMonitor}.
+ */
+public final class ProgressView implements IProgressUiProvider {
+
+    private static enum State {
+        /** View created but there's no task running. Next state can only be ACTIVE. */
+        IDLE,
+        /** A task is currently running. Next state is either STOP_PENDING or IDLE. */
+        ACTIVE,
+        /** Stop button has been clicked. Waiting for thread to finish. Next state is IDLE. */
+        STOP_PENDING,
+    }
+
+    /** The current mode of operation of the dialog. */
+    private State mState = State.IDLE;
+
+
+
+    // UI fields
+    private final Label mLabel;
+    private final Control mStopButton;
+    private final ProgressBar mProgressBar;
+
+    /** Logger object. Cannot not be null. */
+    private final ILogUiProvider mLog;
+
+    /**
+     * Creates a new {@link ProgressView} object, a simple "holder" for the various
+     * widgets used to display and update a progress + status bar.
+     *
+     * @param label The label to display titles of status updates (e.g. task titles and
+     *      calls to {@link #setDescription(String)}.) Must not be null.
+     * @param progressBar The progress bar to update during a task. Must not be null.
+     * @param stopButton The stop button. It will be disabled when there's no task that can
+     *      be interrupted. A selection listener will be attached to it. Optional. Can be null.
+     * @param log A <em>mandatory</em> logger object that will be used to report all the log.
+     *      Must not be null.
+     */
+    public ProgressView(
+            Label label,
+            ProgressBar progressBar,
+            Control stopButton,
+            ILogUiProvider log) {
+        mLabel = label;
+        mProgressBar = progressBar;
+        mLog = log;
+        mProgressBar.setEnabled(false);
+
+        mStopButton = stopButton;
+        if (mStopButton != null) {
+            mStopButton.addListener(SWT.Selection, new Listener() {
+                @Override
+                public void handleEvent(Event event) {
+                    if (mState == State.ACTIVE) {
+                        changeState(State.STOP_PENDING);
+                    }
+                }
+            });
+        }
+    }
+
+    /**
+     * Starts the task and block till it's either finished or canceled.
+     * This can be called from a non-UI thread safely.
+     * <p/>
+     * When a task is started from within a monitor, it reuses the thread
+     * from the parent. Otherwise it starts a new thread and runs it own
+     * UI loop. This means the task can perform UI operations using
+     * {@link Display#asyncExec(Runnable)}.
+     * <p/>
+     * In either case, the method only returns when the task has finished.
+     */
+    public void startTask(
+            final String title,
+            final ITaskMonitor parentMonitor,
+            final ITask task) {
+        if (task != null) {
+            try {
+                if (parentMonitor == null && !mProgressBar.isDisposed()) {
+                    mLabel.setText(title);
+                    mProgressBar.setSelection(0);
+                    mProgressBar.setEnabled(true);
+                    changeState(ProgressView.State.ACTIVE);
+                }
+
+                Runnable r = new Runnable() {
+                    @Override
+                    public void run() {
+                        if (parentMonitor == null) {
+                            task.run(new TaskMonitorImpl(ProgressView.this));
+
+                        } else {
+                            // Use all the reminder of the parent monitor.
+                            if (parentMonitor.getProgressMax() == 0) {
+                                parentMonitor.setProgressMax(1);
+                            }
+                            ITaskMonitor sub = parentMonitor.createSubMonitor(
+                                    parentMonitor.getProgressMax() - parentMonitor.getProgress());
+                            try {
+                                task.run(sub);
+                            } finally {
+                                int delta =
+                                    sub.getProgressMax() - sub.getProgress();
+                                if (delta > 0) {
+                                    sub.incProgress(delta);
+                                }
+                            }
+                        }
+                    }
+                };
+
+                // If for some reason the UI has been disposed, just abort the thread.
+                if (mProgressBar.isDisposed()) {
+                    return;
+                }
+
+                if (TaskMonitorImpl.isTaskMonitorImpl(parentMonitor)) {
+                    // If there's a parent monitor and it's our own class, we know this parent
+                    // is already running a thread and the base one is running an event loop.
+                    // We should thus not run a second event loop and we can process the
+                    // runnable right here instead of spawning a thread inside the thread.
+                    r.run();
+
+                } else {
+                    // No parent monitor. This is the first one so we need a thread and
+                    // we need to process UI events.
+
+                    final Thread t = new Thread(r, title);
+                    t.start();
+
+                    // Process the app's event loop whilst we wait for the thread to finish
+                    while (!mProgressBar.isDisposed() && t.isAlive()) {
+                        Display display = mProgressBar.getDisplay();
+                        if (!mProgressBar.isDisposed() && !display.readAndDispatch()) {
+                            display.sleep();
+                        }
+                    }
+                }
+            } catch (Exception e) {
+                // TODO log
+
+            } finally {
+                if (parentMonitor == null && !mProgressBar.isDisposed()) {
+                    changeState(ProgressView.State.IDLE);
+                    mProgressBar.setSelection(0);
+                    mProgressBar.setEnabled(false);
+                }
+            }
+        }
+    }
+
+    private void syncExec(final Widget widget, final Runnable runnable) {
+        if (widget != null && !widget.isDisposed()) {
+            widget.getDisplay().syncExec(new Runnable() {
+                @Override
+                public void run() {
+                    // Check again whether the widget got disposed between the time where
+                    // we requested the syncExec and the time it actually happened.
+                    if (!widget.isDisposed()) {
+                        runnable.run();
+                    }
+                }
+            });
+        }
+    }
+
+    private void changeState(State state) {
+        if (mState != null ) {
+            mState = state;
+        }
+
+        syncExec(mStopButton, new Runnable() {
+            @Override
+            public void run() {
+                mStopButton.setEnabled(mState == State.ACTIVE);
+            }
+        });
+
+    }
+
+    // --- Implementation of ITaskUiProvider ---
+
+    @Override
+    public boolean isCancelRequested() {
+        return mState != State.ACTIVE;
+    }
+
+    /**
+     * Sets the description in the current task dialog.
+     * This method can be invoked from a non-UI thread.
+     */
+    @Override
+    public void setDescription(final String description) {
+        syncExec(mLabel, new Runnable() {
+            @Override
+            public void run() {
+                mLabel.setText(description);
+            }
+        });
+
+        mLog.setDescription(description);
+    }
+
+    /**
+     * Logs a "normal" information line.
+     * This method can be invoked from a non-UI thread.
+     */
+    @Override
+    public void log(String log) {
+        mLog.log(log);
+    }
+
+    /**
+     * Logs an "error" information line.
+     * This method can be invoked from a non-UI thread.
+     */
+    @Override
+    public void logError(String log) {
+        mLog.logError(log);
+    }
+
+    /**
+     * Logs a "verbose" information line, that is extra details which are typically
+     * not that useful for the end-user and might be hidden until explicitly shown.
+     * This method can be invoked from a non-UI thread.
+     */
+    @Override
+    public void logVerbose(String log) {
+        mLog.logVerbose(log);
+    }
+
+    /**
+     * Sets the max value of the progress bar.
+     * This method can be invoked from a non-UI thread.
+     *
+     * @see ProgressBar#setMaximum(int)
+     */
+    @Override
+    public void setProgressMax(final int max) {
+        syncExec(mProgressBar, new Runnable() {
+            @Override
+            public void run() {
+                mProgressBar.setMaximum(max);
+            }
+        });
+    }
+
+    /**
+     * Sets the current value of the progress bar.
+     * This method can be invoked from a non-UI thread.
+     */
+    @Override
+    public void setProgress(final int value) {
+        syncExec(mProgressBar, new Runnable() {
+            @Override
+            public void run() {
+                mProgressBar.setSelection(value);
+            }
+        });
+    }
+
+    /**
+     * Returns the current value of the progress bar,
+     * between 0 and up to {@link #setProgressMax(int)} - 1.
+     * This method can be invoked from a non-UI thread.
+     */
+    @Override
+    public int getProgress() {
+        final int[] result = new int[] { 0 };
+
+        if (!mProgressBar.isDisposed()) {
+            mProgressBar.getDisplay().syncExec(new Runnable() {
+                @Override
+                public void run() {
+                    if (!mProgressBar.isDisposed()) {
+                        result[0] = mProgressBar.getSelection();
+                    }
+                }
+            });
+        }
+
+        return result[0];
+    }
+
+    @Override
+    public boolean displayPrompt(final String title, final String message) {
+        final boolean[] result = new boolean[] { false };
+
+        syncExec(mProgressBar, new Runnable() {
+            @Override
+            public void run() {
+                Shell shell = mProgressBar.getShell();
+                result[0] = MessageDialog.openQuestion(shell, title, message);
+            }
+        });
+
+        return result[0];
+    }
+
+    /**
+     * This method opens a pop-up window which requests for User Credentials.
+     *
+     * @param title The title of the window.
+     * @param message The message to displayed in the login/password window.
+     * @return Returns user provided credentials.
+     *         If operation is <b>canceled</b> by user the return value must be <b>null</b>.
+     * @see ITaskMonitor#displayLoginCredentialsPrompt(String, String)
+     */
+    @Override
+    public UserCredentials
+            displayLoginCredentialsPrompt(final String title, final String message) {
+        final AtomicReference<UserCredentials> result = new AtomicReference<UserCredentials>(null);
+
+        // open dialog and request login and password
+        syncExec(mProgressBar, new Runnable() {
+            @Override
+            public void run() {
+                Shell shell = mProgressBar.getShell();
+                AuthenticationDialog authenticationDialog = new AuthenticationDialog(shell,
+                        title,
+                        message);
+                int dlgResult = authenticationDialog.open();
+                if (dlgResult == GridDialog.OK) {
+                    result.set(new UserCredentials(
+                        authenticationDialog.getLogin(),
+                        authenticationDialog.getPassword(),
+                        authenticationDialog.getWorkstation(),
+                        authenticationDialog.getDomain()));
+                }
+            }
+        });
+
+        return result.get();
+    }
+}
+
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressViewFactory.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressViewFactory.java
new file mode 100755
index 0000000..2590169
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressViewFactory.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.tasks;
+
+import com.android.sdklib.internal.repository.ITask;
+import com.android.sdklib.internal.repository.ITaskFactory;
+import com.android.sdklib.internal.repository.ITaskMonitor;
+
+/**
+ * An {@link ITaskFactory} that creates a new {@link ProgressTask} dialog
+ * for each new task.
+ */
+public final class ProgressViewFactory implements ITaskFactory {
+
+    private ProgressView mProgressView;
+
+    public ProgressViewFactory() {
+    }
+
+    public void setProgressView(ProgressView progressView) {
+        mProgressView = progressView;
+    }
+
+    @Override
+    public void start(String title, ITask task) {
+        start(title, null /*monitor*/, task);
+    }
+
+    @Override
+    public void start(String title, ITaskMonitor parentMonitor, ITask task) {
+        assert mProgressView != null;
+        mProgressView.startTask(title, parentMonitor, task);
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/TaskMonitorImpl.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/TaskMonitorImpl.java
new file mode 100755
index 0000000..4d4f3c9
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/TaskMonitorImpl.java
@@ -0,0 +1,369 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.tasks;
+
+import com.android.annotations.NonNull;
+import com.android.sdklib.internal.repository.ITaskMonitor;
+import com.android.sdklib.internal.repository.UserCredentials;
+
+/**
+ * Internal class that implements the logic of an {@link ITaskMonitor}.
+ * It doesn't deal with any UI directly. Instead it delegates the UI to
+ * the provided {@link IProgressUiProvider}.
+ */
+class TaskMonitorImpl implements ITaskMonitor {
+
+    private static final double MAX_COUNT = 10000.0;
+
+    private interface ISubTaskMonitor extends ITaskMonitor {
+        public void subIncProgress(double realDelta);
+    }
+
+    private double mIncCoef = 0;
+    private double mValue = 0;
+    private final IProgressUiProvider mUi;
+
+    /**
+     * Returns true if the given {@code monitor} is an instance of {@link TaskMonitorImpl}
+     * or its private SubTaskMonitor.
+     */
+    public static boolean isTaskMonitorImpl(ITaskMonitor monitor) {
+        return monitor instanceof TaskMonitorImpl || monitor instanceof SubTaskMonitor;
+    }
+
+    /**
+     * Constructs a new {@link TaskMonitorImpl} that relies on the given
+     * {@link IProgressUiProvider} to change the user interface.
+     * @param ui The {@link IProgressUiProvider}. Cannot be null.
+     */
+    public TaskMonitorImpl(IProgressUiProvider ui) {
+        mUi = ui;
+    }
+
+    /** Returns the {@link IProgressUiProvider} passed to the constructor. */
+    public IProgressUiProvider getUiProvider() {
+        return mUi;
+    }
+
+    /**
+     * Sets the description in the current task dialog.
+     * This method can be invoked from a non-UI thread.
+     */
+    @Override
+    public void setDescription(String format, Object... args) {
+        final String text = String.format(format, args);
+        mUi.setDescription(text);
+    }
+
+    /**
+     * Logs a "normal" information line.
+     * This method can be invoked from a non-UI thread.
+     */
+    @Override
+    public void log(String format, Object... args) {
+        String text = String.format(format, args);
+        mUi.log(text);
+    }
+
+    /**
+     * Logs an "error" information line.
+     * This method can be invoked from a non-UI thread.
+     */
+    @Override
+    public void logError(String format, Object... args) {
+        String text = String.format(format, args);
+        mUi.logError(text);
+    }
+
+    /**
+     * Logs a "verbose" information line, that is extra details which are typically
+     * not that useful for the end-user and might be hidden until explicitly shown.
+     * This method can be invoked from a non-UI thread.
+     */
+    @Override
+    public void logVerbose(String format, Object... args) {
+        String text = String.format(format, args);
+        mUi.logVerbose(text);
+    }
+
+    /**
+     * Sets the max value of the progress bar.
+     * This method can be invoked from a non-UI thread.
+     *
+     * Weird things will happen if setProgressMax is called multiple times
+     * *after* {@link #incProgress(int)}: we don't try to adjust it on the
+     * fly.
+     */
+    @Override
+    public void setProgressMax(int max) {
+        assert max > 0;
+        // Always set the dialog's progress max to 10k since it only handles
+        // integers and we want to have a better inner granularity. Instead
+        // we use the max to compute a coefficient for inc deltas.
+        mUi.setProgressMax((int) MAX_COUNT);
+        mIncCoef = max > 0 ? MAX_COUNT / max : 0;
+        assert mIncCoef > 0;
+    }
+
+    @Override
+    public int getProgressMax() {
+        return mIncCoef > 0 ? (int) (MAX_COUNT / mIncCoef) : 0;
+    }
+
+    /**
+     * Increments the current value of the progress bar.
+     *
+     * This method can be invoked from a non-UI thread.
+     */
+    @Override
+    public void incProgress(int delta) {
+        if (delta > 0 && mIncCoef > 0) {
+            internalIncProgress(delta * mIncCoef);
+        }
+    }
+
+    private void internalIncProgress(double realDelta) {
+        mValue += realDelta;
+        mUi.setProgress((int)mValue);
+    }
+
+    /**
+     * Returns the current value of the progress bar,
+     * between 0 and up to {@link #setProgressMax(int)} - 1.
+     *
+     * This method can be invoked from a non-UI thread.
+     */
+    @Override
+    public int getProgress() {
+        // mIncCoef is 0 if setProgressMax hasn't been used yet.
+        return mIncCoef > 0 ? (int)(mUi.getProgress() / mIncCoef) : 0;
+    }
+
+    /**
+     * Returns true if the "Cancel" button was selected.
+     * It is up to the task thread to pool this and exit.
+     */
+    @Override
+    public boolean isCancelRequested() {
+        return mUi.isCancelRequested();
+    }
+
+    /**
+     * Displays a yes/no question dialog box.
+     *
+     * This implementation allow this to be called from any thread, it
+     * makes sure the dialog is opened synchronously in the ui thread.
+     *
+     * @param title The title of the dialog box
+     * @param message The error message
+     * @return true if YES was clicked.
+     */
+    @Override
+    public boolean displayPrompt(final String title, final String message) {
+        return mUi.displayPrompt(title, message);
+    }
+
+    /**
+     * Displays a Login/Password dialog. This implementation allows this method to be
+     * called from any thread, it makes sure the dialog is opened synchronously
+     * in the ui thread.
+     *
+     * @param title The title of the dialog box
+     * @param message Message to be displayed
+     * @return Pair with entered login/password. Login is always the first
+     *         element and Password is always the second. If any error occurs a
+     *         pair with empty strings is returned.
+     */
+    @Override
+    public UserCredentials displayLoginCredentialsPrompt(String title, String message) {
+        return mUi.displayLoginCredentialsPrompt(title, message);
+    }
+
+    /**
+     * Creates a sub-monitor that will use up to tickCount on the progress bar.
+     * tickCount must be 1 or more.
+     */
+    @Override
+    public ITaskMonitor createSubMonitor(int tickCount) {
+        assert mIncCoef > 0;
+        assert tickCount > 0;
+        return new SubTaskMonitor(this, null, mValue, tickCount * mIncCoef);
+    }
+
+    // ----- ILogger interface ----
+
+    @Override
+    public void error(Throwable throwable, String errorFormat, Object... arg) {
+        if (errorFormat != null) {
+            logError("Error: " + errorFormat, arg);
+        }
+
+        if (throwable != null) {
+            logError("%s", throwable.getMessage()); //$NON-NLS-1$
+        }
+    }
+
+    @Override
+    public void warning(@NonNull String warningFormat, Object... arg) {
+        log("Warning: " + warningFormat, arg);
+    }
+
+    @Override
+    public void info(@NonNull String msgFormat, Object... arg) {
+        log(msgFormat, arg);
+    }
+
+    @Override
+    public void verbose(@NonNull String msgFormat, Object... arg) {
+        log(msgFormat, arg);
+    }
+
+    // ----- Sub Monitor -----
+
+    private static class SubTaskMonitor implements ISubTaskMonitor {
+
+        private final TaskMonitorImpl mRoot;
+        private final ISubTaskMonitor mParent;
+        private final double mStart;
+        private final double mSpan;
+        private double mSubValue;
+        private double mSubCoef;
+
+        /**
+         * Creates a new sub task monitor which will work for the given range [start, start+span]
+         * in its parent.
+         *
+         * @param taskMonitor The ProgressTask root
+         * @param parent The immediate parent. Can be the null or another sub task monitor.
+         * @param start The start value in the root's coordinates
+         * @param span The span value in the root's coordinates
+         */
+        public SubTaskMonitor(TaskMonitorImpl taskMonitor,
+                ISubTaskMonitor parent,
+                double start,
+                double span) {
+            mRoot = taskMonitor;
+            mParent = parent;
+            mStart = start;
+            mSpan = span;
+            mSubValue = start;
+        }
+
+        @Override
+        public boolean isCancelRequested() {
+            return mRoot.isCancelRequested();
+        }
+
+        @Override
+        public void setDescription(String format, Object... args) {
+            mRoot.setDescription(format, args);
+        }
+
+        @Override
+        public void log(String format, Object... args) {
+            mRoot.log(format, args);
+        }
+
+        @Override
+        public void logError(String format, Object... args) {
+            mRoot.logError(format, args);
+        }
+
+        @Override
+        public void logVerbose(String format, Object... args) {
+            mRoot.logVerbose(format, args);
+        }
+
+        @Override
+        public void setProgressMax(int max) {
+            assert max > 0;
+            mSubCoef = max > 0 ? mSpan / max : 0;
+            assert mSubCoef > 0;
+        }
+
+        @Override
+        public int getProgressMax() {
+            return mSubCoef > 0 ? (int) (mSpan / mSubCoef) : 0;
+        }
+
+        @Override
+        public int getProgress() {
+            // subCoef can be 0 if setProgressMax() and incProgress() haven't been called yet
+            assert mSubValue == mStart || mSubCoef > 0;
+            return mSubCoef > 0 ? (int)((mSubValue - mStart) / mSubCoef) : 0;
+        }
+
+        @Override
+        public void incProgress(int delta) {
+            if (delta > 0 && mSubCoef > 0) {
+                subIncProgress(delta * mSubCoef);
+            }
+        }
+
+        @Override
+        public void subIncProgress(double realDelta) {
+            mSubValue += realDelta;
+            if (mParent != null) {
+                mParent.subIncProgress(realDelta);
+            } else {
+                mRoot.internalIncProgress(realDelta);
+            }
+        }
+
+        @Override
+        public boolean displayPrompt(String title, String message) {
+            return mRoot.displayPrompt(title, message);
+        }
+
+        @Override
+        public UserCredentials displayLoginCredentialsPrompt(String title, String message) {
+            return mRoot.displayLoginCredentialsPrompt(title, message);
+        }
+
+        @Override
+        public ITaskMonitor createSubMonitor(int tickCount) {
+            assert mSubCoef > 0;
+            assert tickCount > 0;
+            return new SubTaskMonitor(mRoot,
+                    this,
+                    mSubValue,
+                    tickCount * mSubCoef);
+        }
+
+        // ----- ILogger interface ----
+
+        @Override
+        public void error(Throwable throwable, String errorFormat, Object... arg) {
+            mRoot.error(throwable, errorFormat, arg);
+        }
+
+        @Override
+        public void warning(@NonNull String warningFormat, Object... arg) {
+            mRoot.warning(warningFormat, arg);
+        }
+
+        @Override
+        public void info(@NonNull String msgFormat, Object... arg) {
+            mRoot.info(msgFormat, arg);
+        }
+
+        @Override
+        public void verbose(@NonNull String msgFormat, Object... arg) {
+            mRoot.verbose(msgFormat, arg);
+        }
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationDialog.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationDialog.java
new file mode 100644
index 0000000..c583762
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationDialog.java
@@ -0,0 +1,1392 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.widgets;
+
+import com.android.SdkConstants;
+import com.android.annotations.Nullable;
+import com.android.prefs.AndroidLocation.AndroidLocationException;
+import com.android.resources.Density;
+import com.android.resources.ScreenSize;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.ISystemImage;
+import com.android.sdklib.SdkManager;
+import com.android.sdklib.devices.Camera;
+import com.android.sdklib.devices.CameraLocation;
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.devices.DeviceManager;
+import com.android.sdklib.devices.Hardware;
+import com.android.sdklib.devices.Screen;
+import com.android.sdklib.devices.Software;
+import com.android.sdklib.devices.Storage;
+import com.android.sdklib.internal.avd.AvdInfo;
+import com.android.sdklib.internal.avd.AvdManager;
+import com.android.sdklib.internal.avd.AvdManager.AvdConflict;
+import com.android.sdklib.internal.avd.HardwareProperties;
+import com.android.sdkuilib.internal.repository.icons.ImageFactory;
+import com.android.sdkuilib.ui.GridDialog;
+import com.android.utils.ILogger;
+import com.android.utils.Pair;
+
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.VerifyEvent;
+import org.eclipse.swt.events.VerifyListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class AvdCreationDialog extends GridDialog {
+
+    private AvdManager mAvdManager;
+    private ImageFactory mImageFactory;
+    private ILogger mSdkLog;
+    private AvdInfo mAvdInfo;
+    private boolean mHaveSystemImage;
+
+    private final TreeMap<String, IAndroidTarget> mCurrentTargets =
+            new TreeMap<String, IAndroidTarget>();
+
+    private Button mOkButton;
+
+    private Text mAvdName;
+
+    private Combo mDevice;
+
+    private Combo mTarget;
+    private Combo mAbi;
+
+    private Button mKeyboard;
+    private Button mSkin;
+
+    private Combo mFrontCamera;
+    private Combo mBackCamera;
+
+    private Button mSnapshot;
+    private Button mGpuEmulation;
+
+    private Text mRam;
+    private Text mVmHeap;
+
+    private Text mDataPartition;
+    private Combo mDataPartitionSize;
+
+    private Button mSdCardSizeRadio;
+    private Text mSdCardSize;
+    private Combo mSdCardSizeCombo;
+    private Button mSdCardFileRadio;
+    private Text mSdCardFile;
+    private Button mBrowseSdCard;
+
+    private Button mForceCreation;
+    private Composite mStatusComposite;
+
+    private Label mStatusIcon;
+    private Label mStatusLabel;
+
+    private Device mInitWithDevice;
+    private AvdInfo mCreatedAvd;
+
+    /**
+     * {@link VerifyListener} for {@link Text} widgets that should only contains
+     * numbers.
+     */
+    private final VerifyListener mDigitVerifier = new VerifyListener() {
+        @Override
+        public void verifyText(VerifyEvent event) {
+            int count = event.text.length();
+            for (int i = 0; i < count; i++) {
+                char c = event.text.charAt(i);
+                if (c < '0' || c > '9') {
+                    event.doit = false;
+                    return;
+                }
+            }
+        }
+    };
+
+    public AvdCreationDialog(Shell shell,
+            AvdManager avdManager,
+            ImageFactory imageFactory,
+            ILogger log,
+            AvdInfo editAvdInfo) {
+
+        super(shell, 2, false);
+        mAvdManager = avdManager;
+        mImageFactory = imageFactory;
+        mSdkLog = log;
+        mAvdInfo = editAvdInfo;
+    }
+
+    /** Returns the AVD Created, if successful. */
+    public AvdInfo getCreatedAvd() {
+        return mCreatedAvd;
+    }
+
+    @Override
+    protected Control createContents(Composite parent) {
+        Control control = super.createContents(parent);
+        getShell().setText(mAvdInfo == null ? "Create new Android Virtual Device (AVD)"
+                                            : "Edit Android Virtual Device (AVD)");
+
+        mOkButton = getButton(IDialogConstants.OK_ID);
+
+        if (mAvdInfo != null) {
+            fillExistingAvdInfo(mAvdInfo);
+        } else if (mInitWithDevice != null) {
+            fillInitialDeviceInfo(mInitWithDevice);
+        }
+
+        validatePage();
+        return control;
+    }
+
+    @Override
+    public void createDialogContent(Composite parent) {
+
+        Label label;
+        String tooltip;
+        ValidateListener validateListener = new ValidateListener();
+
+        // --- avd name
+        label = new Label(parent, SWT.NONE);
+        label.setText("AVD Name:");
+        tooltip = "The name of the Android Virtual Device";
+        label.setToolTipText(tooltip);
+        mAvdName = new Text(parent, SWT.BORDER);
+        mAvdName.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mAvdName.addModifyListener(new CreateNameModifyListener());
+
+        // --- device selection
+        label = new Label(parent, SWT.NONE);
+        label.setText("Device:");
+        tooltip = "The device this AVD will be based on";
+        mDevice = new Combo(parent, SWT.READ_ONLY | SWT.DROP_DOWN);
+        mDevice.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        initializeDevices();
+        mDevice.addSelectionListener(new DeviceSelectionListener());
+
+        // --- api target
+        label = new Label(parent, SWT.NONE);
+        label.setText("Target:");
+        tooltip = "The target API of the AVD";
+        label.setToolTipText(tooltip);
+        mTarget = new Combo(parent, SWT.READ_ONLY | SWT.DROP_DOWN);
+        mTarget.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mTarget.setToolTipText(tooltip);
+        mTarget.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                reloadAbiTypeCombo();
+                validatePage();
+            }
+        });
+
+        reloadTargetCombo();
+
+        // --- avd ABIs
+        label = new Label(parent, SWT.NONE);
+        label.setText("CPU/ABI:");
+        tooltip = "The CPU/ABI of the virtual device";
+        label.setToolTipText(tooltip);
+        mAbi = new Combo(parent, SWT.READ_ONLY | SWT.DROP_DOWN);
+        mAbi.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mAbi.setToolTipText(tooltip);
+        mAbi.addSelectionListener(validateListener);
+
+        label = new Label(parent, SWT.NONE);
+        label.setText("Keyboard:");
+        mKeyboard = new Button(parent, SWT.CHECK);
+        mKeyboard.setSelection(true); // default to having a keyboard irrespective of device
+        mKeyboard.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mKeyboard.setText("Hardware keyboard present");
+
+        label = new Label(parent, SWT.NONE);
+        label.setText("Skin:");
+        mSkin = new Button(parent, SWT.CHECK);
+        mSkin.setSelection(true);
+        mSkin.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mSkin.setText("Display a skin with hardware controls");
+
+        label = new Label(parent, SWT.NONE);
+        label.setText("Front Camera:");
+        tooltip = "";
+        label.setToolTipText(tooltip);
+        mFrontCamera = new Combo(parent, SWT.READ_ONLY | SWT.DROP_DOWN);
+        mFrontCamera.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mFrontCamera.add("None");
+        mFrontCamera.add("Emulated");
+        mFrontCamera.add("Webcam0");
+        mFrontCamera.select(0);
+
+        label = new Label(parent, SWT.NONE);
+        label.setText("Back Camera:");
+        tooltip = "";
+        label.setToolTipText(tooltip);
+        mBackCamera = new Combo(parent, SWT.READ_ONLY | SWT.DROP_DOWN);
+        mBackCamera.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mBackCamera.add("None");
+        mBackCamera.add("Emulated");
+        mBackCamera.add("Webcam0");
+        mBackCamera.select(0);
+
+        toggleCameras();
+
+        // --- memory options group
+        label = new Label(parent, SWT.NONE);
+        label.setText("Memory Options:");
+
+
+        Group memoryGroup = new Group(parent, SWT.BORDER);
+        memoryGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        memoryGroup.setLayout(new GridLayout(4, false));
+
+        label = new Label(memoryGroup, SWT.NONE);
+        label.setText("RAM:");
+        tooltip = "The amount of RAM the emulated device should have in MiB";
+        label.setToolTipText(tooltip);
+        mRam = new Text(memoryGroup, SWT.BORDER);
+        mRam.addVerifyListener(mDigitVerifier);
+        mRam.addModifyListener(validateListener);
+        mRam.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        label = new Label(memoryGroup, SWT.NONE);
+        label.setText("VM Heap:");
+        tooltip = "The amount of memory, in MiB, available to typical Android applications";
+        label.setToolTipText(tooltip);
+        mVmHeap = new Text(memoryGroup, SWT.BORDER);
+        mVmHeap.addVerifyListener(mDigitVerifier);
+        mVmHeap.addModifyListener(validateListener);
+        mVmHeap.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mVmHeap.setToolTipText(tooltip);
+
+        // --- Data partition group
+        label = new Label(parent, SWT.NONE);
+        label.setText("Internal Storage:");
+        tooltip = "The size of the data partition on the device.";
+        Group storageGroup = new Group(parent, SWT.NONE);
+        storageGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        storageGroup.setLayout(new GridLayout(2, false));
+        mDataPartition = new Text(storageGroup, SWT.BORDER);
+        mDataPartition.setText("200");
+        mDataPartition.addVerifyListener(mDigitVerifier);
+        mDataPartition.addModifyListener(validateListener);
+        mDataPartition.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mDataPartitionSize = new Combo(storageGroup, SWT.READ_ONLY | SWT.DROP_DOWN);
+        mDataPartitionSize.add("MiB");
+        mDataPartitionSize.add("GiB");
+        mDataPartitionSize.select(0);
+        mDataPartitionSize.addModifyListener(validateListener);
+
+        // --- sd card group
+        label = new Label(parent, SWT.NONE);
+        label.setText("SD Card:");
+        label.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING,
+                false, false));
+
+        final Group sdCardGroup = new Group(parent, SWT.NONE);
+        sdCardGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        sdCardGroup.setLayout(new GridLayout(3, false));
+
+        mSdCardSizeRadio = new Button(sdCardGroup, SWT.RADIO);
+        mSdCardSizeRadio.setText("Size:");
+        mSdCardSizeRadio.setToolTipText("Create a new SD Card file");
+        mSdCardSizeRadio.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                boolean sizeMode = mSdCardSizeRadio.getSelection();
+                enableSdCardWidgets(sizeMode);
+                validatePage();
+            }
+        });
+
+        mSdCardSize = new Text(sdCardGroup, SWT.BORDER);
+        mSdCardSize.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mSdCardSize.addVerifyListener(mDigitVerifier);
+        mSdCardSize.addModifyListener(validateListener);
+        mSdCardSize.setToolTipText("Size of the new SD Card file (must be at least 9 MiB)");
+
+        mSdCardSizeCombo = new Combo(sdCardGroup, SWT.DROP_DOWN | SWT.READ_ONLY);
+        mSdCardSizeCombo.add("KiB");
+        mSdCardSizeCombo.add("MiB");
+        mSdCardSizeCombo.add("GiB");
+        mSdCardSizeCombo.select(1);
+        mSdCardSizeCombo.addSelectionListener(validateListener);
+
+        mSdCardFileRadio = new Button(sdCardGroup, SWT.RADIO);
+        mSdCardFileRadio.setText("File:");
+        mSdCardFileRadio.setToolTipText("Use an existing file for the SD Card");
+
+        mSdCardFile = new Text(sdCardGroup, SWT.BORDER);
+        mSdCardFile.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mSdCardFile.addModifyListener(validateListener);
+        mSdCardFile.setToolTipText("File to use for the SD Card");
+
+        mBrowseSdCard = new Button(sdCardGroup, SWT.PUSH);
+        mBrowseSdCard.setText("Browse...");
+        mBrowseSdCard.setToolTipText("Select the file to use for the SD Card");
+        mBrowseSdCard.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                onBrowseSdCard();
+                validatePage();
+            }
+        });
+
+        mSdCardSizeRadio.setSelection(true);
+        enableSdCardWidgets(true);
+
+        // --- avd options group
+        label = new Label(parent, SWT.NONE);
+        label.setText("Emulation Options:");
+        Group optionsGroup = new Group(parent, SWT.NONE);
+        optionsGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        optionsGroup.setLayout(new GridLayout(2, true));
+        mSnapshot = new Button(optionsGroup, SWT.CHECK);
+        mSnapshot.setText("Snapshot");
+        mSnapshot.setToolTipText("Emulator's state will be persisted between emulator executions");
+        mSnapshot.addSelectionListener(validateListener);
+        mGpuEmulation = new Button(optionsGroup, SWT.CHECK);
+        mGpuEmulation.setText("Use Host GPU");
+        mGpuEmulation.setToolTipText("Enable hardware OpenGLES emulation");
+        mGpuEmulation.addSelectionListener(validateListener);
+
+        // --- force creation group
+        mForceCreation = new Button(parent, SWT.CHECK);
+        mForceCreation.setText("Override the existing AVD with the same name");
+        mForceCreation
+                .setToolTipText("There's already an AVD with the same name. Check this to delete it and replace it by the new AVD.");
+        mForceCreation.setLayoutData(new GridData(GridData.BEGINNING, GridData.CENTER,
+                true, false, 2, 1));
+        mForceCreation.setEnabled(false);
+        mForceCreation.addSelectionListener(validateListener);
+
+        // add a separator to separate from the ok/cancel button
+        label = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL);
+        label.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, true, false, 3, 1));
+
+        // add stuff for the error display
+        mStatusComposite = new Composite(parent, SWT.NONE);
+        mStatusComposite.setLayoutData(new GridData(GridData.FILL, GridData.CENTER,
+                true, false, 3, 1));
+        GridLayout gl;
+        mStatusComposite.setLayout(gl = new GridLayout(2, false));
+        gl.marginHeight = gl.marginWidth = 0;
+
+        mStatusIcon = new Label(mStatusComposite, SWT.NONE);
+        mStatusIcon.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING,
+                false, false));
+        mStatusLabel = new Label(mStatusComposite, SWT.NONE);
+        mStatusLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mStatusLabel.setText(""); //$NON-NLS-1$
+    }
+
+    @Nullable
+    private Device getSelectedDevice() {
+        Device[] devices = (Device[]) mDevice.getData();
+        if (devices != null) {
+            int index = mDevice.getSelectionIndex();
+            if (index != -1 && index < devices.length) {
+                return devices[index];
+            }
+        }
+
+        return null;
+    }
+
+    private void selectDevice(String manufacturer, String name) {
+        Device[] devices = (Device[]) mDevice.getData();
+        if (devices != null) {
+            for (int i = 0, n = devices.length; i < n; i++) {
+                Device device = devices[i];
+                if (device.getManufacturer().equals(manufacturer)
+                        && device.getName().equals(name)) {
+                    mDevice.select(i);
+                    break;
+                }
+            }
+        }
+    }
+
+    private void selectDevice(Device device) {
+        Device[] devices = (Device[]) mDevice.getData();
+        if (devices != null) {
+            for (int i = 0, n = devices.length; i < n; i++) {
+                if (devices[i].equals(device)) {
+                    mDevice.select(i);
+                    break;
+                }
+            }
+        }
+    }
+
+    private void initializeDevices() {
+        assert mDevice != null;
+
+        SdkManager sdkManager = mAvdManager.getSdkManager();
+        String location = sdkManager.getLocation();
+        if (sdkManager != null && location != null) {
+            DeviceManager deviceManager = DeviceManager.createInstance(location, mSdkLog);
+            List<Device>  deviceList    = deviceManager.getDevices(DeviceManager.ALL_DEVICES);
+
+            // Sort
+            List<Device> nexus = new ArrayList<Device>(deviceList.size());
+            List<Device> other = new ArrayList<Device>(deviceList.size());
+            for (Device device : deviceList) {
+                if (isNexus(device) && !isGeneric(device)) {
+                    nexus.add(device);
+                } else {
+                    other.add(device);
+                }
+            }
+            Collections.reverse(other);
+            Collections.sort(nexus, new Comparator<Device>() {
+                @Override
+                public int compare(Device device1, Device device2) {
+                    // Descending order of age
+                    return nexusRank(device2) - nexusRank(device1);
+                }
+            });
+            List<Device> all = nexus;
+            all.addAll(other);
+
+            Device[] devices = all.toArray(new Device[all.size()]);
+            String[] labels = new String[devices.length];
+            for (int i = 0, n = devices.length; i < n; i++) {
+                Device device = devices[i];
+                if (isNexus(device) && !isGeneric(device)) {
+                    labels[i] = getNexusLabel(device);
+                } else {
+                    labels[i] = getGenericLabel(device);
+                }
+            }
+            mDevice.setData(devices);
+            mDevice.setItems(labels);
+        }
+    }
+
+    /**
+     * Can be called after the constructor to set the default device for this AVD.
+     * Useful especially for new AVDs.
+     * @param device
+     */
+    public void selectInitialDevice(Device device) {
+        mInitWithDevice = device;
+    }
+
+    /**
+     * {@link ModifyListener} used for live-validation of the fields content.
+     */
+    private class ValidateListener extends SelectionAdapter implements ModifyListener {
+        @Override
+        public void modifyText(ModifyEvent e) {
+            validatePage();
+        }
+
+        @Override
+        public void widgetSelected(SelectionEvent e) {
+            super.widgetSelected(e);
+            validatePage();
+        }
+    }
+
+    /**
+     * Callback when the AVD name is changed. When creating a new AVD, enables
+     * the force checkbox if the name is a duplicate. When editing an existing
+     * AVD, it's OK for the name to match the existing AVD.
+     */
+    private class CreateNameModifyListener implements ModifyListener {
+        @Override
+        public void modifyText(ModifyEvent e) {
+            String name = mAvdName.getText().trim();
+            if (mAvdInfo == null || !name.equals(mAvdInfo.getName())) {
+                // Case where we're creating a new AVD or editing an existing
+                // one
+                // and the AVD name has been changed... check for name
+                // uniqueness.
+
+                Pair<AvdConflict, String> conflict = mAvdManager.isAvdNameConflicting(name);
+                if (conflict.getFirst() != AvdManager.AvdConflict.NO_CONFLICT) {
+                    // If we're changing the state from disabled to enabled,
+                    // make sure
+                    // to uncheck the button, to force the user to voluntarily
+                    // re-enforce it.
+                    // This happens when editing an existing AVD and changing
+                    // the name from
+                    // the existing AVD to another different existing AVD.
+                    if (!mForceCreation.isEnabled()) {
+                        mForceCreation.setEnabled(true);
+                        mForceCreation.setSelection(false);
+                    }
+                } else {
+                    mForceCreation.setEnabled(false);
+                    mForceCreation.setSelection(false);
+                }
+            } else {
+                // Case where we're editing an existing AVD with the name
+                // unchanged.
+
+                mForceCreation.setEnabled(false);
+                mForceCreation.setSelection(false);
+            }
+            validatePage();
+        }
+    }
+
+    private class DeviceSelectionListener extends SelectionAdapter {
+
+        @Override
+        public void widgetSelected(SelectionEvent arg0) {
+            Device currentDevice = getSelectedDevice();
+            if (currentDevice != null) {
+                fillDeviceProperties(currentDevice);
+            }
+
+            toggleCameras();
+            validatePage();
+        }
+    }
+
+    private void fillDeviceProperties(Device device) {
+        Hardware hw = device.getDefaultHardware();
+        Long ram = hw.getRam().getSizeAsUnit(Storage.Unit.MiB);
+        mRam.setText(Long.toString(ram));
+
+        // Set the default VM heap size. This is based on the Android CDD minimums for each
+        // screen size and density.
+        Screen s = hw.getScreen();
+        ScreenSize size = s.getSize();
+        Density density = s.getPixelDensity();
+        int vmHeapSize = 32;
+        if (size.equals(ScreenSize.XLARGE)) {
+            switch (density) {
+                case LOW:
+                case MEDIUM:
+                    vmHeapSize = 32;
+                    break;
+                case TV:
+                case HIGH:
+                    vmHeapSize = 64;
+                    break;
+                case XHIGH:
+                case XXHIGH:
+                    vmHeapSize = 128;
+                break;
+                case NODPI:
+                    break;
+            }
+        } else {
+            switch (density) {
+                case LOW:
+                case MEDIUM:
+                    vmHeapSize = 16;
+                    break;
+                case TV:
+                case HIGH:
+                    vmHeapSize = 32;
+                    break;
+                case XHIGH:
+                case XXHIGH:
+                    vmHeapSize = 64;
+                break;
+                case NODPI:
+                    break;
+            }
+        }
+        mVmHeap.setText(Integer.toString(vmHeapSize));
+
+        List<Software> allSoftware = device.getAllSoftware();
+        if (allSoftware != null && !allSoftware.isEmpty()) {
+            Software first = allSoftware.get(0);
+            int min = first.getMinSdkLevel();;
+            int max = first.getMaxSdkLevel();;
+            for (int i = 1; i < allSoftware.size(); i++) {
+                min = Math.min(min, first.getMinSdkLevel());
+                max = Math.max(max, first.getMaxSdkLevel());
+            }
+            if (mCurrentTargets != null) {
+                int bestApiLevel = Integer.MAX_VALUE;
+                IAndroidTarget bestTarget = null;
+                for (IAndroidTarget target : mCurrentTargets.values()) {
+                    if (!target.isPlatform()) {
+                        continue;
+                    }
+                    int apiLevel = target.getVersion().getApiLevel();
+                    if (apiLevel >= min && apiLevel <= max) {
+                        if (bestTarget == null || apiLevel < bestApiLevel) {
+                            bestTarget = target;
+                            bestApiLevel = apiLevel;
+                        }
+                    }
+                }
+
+                if (bestTarget != null) {
+                    selectTarget(bestTarget);
+                    reloadAbiTypeCombo();
+                }
+            }
+        }
+    }
+
+    private void toggleCameras() {
+        mFrontCamera.setEnabled(false);
+        mBackCamera.setEnabled(false);
+        Device d = getSelectedDevice();
+        if (d != null) {
+            for (Camera c : d.getDefaultHardware().getCameras()) {
+                if (CameraLocation.FRONT.equals(c.getLocation())) {
+                    mFrontCamera.setEnabled(true);
+                }
+                if (CameraLocation.BACK.equals(c.getLocation())) {
+                    mBackCamera.setEnabled(true);
+                }
+            }
+        }
+    }
+
+    private void reloadTargetCombo() {
+        String selected = null;
+        int index = mTarget.getSelectionIndex();
+        if (index >= 0) {
+            selected = mTarget.getItem(index);
+        }
+
+        mCurrentTargets.clear();
+        mTarget.removeAll();
+
+        boolean found = false;
+        index = -1;
+
+        List<IAndroidTarget> targetData = new ArrayList<IAndroidTarget>();
+        SdkManager sdkManager = mAvdManager.getSdkManager();
+        if (sdkManager != null) {
+            for (IAndroidTarget target : sdkManager.getTargets()) {
+                String name;
+                if (target.isPlatform()) {
+                    name = String.format("%s - API Level %s",
+                            target.getName(),
+                            target.getVersion().getApiString());
+                } else {
+                    name = String.format("%s (%s) - API Level %s",
+                            target.getName(),
+                            target.getVendor(),
+                            target.getVersion().getApiString());
+                }
+                mCurrentTargets.put(name, target);
+                mTarget.add(name);
+                targetData.add(target);
+                if (!found) {
+                    index++;
+                    found = name.equals(selected);
+                }
+            }
+        }
+
+        mTarget.setEnabled(mCurrentTargets.size() > 0);
+        mTarget.setData(targetData.toArray(new IAndroidTarget[targetData.size()]));
+
+        if (found) {
+            mTarget.select(index);
+        }
+    }
+
+    private void selectTarget(IAndroidTarget target) {
+        IAndroidTarget[] targets = (IAndroidTarget[]) mTarget.getData();
+        if (targets != null) {
+            for (int i = 0; i < targets.length; i++) {
+                if (target == targets[i]) {
+                    mTarget.select(i);
+                    break;
+                }
+            }
+        }
+    }
+
+    @SuppressWarnings("unused")
+    @Deprecated // FIXME unused, cleanup later
+    private IAndroidTarget getSelectedTarget() {
+        IAndroidTarget[] targets = (IAndroidTarget[]) mTarget.getData();
+        int index = mTarget.getSelectionIndex();
+        if (targets != null && index != -1 && index < targets.length) {
+            return targets[index];
+        }
+
+        return null;
+    }
+
+    /**
+     * Reload all the abi types in the selection list
+     */
+    private void reloadAbiTypeCombo() {
+        String selected = null;
+        boolean found = false;
+
+        int index = mTarget.getSelectionIndex();
+        if (index >= 0) {
+            String targetName = mTarget.getItem(index);
+            IAndroidTarget target = mCurrentTargets.get(targetName);
+
+            ISystemImage[] systemImages = getSystemImages(target);
+
+            mAbi.setEnabled(systemImages.length > 1);
+
+            // If user explicitly selected an ABI before, preserve that option
+            // If user did not explicitly select before (only one option before)
+            // force them to select
+            index = mAbi.getSelectionIndex();
+            if (index >= 0 && mAbi.getItemCount() > 1) {
+                selected = mAbi.getItem(index);
+            }
+
+            mAbi.removeAll();
+
+            int i;
+            for (i = 0; i < systemImages.length; i++) {
+                String prettyAbiType = AvdInfo.getPrettyAbiType(systemImages[i].getAbiType());
+                mAbi.add(prettyAbiType);
+                if (!found) {
+                    found = prettyAbiType.equals(selected);
+                    if (found) {
+                        mAbi.select(i);
+                    }
+                }
+            }
+
+            mHaveSystemImage = systemImages.length > 0;
+            if (!mHaveSystemImage) {
+                mAbi.add("No system images installed for this target.");
+                mAbi.select(0);
+            } else if (systemImages.length == 1) {
+                mAbi.select(0);
+            }
+        }
+    }
+
+    /**
+     * Enable or disable the sd card widgets.
+     *
+     * @param sizeMode if true the size-based widgets are to be enabled, and the
+     *            file-based ones disabled.
+     */
+    private void enableSdCardWidgets(boolean sizeMode) {
+        mSdCardSize.setEnabled(sizeMode);
+        mSdCardSizeCombo.setEnabled(sizeMode);
+
+        mSdCardFile.setEnabled(!sizeMode);
+        mBrowseSdCard.setEnabled(!sizeMode);
+    }
+
+    private void onBrowseSdCard() {
+        FileDialog dlg = new FileDialog(getContents().getShell(), SWT.OPEN);
+        dlg.setText("Choose SD Card image file.");
+
+        String fileName = dlg.open();
+        if (fileName != null) {
+            mSdCardFile.setText(fileName);
+        }
+    }
+
+    @Override
+    public void okPressed() {
+        if (createAvd()) {
+            super.okPressed();
+        }
+    }
+
+    private void validatePage() {
+        String error = null;
+        String warning = null;
+        boolean valid = true;
+
+        if (mAvdName.getText().isEmpty()) {
+            error = "AVD Name cannot be empty";
+            setPageValid(false, error, warning);
+            return;
+        }
+
+        String avdName = mAvdName.getText();
+        if (!AvdManager.RE_AVD_NAME.matcher(avdName).matches()) {
+            error = String.format(
+                    "AVD name '%1$s' contains invalid characters.\nAllowed characters are: %2$s",
+                    avdName, AvdManager.CHARS_AVD_NAME);
+            setPageValid(false, error, warning);
+            return;
+        }
+
+        if (mDevice.getSelectionIndex() < 0) {
+            setPageValid(false, error, warning);
+            return;
+        }
+
+        if (mTarget.getSelectionIndex() < 0 ||
+                !mHaveSystemImage || mAbi.getSelectionIndex() < 0) {
+            setPageValid(false, error, warning);
+            return;
+        }
+
+        if (mRam.getText().isEmpty()) {
+            setPageValid(false, error, warning);
+            return;
+        }
+
+        if (mVmHeap.getText().isEmpty()) {
+            setPageValid(false, error, warning);
+            return;
+        }
+
+        if (mDataPartition.getText().isEmpty() || mDataPartitionSize.getSelectionIndex() < 0) {
+            error = "Invalid Data partition size.";
+            setPageValid(false, error, warning);
+            return;
+        }
+
+        // validate sdcard size or file
+        if (mSdCardSizeRadio.getSelection()) {
+            if (!mSdCardSize.getText().isEmpty() && mSdCardSizeCombo.getSelectionIndex() >= 0) {
+                try {
+                    long sdSize = Long.parseLong(mSdCardSize.getText());
+
+                    int sizeIndex = mSdCardSizeCombo.getSelectionIndex();
+                    if (sizeIndex >= 0) {
+                        // index 0 shifts by 10 (1024=K), index 1 by 20, etc.
+                        sdSize <<= 10 * (1 + sizeIndex);
+                    }
+
+                    if (sdSize < AvdManager.SDCARD_MIN_BYTE_SIZE ||
+                            sdSize > AvdManager.SDCARD_MAX_BYTE_SIZE) {
+                        valid = false;
+                        error = "SD Card size is invalid. Range is 9 MiB..1023 GiB.";
+                    }
+                } catch (NumberFormatException e) {
+                    valid = false;
+                    error = " SD Card size must be a valid integer between 9 MiB and 1023 GiB";
+                }
+            }
+        } else {
+            if (mSdCardFile.getText().isEmpty() || !new File(mSdCardFile.getText()).isFile()) {
+                valid = false;
+                error = "SD Card path isn't valid.";
+            }
+        }
+        if (!valid) {
+            setPageValid(valid, error, warning);
+            return;
+        }
+
+        if (mForceCreation.isEnabled() && !mForceCreation.getSelection()) {
+            valid = false;
+            error = String.format(
+                    "The AVD name '%s' is already used.\n" +
+                            "Check \"Override the existing AVD\" to delete the existing one.",
+                    mAvdName.getText());
+        }
+
+        if (mAvdInfo != null && !mAvdInfo.getName().equals(mAvdName.getText())) {
+            warning = String.format("The AVD '%1$s' will be duplicated into '%2$s'.",
+                    mAvdInfo.getName(),
+                    mAvdName.getText());
+        }
+
+        // On Windows, display a warning if attempting to create AVD's with RAM > 512 MB.
+        // This restriction should go away when we switch to using a 64 bit emulator.
+        if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) {
+            long ramSize = 0;
+            try {
+                ramSize = Long.parseLong(mRam.getText());
+            } catch (NumberFormatException e) {
+                // ignore
+            }
+
+            if (ramSize > 768) {
+                warning = "On Windows, emulating RAM greater than 768M may fail depending on the"
+                        + " system load.\nTry progressively smaller values of RAM if the emulator"
+                        + " fails to launch.";
+            }
+        }
+
+        if (mGpuEmulation.getSelection() && mSnapshot.getSelection()) {
+            valid = false;
+            error = "GPU Emulation and Snapshot cannot be used simultaneously";
+        }
+
+        setPageValid(valid, error, warning);
+        return;
+    }
+
+    private void setPageValid(boolean valid, String error, String warning) {
+        mOkButton.setEnabled(valid);
+        if (error != null) {
+            mStatusIcon.setImage(mImageFactory.getImageByName("reject_icon16.png")); //$NON-NLS-1$
+            mStatusLabel.setText(error);
+        } else if (warning != null) {
+            mStatusIcon.setImage(mImageFactory.getImageByName("warning_icon16.png")); //$NON-NLS-1$
+            mStatusLabel.setText(warning);
+        } else {
+            mStatusIcon.setImage(null);
+            mStatusLabel.setText(" \n "); //$NON-NLS-1$
+        }
+
+        mStatusComposite.pack(true);
+    }
+
+    private boolean createAvd() {
+
+        String avdName = mAvdName.getText();
+        if (avdName == null || avdName.isEmpty()) {
+            return false;
+        }
+
+        String targetName = mTarget.getItem(mTarget.getSelectionIndex());
+        IAndroidTarget target = mCurrentTargets.get(targetName);
+        if (target == null) {
+            return false;
+        }
+
+        // get the abi type
+        String abiType = SdkConstants.ABI_ARMEABI;
+        ISystemImage[] systemImages = getSystemImages(target);
+        if (systemImages.length > 0) {
+            int abiIndex = mAbi.getSelectionIndex();
+            if (abiIndex >= 0) {
+                String prettyname = mAbi.getItem(abiIndex);
+                // Extract the abi type
+                int firstIndex = prettyname.indexOf("(");
+                int lastIndex = prettyname.indexOf(")");
+                abiType = prettyname.substring(firstIndex + 1, lastIndex);
+            }
+        }
+
+        // get the SD card data from the UI.
+        String sdName = null;
+        if (mSdCardSizeRadio.getSelection()) {
+            // size mode
+            String value = mSdCardSize.getText().trim();
+            if (value.length() > 0) {
+                sdName = value;
+                // add the unit
+                switch (mSdCardSizeCombo.getSelectionIndex()) {
+                    case 0:
+                        sdName += "K"; //$NON-NLS-1$
+                        break;
+                    case 1:
+                        sdName += "M"; //$NON-NLS-1$
+                        break;
+                    case 2:
+                        sdName += "G"; //$NON-NLS-1$
+                        break;
+                    default:
+                        // shouldn't be here
+                        assert false;
+                }
+            }
+        } else {
+            // file mode.
+            sdName = mSdCardFile.getText().trim();
+        }
+
+        // Get the device
+        Device device = getSelectedDevice();
+        if (device == null) {
+            return false;
+        }
+
+        Screen s = device.getDefaultHardware().getScreen();
+        String skinName = s.getXDimension() + "x" + s.getYDimension();
+
+        ILogger log = mSdkLog;
+        if (log == null || log instanceof MessageBoxLog) {
+            // If the current logger is a message box, we use our own (to make sure
+            // to display errors right away and customize the title).
+            log = new MessageBoxLog(
+                    String.format("Result of creating AVD '%s':", avdName),
+                    getContents().getDisplay(),
+                    false /* logErrorsOnly */);
+        }
+
+        Map<String, String> hwProps = DeviceManager.getHardwareProperties(device);
+        if (mGpuEmulation.getSelection()) {
+            hwProps.put(AvdManager.AVD_INI_GPU_EMULATION, HardwareProperties.BOOLEAN_YES);
+        }
+
+        File avdFolder = null;
+        try {
+            avdFolder = AvdInfo.getDefaultAvdFolder(mAvdManager, avdName);
+        } catch (AndroidLocationException e) {
+            return false;
+        }
+
+        // Although the device has this information, some devices have more RAM than we'd want to
+        // allocate to an emulator.
+        hwProps.put(AvdManager.AVD_INI_RAM_SIZE, mRam.getText());
+        hwProps.put(AvdManager.AVD_INI_VM_HEAP_SIZE, mVmHeap.getText());
+
+        String suffix;
+        switch (mDataPartitionSize.getSelectionIndex()) {
+            case 0:
+                suffix = "M";
+                break;
+            case 1:
+                suffix = "G";
+                break;
+            default:
+                suffix = "K";
+        }
+        hwProps.put(AvdManager.AVD_INI_DATA_PARTITION_SIZE, mDataPartition.getText()+suffix);
+
+        hwProps.put(HardwareProperties.HW_KEYBOARD,
+                mKeyboard.getSelection() ?
+                        HardwareProperties.BOOLEAN_YES : HardwareProperties.BOOLEAN_NO);
+
+        hwProps.put(AvdManager.AVD_INI_SKIN_DYNAMIC,
+                mSkin.getSelection() ?
+                        HardwareProperties.BOOLEAN_YES : HardwareProperties.BOOLEAN_NO);
+
+        if (mFrontCamera.isEnabled()) {
+            hwProps.put(AvdManager.AVD_INI_CAMERA_FRONT,
+                    mFrontCamera.getText().toLowerCase());
+        }
+
+        if (mBackCamera.isEnabled()) {
+            hwProps.put(AvdManager.AVD_INI_CAMERA_BACK,
+                    mBackCamera.getText().toLowerCase());
+        }
+
+        if (sdName != null) {
+            hwProps.put(HardwareProperties.HW_SDCARD, HardwareProperties.BOOLEAN_YES);
+        }
+
+        AvdInfo avdInfo = mAvdManager.createAvd(avdFolder,
+                avdName,
+                target,
+                abiType,
+                skinName,
+                sdName,
+                hwProps,
+                mSnapshot.getSelection(),
+                mForceCreation.getSelection(),
+                mAvdInfo != null, // edit existing
+                log);
+
+        mCreatedAvd = avdInfo;
+        boolean success = avdInfo != null;
+
+        if (log instanceof MessageBoxLog) {
+            ((MessageBoxLog) log).displayResult(success);
+        }
+        return success;
+    }
+
+    private void fillExistingAvdInfo(AvdInfo avd) {
+        mAvdName.setText(avd.getName());
+        selectDevice(avd.getDeviceManufacturer(), avd.getDeviceName());
+        toggleCameras();
+
+        IAndroidTarget target = avd.getTarget();
+
+        if (target != null && !mCurrentTargets.isEmpty()) {
+            // Try to select the target in the target combo.
+            // This will fail if the AVD needs to be repaired.
+            //
+            // This is a linear search but the list is always
+            // small enough and we only do this once.
+            int n = mTarget.getItemCount();
+            for (int i = 0; i < n; i++) {
+                if (target.equals(mCurrentTargets.get(mTarget.getItem(i)))) {
+                    mTarget.select(i);
+                    reloadAbiTypeCombo();
+                    break;
+                }
+            }
+        }
+
+        ISystemImage[] systemImages = getSystemImages(target);
+        if (target != null && systemImages.length > 0) {
+            mAbi.setEnabled(systemImages.length > 1);
+            String abiType = AvdInfo.getPrettyAbiType(avd.getAbiType());
+            int n = mAbi.getItemCount();
+            for (int i = 0; i < n; i++) {
+                if (abiType.equals(mAbi.getItem(i))) {
+                    mAbi.select(i);
+                    break;
+                }
+            }
+        }
+
+        Map<String, String> props = avd.getProperties();
+
+        if (props != null) {
+            String snapshots = props.get(AvdManager.AVD_INI_SNAPSHOT_PRESENT);
+            if (snapshots != null && snapshots.length() > 0) {
+                mSnapshot.setSelection(snapshots.equals("true"));
+            }
+
+            String gpuEmulation = props.get(AvdManager.AVD_INI_GPU_EMULATION);
+            mGpuEmulation.setSelection(gpuEmulation != null &&
+                    gpuEmulation.equals(HardwareProperties.BOOLEAN_VALUES[0]));
+
+            String sdcard = props.get(AvdManager.AVD_INI_SDCARD_PATH);
+            if (sdcard != null && sdcard.length() > 0) {
+                enableSdCardWidgets(false);
+                mSdCardSizeRadio.setSelection(false);
+                mSdCardFileRadio.setSelection(true);
+                mSdCardFile.setText(sdcard);
+            }
+
+            String ramSize = props.get(AvdManager.AVD_INI_RAM_SIZE);
+            if (ramSize != null) {
+                mRam.setText(ramSize);
+            }
+
+            String vmHeapSize = props.get(AvdManager.AVD_INI_VM_HEAP_SIZE);
+            if (vmHeapSize != null) {
+                mVmHeap.setText(vmHeapSize);
+            }
+
+            String dataPartitionSize = props.get(AvdManager.AVD_INI_DATA_PARTITION_SIZE);
+            if (dataPartitionSize != null) {
+                mDataPartition.setText(
+                        dataPartitionSize.substring(0, dataPartitionSize.length() - 1));
+                switch (dataPartitionSize.charAt(dataPartitionSize.length() - 1)) {
+                    case 'M':
+                        mDataPartitionSize.select(0);
+                        break;
+                    case 'G':
+                        mDataPartitionSize.select(1);
+                        break;
+                    default:
+                        mDataPartitionSize.select(-1);
+                }
+            }
+
+            mKeyboard.setSelection(
+                    HardwareProperties.BOOLEAN_YES.equalsIgnoreCase(
+                            props.get(HardwareProperties.HW_KEYBOARD)));
+            mSkin.setSelection(
+                    HardwareProperties.BOOLEAN_YES.equalsIgnoreCase(
+                            props.get(AvdManager.AVD_INI_SKIN_DYNAMIC)));
+
+            String cameraFront = props.get(AvdManager.AVD_INI_CAMERA_FRONT);
+            if (cameraFront != null) {
+                String[] items = mFrontCamera.getItems();
+                for (int i = 0; i < items.length; i++) {
+                    if (items[i].toLowerCase().equals(cameraFront)) {
+                        mFrontCamera.select(i);
+                        break;
+                    }
+                }
+            }
+
+            String cameraBack = props.get(AvdManager.AVD_INI_CAMERA_BACK);
+            if (cameraBack != null) {
+                String[] items = mBackCamera.getItems();
+                for (int i = 0; i < items.length; i++) {
+                    if (items[i].toLowerCase().equals(cameraBack)) {
+                        mBackCamera.select(i);
+                        break;
+                    }
+                }
+            }
+
+            sdcard = props.get(AvdManager.AVD_INI_SDCARD_SIZE);
+            if (sdcard != null && sdcard.length() > 0) {
+                String[] values = new String[2];
+                long sdcardSize = AvdManager.parseSdcardSize(sdcard, values);
+
+                if (sdcardSize != AvdManager.SDCARD_NOT_SIZE_PATTERN) {
+                    enableSdCardWidgets(true);
+                    mSdCardFileRadio.setSelection(false);
+                    mSdCardSizeRadio.setSelection(true);
+
+                    mSdCardSize.setText(values[0]);
+
+                    String suffix = values[1];
+                    int n = mSdCardSizeCombo.getItemCount();
+                    for (int i = 0; i < n; i++) {
+                        if (mSdCardSizeCombo.getItem(i).startsWith(suffix)) {
+                            mSdCardSizeCombo.select(i);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private void fillInitialDeviceInfo(Device device) {
+        String name = device.getManufacturer();
+        if (!name.equals("Generic") &&      // TODO define & use constants
+                !name.equals("User") &&
+                device.getName().indexOf(name) == -1) {
+            name = " by " + name;
+        } else {
+            name = "";
+        }
+        name = "AVD for " + device.getName() + name;
+        // sanitize the name
+        name = name.replaceAll("[^0-9a-zA-Z_-]+", " ").trim().replaceAll("[ _]+", "_");
+        mAvdName.setText(name);
+
+        // Select the device
+        selectDevice(device);
+        toggleCameras();
+
+        // If there's only one target, select it by default.
+        // TODO: if there are more than 1 target, select the higher platform target as
+        // a likely default.
+        if (mTarget.getItemCount() == 1) {
+            mTarget.select(0);
+            reloadAbiTypeCombo();
+        }
+
+        fillDeviceProperties(device);
+    }
+
+    /**
+     * Returns the list of system images of a target.
+     * <p/>
+     * If target is null, returns an empty list. If target is an add-on with no
+     * system images, return the list from its parent platform.
+     *
+     * @param target An IAndroidTarget. Can be null.
+     * @return A non-null ISystemImage array. Can be empty.
+     */
+    private ISystemImage[] getSystemImages(IAndroidTarget target) {
+        if (target != null) {
+            ISystemImage[] images = target.getSystemImages();
+
+            if ((images == null || images.length == 0) && !target.isPlatform()) {
+                // If an add-on does not provide any system images, use the ones
+                // from the parent.
+                images = target.getParent().getSystemImages();
+            }
+
+            if (images != null) {
+                return images;
+            }
+        }
+
+        return new ISystemImage[0];
+    }
+
+    // Code copied from DeviceMenuListener in ADT; unify post release
+
+    private static final String NEXUS = "Nexus";       //$NON-NLS-1$
+    private static final String GENERIC = "Generic";   //$NON-NLS-1$
+    private static Pattern PATTERN = Pattern.compile(
+            "(\\d+\\.?\\d*)in (.+?)( \\(.*Nexus.*\\))?"); //$NON-NLS-1$
+
+    private static int nexusRank(Device device) {
+        String name = device.getName();
+        if (name.endsWith(" One")) {     //$NON-NLS-1$
+            return 1;
+        }
+        if (name.endsWith(" S")) {       //$NON-NLS-1$
+            return 2;
+        }
+        if (name.startsWith("Galaxy")) { //$NON-NLS-1$
+            return 3;
+        }
+        if (name.endsWith(" 7")) {       //$NON-NLS-1$
+            return 4;
+        }
+        if (name.endsWith(" 10")) {       //$NON-NLS-1$
+            return 5;
+        }
+        if (name.endsWith(" 4")) {       //$NON-NLS-1$
+            return 6;
+        }
+
+        return 7;
+    }
+
+    private static boolean isGeneric(Device device) {
+        return device.getManufacturer().equals(GENERIC);
+    }
+
+    private static boolean isNexus(Device device) {
+        return device.getName().contains(NEXUS);
+    }
+
+    private static String getGenericLabel(Device d) {
+        // * Replace "'in'" with '"' (e.g. 2.7" QVGA instead of 2.7in QVGA)
+        // * Use the same precision for all devices (all but one specify decimals)
+        // * Add some leading space such that the dot ends up roughly in the
+        //   same space
+        // * Add in screen resolution and density
+        String name = d.getName();
+        if (name.equals("3.7 FWVGA slider")) {                        //$NON-NLS-1$
+            // Fix metadata: this one entry doesn't have "in" like the rest of them
+            name = "3.7in FWVGA slider";                              //$NON-NLS-1$
+        }
+
+        Matcher matcher = PATTERN.matcher(name);
+        if (matcher.matches()) {
+            String size = matcher.group(1);
+            String n = matcher.group(2);
+            int dot = size.indexOf('.');
+            if (dot == -1) {
+                size = size + ".0";
+                dot = size.length() - 2;
+            }
+            for (int i = 0; i < 2 - dot; i++) {
+                size = ' ' + size;
+            }
+            name = size + "\" " + n;
+        }
+
+        return String.format(java.util.Locale.US, "%1$s (%2$s)", name,
+                getResolutionString(d));
+    }
+
+    private static String getNexusLabel(Device d) {
+        String name = d.getName();
+        Screen screen = d.getDefaultHardware().getScreen();
+        float length = (float) screen.getDiagonalLength();
+        return String.format(java.util.Locale.US, "%1$s (%3$s\", %2$s)",
+                name, getResolutionString(d), Float.toString(length));
+    }
+
+    @Nullable
+    private static String getResolutionString(Device device) {
+        Screen screen = device.getDefaultHardware().getScreen();
+        return String.format(java.util.Locale.US,
+                "%1$d \u00D7 %2$d: %3$s", // U+00D7: Unicode multiplication sign
+                screen.getXDimension(),
+                screen.getYDimension(),
+                screen.getPixelDensity().getResourceValue());
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdDetailsDialog.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdDetailsDialog.java
new file mode 100644
index 0000000..ce40360
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdDetailsDialog.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.widgets;
+
+import com.android.sdklib.AndroidVersion;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.internal.avd.AvdInfo;
+import com.android.sdklib.internal.avd.AvdManager;
+import com.android.sdklib.internal.avd.AvdInfo.AvdStatus;
+import com.android.sdkuilib.ui.GridDataBuilder;
+import com.android.sdkuilib.ui.GridLayoutBuilder;
+import com.android.sdkuilib.ui.SwtBaseDialog;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Dialog displaying the details of an AVD.
+ */
+final class AvdDetailsDialog extends SwtBaseDialog {
+
+    private final AvdInfo mAvdInfo;
+
+    public AvdDetailsDialog(Shell shell, AvdInfo avdInfo) {
+        super(shell, SWT.APPLICATION_MODAL, "AVD details");
+        mAvdInfo = avdInfo;
+    }
+
+    /**
+     * Create contents of the dialog.
+     */
+    @Override
+    protected void createContents() {
+        Shell shell = getShell();
+        GridLayoutBuilder.create(shell).columns(2);
+        GridDataBuilder.create(shell).fill();
+
+        GridLayout gl;
+
+        Composite c = new Composite(shell, SWT.NONE);
+        c.setLayout(gl = new GridLayout(2, false));
+        gl.marginHeight = gl.marginWidth = 0;
+        c.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        if (mAvdInfo != null) {
+            displayValue(c, "Name:", mAvdInfo.getName());
+            displayValue(c, "CPU/ABI:", AvdInfo.getPrettyAbiType(mAvdInfo.getAbiType()));
+
+            displayValue(c, "Path:", mAvdInfo.getDataFolderPath());
+
+            if (mAvdInfo.getStatus() != AvdStatus.OK) {
+                displayValue(c, "Error:", mAvdInfo.getErrorMessage());
+            } else {
+                IAndroidTarget target = mAvdInfo.getTarget();
+                AndroidVersion version = target.getVersion();
+                displayValue(c, "Target:", String.format("%s (API level %s)",
+                        target.getName(), version.getApiString()));
+
+                // display some extra values.
+                Map<String, String> properties = mAvdInfo.getProperties();
+                if (properties != null) {
+                    String skin = properties.get(AvdManager.AVD_INI_SKIN_NAME);
+                    if (skin != null) {
+                        displayValue(c, "Skin:", skin);
+                    }
+
+                    String sdcard = properties.get(AvdManager.AVD_INI_SDCARD_SIZE);
+                    if (sdcard == null) {
+                        sdcard = properties.get(AvdManager.AVD_INI_SDCARD_PATH);
+                    }
+                    if (sdcard != null) {
+                        displayValue(c, "SD Card:", sdcard);
+                    }
+
+                    String snapshot = properties.get(AvdManager.AVD_INI_SNAPSHOT_PRESENT);
+                    if (snapshot != null) {
+                        displayValue(c, "Snapshot:", snapshot);
+                    }
+
+                    // display other hardware
+                    HashMap<String, String> copy = new HashMap<String, String>(properties);
+                    // remove stuff we already displayed (or that we don't want to display)
+                    copy.remove(AvdManager.AVD_INI_ABI_TYPE);
+                    copy.remove(AvdManager.AVD_INI_CPU_ARCH);
+                    copy.remove(AvdManager.AVD_INI_SKIN_NAME);
+                    copy.remove(AvdManager.AVD_INI_SKIN_PATH);
+                    copy.remove(AvdManager.AVD_INI_SDCARD_SIZE);
+                    copy.remove(AvdManager.AVD_INI_SDCARD_PATH);
+                    copy.remove(AvdManager.AVD_INI_IMAGES_1);
+                    copy.remove(AvdManager.AVD_INI_IMAGES_2);
+
+                    if (copy.size() > 0) {
+                        Label l = new Label(shell, SWT.SEPARATOR | SWT.HORIZONTAL);
+                        l.setLayoutData(new GridData(
+                                GridData.FILL, GridData.CENTER, false, false, 2, 1));
+
+                        c = new Composite(shell, SWT.NONE);
+                        c.setLayout(gl = new GridLayout(2, false));
+                        gl.marginHeight = gl.marginWidth = 0;
+                        c.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+                        for (Map.Entry<String, String> entry : copy.entrySet()) {
+                            displayValue(c, entry.getKey() + ":", entry.getValue());
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    // -- Start of internal part ----------
+    // Hide everything down-below from SWT designer
+    //$hide>>$
+
+
+    @Override
+    protected void postCreate() {
+        // pass
+    }
+
+    /**
+     * Displays a value with a label.
+     *
+     * @param parent the parent Composite in which to display the value. This Composite must use a
+     * {@link GridLayout} with 2 columns.
+     * @param label the label of the value to display.
+     * @param value the string value to display.
+     */
+    private void displayValue(Composite parent, String label, String value) {
+        Label l = new Label(parent, SWT.NONE);
+        l.setText(label);
+        l.setLayoutData(new GridData(GridData.END, GridData.CENTER, false, false));
+
+        l = new Label(parent, SWT.NONE);
+        l.setText(value);
+        l.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, true, false));
+    }
+
+    // End of hiding from SWT Designer
+    //$hide<<$
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdSelector.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdSelector.java
new file mode 100644
index 0000000..0a9d303
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdSelector.java
@@ -0,0 +1,1252 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.widgets;
+
+import com.android.SdkConstants;
+import com.android.annotations.Nullable;
+import com.android.prefs.AndroidLocation.AndroidLocationException;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.devices.DeviceManager;
+import com.android.sdklib.internal.avd.AvdInfo;
+import com.android.sdklib.internal.avd.AvdInfo.AvdStatus;
+import com.android.sdklib.internal.avd.AvdManager;
+import com.android.sdklib.internal.repository.ITask;
+import com.android.sdklib.internal.repository.ITaskMonitor;
+import com.android.sdklib.internal.repository.updater.SettingsController;
+import com.android.sdklib.util.GrabProcessOutput;
+import com.android.sdklib.util.GrabProcessOutput.IProcessOutput;
+import com.android.sdklib.util.GrabProcessOutput.Wait;
+import com.android.sdkuilib.internal.repository.icons.ImageFactory;
+import com.android.sdkuilib.internal.repository.ui.AvdManagerWindowImpl1;
+import com.android.sdkuilib.internal.tasks.ProgressTask;
+import com.android.sdkuilib.repository.AvdManagerWindow.AvdInvocationContext;
+import com.android.sdkuilib.ui.GridDialog;
+import com.android.utils.ILogger;
+import com.android.utils.NullLogger;
+
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.TableItem;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Formatter;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+
+/**
+ * The AVD selector is a table that is added to the given parent composite.
+ * <p/>
+ * After using one of the constructors, call {@link #setSelection(AvdInfo)},
+ * {@link #setSelectionListener(SelectionListener)} and finally use
+ * {@link #getSelected()} to retrieve the selection.
+ */
+public final class AvdSelector {
+    private static int NUM_COL = 2;
+
+    private final DisplayMode mDisplayMode;
+
+    private AvdManager mAvdManager;
+    private final String mOsSdkPath;
+
+    private Table mTable;
+    private Button mDeleteButton;
+    private Button mDetailsButton;
+    private Button mNewButton;
+    private Button mEditButton;
+    private Button mRefreshButton;
+    private Button mManagerButton;
+    private Button mRepairButton;
+    private Button mStartButton;
+
+    private SelectionListener mSelectionListener;
+    private IAvdFilter mTargetFilter;
+
+    /** Defaults to true. Changed by the {@link #setEnabled(boolean)} method to represent the
+     * "global" enabled state on this composite. */
+    private boolean mIsEnabled = true;
+
+    private ImageFactory mImageFactory;
+    private Image mOkImage;
+    private Image mBrokenImage;
+    private Image mInvalidImage;
+
+    private SettingsController mController;
+
+    private final ILogger mSdkLog;
+
+    private boolean mInternalRefresh;
+
+
+    /**
+     * The display mode of the AVD Selector.
+     */
+    public static enum DisplayMode {
+        /**
+         * Manager mode. Invalid AVDs are displayed. Buttons to create/delete AVDs
+         */
+        MANAGER,
+
+        /**
+         * Non manager mode. Only valid AVDs are displayed. Cannot create/delete AVDs, but
+         * there is a button to open the AVD Manager.
+         * In the "check" selection mode, checkboxes are displayed on each line
+         * and {@link AvdSelector#getSelected()} returns the line that is checked
+         * even if it is not the currently selected line. Only one line can
+         * be checked at once.
+         */
+        SIMPLE_CHECK,
+
+        /**
+         * Non manager mode. Only valid AVDs are displayed. Cannot create/delete AVDs, but
+         * there is a button to open the AVD Manager.
+         * In the "select" selection mode, there are no checkboxes and
+         * {@link AvdSelector#getSelected()} returns the line currently selected.
+         * Only one line can be selected at once.
+         */
+        SIMPLE_SELECTION,
+    }
+
+    /**
+     * A filter to control the whether or not an AVD should be displayed by the AVD Selector.
+     */
+    public interface IAvdFilter {
+        /**
+         * Called before {@link #accept(AvdInfo)} is called for any AVD.
+         */
+        void prepare();
+
+        /**
+         * Called to decided whether an AVD should be displayed.
+         * @param avd the AVD to test.
+         * @return true if the AVD should be displayed.
+         */
+        boolean accept(AvdInfo avd);
+
+        /**
+         * Called after {@link #accept(AvdInfo)} has been called on all the AVDs.
+         */
+        void cleanup();
+    }
+
+    /**
+     * Internal implementation of {@link IAvdFilter} to filter out the AVDs that are not
+     * running an image compatible with a specific target.
+     */
+    private final static class TargetBasedFilter implements IAvdFilter {
+        private final IAndroidTarget mTarget;
+
+        TargetBasedFilter(IAndroidTarget target) {
+            mTarget = target;
+        }
+
+        @Override
+        public void prepare() {
+            // nothing to prepare
+        }
+
+        @Override
+        public boolean accept(AvdInfo avd) {
+            if (avd != null) {
+                return mTarget.canRunOn(avd.getTarget());
+            }
+
+            return false;
+        }
+
+        @Override
+        public void cleanup() {
+            // nothing to clean up
+        }
+    }
+
+    /**
+     * Creates a new SDK Target Selector, and fills it with a list of {@link AvdInfo}, filtered
+     * by a {@link IAndroidTarget}.
+     * <p/>Only the {@link AvdInfo} able to run application developed for the given
+     * {@link IAndroidTarget} will be displayed.
+     *
+     * @param parent The parent composite where the selector will be added.
+     * @param osSdkPath The SDK root path. When not null, enables the start button to start
+     *                  an emulator on a given AVD.
+     * @param manager the AVD manager.
+     * @param filter When non-null, will allow filtering the AVDs to display.
+     * @param displayMode The display mode ({@link DisplayMode}).
+     * @param sdkLog The logger. Cannot be null.
+     */
+    public AvdSelector(Composite parent,
+            String osSdkPath,
+            AvdManager manager,
+            IAvdFilter filter,
+            DisplayMode displayMode,
+            ILogger sdkLog) {
+        mOsSdkPath = osSdkPath;
+        mAvdManager = manager;
+        mTargetFilter = filter;
+        mDisplayMode = displayMode;
+        mSdkLog = sdkLog;
+
+        // get some bitmaps.
+        mImageFactory = new ImageFactory(parent.getDisplay());
+        mOkImage = mImageFactory.getImageByName("accept_icon16.png");
+        mBrokenImage = mImageFactory.getImageByName("broken_16.png");
+        mInvalidImage = mImageFactory.getImageByName("reject_icon16.png");
+
+        // Layout has 2 columns
+        Composite group = new Composite(parent, SWT.NONE);
+        GridLayout gl;
+        group.setLayout(gl = new GridLayout(NUM_COL, false /*makeColumnsEqualWidth*/));
+        gl.marginHeight = gl.marginWidth = 0;
+        group.setLayoutData(new GridData(GridData.FILL_BOTH));
+        group.setFont(parent.getFont());
+        group.addDisposeListener(new DisposeListener() {
+            @Override
+            public void widgetDisposed(DisposeEvent arg0) {
+                mImageFactory.dispose();
+            }
+        });
+
+        int style = SWT.FULL_SELECTION | SWT.SINGLE | SWT.BORDER;
+        if (displayMode == DisplayMode.SIMPLE_CHECK) {
+            style |= SWT.CHECK;
+        }
+        mTable = new Table(group, style);
+        mTable.setHeaderVisible(true);
+        mTable.setLinesVisible(false);
+        setTableHeightHint(0);
+
+        Composite buttons = new Composite(group, SWT.NONE);
+        buttons.setLayout(gl = new GridLayout(1, false /*makeColumnsEqualWidth*/));
+        gl.marginHeight = gl.marginWidth = 0;
+        buttons.setLayoutData(new GridData(GridData.FILL_VERTICAL));
+        buttons.setFont(group.getFont());
+
+        if (displayMode == DisplayMode.MANAGER) {
+            mNewButton = new Button(buttons, SWT.PUSH | SWT.FLAT);
+            mNewButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+            mNewButton.setText("New...");
+            mNewButton.setToolTipText("Creates a new AVD.");
+            mNewButton.addSelectionListener(new SelectionAdapter() {
+                @Override
+                public void widgetSelected(SelectionEvent arg0) {
+                    onNew();
+                }
+            });
+
+            mEditButton = new Button(buttons, SWT.PUSH | SWT.FLAT);
+            mEditButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+            mEditButton.setText("Edit...");
+            mEditButton.setToolTipText("Edit an existing AVD.");
+            mEditButton.addSelectionListener(new SelectionAdapter() {
+                @Override
+                public void widgetSelected(SelectionEvent arg0) {
+                    onEdit();
+                }
+            });
+
+            mDeleteButton = new Button(buttons, SWT.PUSH | SWT.FLAT);
+            mDeleteButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+            mDeleteButton.setText("Delete...");
+            mDeleteButton.setToolTipText("Deletes the selected AVD.");
+            mDeleteButton.addSelectionListener(new SelectionAdapter() {
+                @Override
+                public void widgetSelected(SelectionEvent arg0) {
+                    onDelete();
+                }
+            });
+
+            mRepairButton = new Button(buttons, SWT.PUSH | SWT.FLAT);
+            mRepairButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+            mRepairButton.setText("Repair...");
+            mRepairButton.setToolTipText("Repairs the selected AVD.");
+            mRepairButton.addSelectionListener(new SelectionAdapter() {
+                @Override
+                public void widgetSelected(SelectionEvent arg0) {
+                    onRepair();
+                }
+            });
+
+            Label l = new Label(buttons, SWT.SEPARATOR | SWT.HORIZONTAL);
+            l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        }
+
+        mDetailsButton = new Button(buttons, SWT.PUSH | SWT.FLAT);
+        mDetailsButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mDetailsButton.setText("Details...");
+        mDetailsButton.setToolTipText("Displays details of the selected AVD.");
+        mDetailsButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                onDetails();
+            }
+        });
+
+        mStartButton = new Button(buttons, SWT.PUSH | SWT.FLAT);
+        mStartButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mStartButton.setText("Start...");
+        mStartButton.setToolTipText("Starts the selected AVD.");
+        mStartButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                onStart();
+            }
+        });
+
+        Composite padding = new Composite(buttons, SWT.NONE);
+        padding.setLayoutData(new GridData(GridData.FILL_VERTICAL));
+
+        mRefreshButton = new Button(buttons, SWT.PUSH | SWT.FLAT);
+        mRefreshButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mRefreshButton.setText("Refresh");
+        mRefreshButton.setToolTipText("Reloads the list of AVD.\nUse this if you create AVDs from the command line.");
+        mRefreshButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                refresh(true);
+            }
+        });
+
+        if (displayMode != DisplayMode.MANAGER) {
+            mManagerButton = new Button(buttons, SWT.PUSH | SWT.FLAT);
+            mManagerButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+            mManagerButton.setText("Manager...");
+            mManagerButton.setToolTipText("Launches the AVD manager.");
+            mManagerButton.addSelectionListener(new SelectionAdapter() {
+                @Override
+                public void widgetSelected(SelectionEvent e) {
+                    onAvdManager();
+                }
+            });
+        } else {
+            Composite legend = new Composite(group, SWT.NONE);
+            legend.setLayout(gl = new GridLayout(4, false /*makeColumnsEqualWidth*/));
+            gl.marginHeight = gl.marginWidth = 0;
+            legend.setLayoutData(new GridData(GridData.FILL, GridData.BEGINNING, true, false,
+                    NUM_COL, 1));
+            legend.setFont(group.getFont());
+
+            new Label(legend, SWT.NONE).setImage(mOkImage);
+            new Label(legend, SWT.NONE).setText("A valid Android Virtual Device.");
+            new Label(legend, SWT.NONE).setImage(mBrokenImage);
+            new Label(legend, SWT.NONE).setText(
+                    "A repairable Android Virtual Device.");
+            new Label(legend, SWT.NONE).setImage(mInvalidImage);
+            Label l = new Label(legend, SWT.NONE);
+            l.setText("An Android Virtual Device that failed to load. Click 'Details' to see the error.");
+            GridData gd;
+            l.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+            gd.horizontalSpan = 3;
+        }
+
+        // create the table columns
+        final TableColumn column0 = new TableColumn(mTable, SWT.NONE);
+        column0.setText("AVD Name");
+        final TableColumn column1 = new TableColumn(mTable, SWT.NONE);
+        column1.setText("Target Name");
+        final TableColumn column2 = new TableColumn(mTable, SWT.NONE);
+        column2.setText("Platform");
+        final TableColumn column3 = new TableColumn(mTable, SWT.NONE);
+        column3.setText("API Level");
+        final TableColumn column4 = new TableColumn(mTable, SWT.NONE);
+        column4.setText("CPU/ABI");
+
+        adjustColumnsWidth(mTable, column0, column1, column2, column3, column4);
+        setupSelectionListener(mTable);
+        fillTable(mTable);
+        setEnabled(true);
+    }
+
+    /**
+     * Creates a new SDK Target Selector, and fills it with a list of {@link AvdInfo}.
+     *
+     * @param parent The parent composite where the selector will be added.
+     * @param manager the AVD manager.
+     * @param displayMode The display mode ({@link DisplayMode}).
+     * @param sdkLog The logger. Cannot be null.
+     */
+    public AvdSelector(Composite parent,
+            String osSdkPath,
+            AvdManager manager,
+            DisplayMode displayMode,
+            ILogger sdkLog) {
+        this(parent, osSdkPath, manager, (IAvdFilter)null /* filter */, displayMode, sdkLog);
+    }
+
+    /**
+     * Creates a new SDK Target Selector, and fills it with a list of {@link AvdInfo}, filtered
+     * by an {@link IAndroidTarget}.
+     * <p/>Only the {@link AvdInfo} able to run applications developed for the given
+     * {@link IAndroidTarget} will be displayed.
+     *
+     * @param parent The parent composite where the selector will be added.
+     * @param manager the AVD manager.
+     * @param filter Only shows the AVDs matching this target (must not be null).
+     * @param displayMode The display mode ({@link DisplayMode}).
+     * @param sdkLog The logger. Cannot be null.
+     */
+    public AvdSelector(Composite parent,
+            String osSdkPath,
+            AvdManager manager,
+            IAndroidTarget filter,
+            DisplayMode displayMode,
+            ILogger sdkLog) {
+        this(parent, osSdkPath, manager, new TargetBasedFilter(filter), displayMode, sdkLog);
+    }
+
+    /**
+     * Sets an optional SettingsController.
+     * @param controller the controller.
+     */
+    public void setSettingsController(SettingsController controller) {
+        mController = controller;
+    }
+
+    /**
+     * Sets the table grid layout data.
+     *
+     * @param heightHint If > 0, the height hint is set to the requested value.
+     */
+    public void setTableHeightHint(int heightHint) {
+        GridData data = new GridData();
+        if (heightHint > 0) {
+            data.heightHint = heightHint;
+        }
+        data.grabExcessVerticalSpace = true;
+        data.grabExcessHorizontalSpace = true;
+        data.horizontalAlignment = GridData.FILL;
+        data.verticalAlignment = GridData.FILL;
+        mTable.setLayoutData(data);
+    }
+
+    /**
+     * Refresh the display of Android Virtual Devices.
+     * Tries to keep the selection.
+     * <p/>
+     * This must be called from the UI thread.
+     *
+     * @param reload if true, the AVD manager will reload the AVD from the disk.
+     * @return false if the reloading failed. This is always true if <var>reload</var> is
+     * <code>false</code>.
+     */
+    public boolean refresh(boolean reload) {
+        if (!mInternalRefresh) {
+            try {
+                // Note that AvdManagerPage.onDevicesChange() will trigger a
+                // refresh while the AVDs are being reloaded so prevent from
+                // having a recursive call to here.
+                mInternalRefresh = true;
+                if (reload) {
+                    try {
+                        mAvdManager.reloadAvds(NullLogger.getLogger());
+                    } catch (AndroidLocationException e) {
+                        return false;
+                    }
+                }
+
+                AvdInfo selected = getSelected();
+                fillTable(mTable);
+                setSelection(selected);
+                return true;
+            } finally {
+                mInternalRefresh = false;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Sets a new AVD manager
+     * This does not refresh the display. Call {@link #refresh(boolean)} to do so.
+     * @param manager the AVD manager.
+     */
+    public void setManager(AvdManager manager) {
+        mAvdManager = manager;
+    }
+
+    /**
+     * Sets a new AVD filter.
+     * This does not refresh the display. Call {@link #refresh(boolean)} to do so.
+     * @param filter An IAvdFilter. If non-null, this will filter out the AVD to not display.
+     */
+    public void setFilter(IAvdFilter filter) {
+        mTargetFilter = filter;
+    }
+
+    /**
+     * Sets a new Android Target-based AVD filter.
+     * This does not refresh the display. Call {@link #refresh(boolean)} to do so.
+     * @param target An IAndroidTarget. If non-null, only AVD whose target are compatible with the
+     * filter target will displayed an available for selection.
+     */
+    public void setFilter(IAndroidTarget target) {
+        if (target != null) {
+            mTargetFilter = new TargetBasedFilter(target);
+        } else {
+            mTargetFilter = null;
+        }
+    }
+
+    /**
+     * Sets a selection listener. Set it to null to remove it.
+     * The listener will be called <em>after</em> this table processed its selection
+     * events so that the caller can see the updated state.
+     * <p/>
+     * The event's item contains a {@link TableItem}.
+     * The {@link TableItem#getData()} contains an {@link IAndroidTarget}.
+     * <p/>
+     * It is recommended that the caller uses the {@link #getSelected()} method instead.
+     * <p/>
+     * The default behavior for double click (when not in {@link DisplayMode#SIMPLE_CHECK}) is to
+     * display the details of the selected AVD.<br>
+     * To disable it (when you provide your own double click action), set
+     * {@link SelectionEvent#doit} to false in
+     * {@link SelectionListener#widgetDefaultSelected(SelectionEvent)}
+     *
+     * @param selectionListener The new listener or null to remove it.
+     */
+    public void setSelectionListener(SelectionListener selectionListener) {
+        mSelectionListener = selectionListener;
+    }
+
+    /**
+     * Sets the current target selection.
+     * <p/>
+     * If the selection is actually changed, this will invoke the selection listener
+     * (if any) with a null event.
+     *
+     * @param target the target to be selected. Use null to deselect everything.
+     * @return true if the target could be selected, false otherwise.
+     */
+    public boolean setSelection(AvdInfo target) {
+        boolean found = false;
+        boolean modified = false;
+
+        int selIndex = mTable.getSelectionIndex();
+        int index = 0;
+        for (TableItem i : mTable.getItems()) {
+            if (mDisplayMode == DisplayMode.SIMPLE_CHECK) {
+                if ((AvdInfo) i.getData() == target) {
+                    found = true;
+                    if (!i.getChecked()) {
+                        modified = true;
+                        i.setChecked(true);
+                    }
+                } else if (i.getChecked()) {
+                    modified = true;
+                    i.setChecked(false);
+                }
+            } else {
+                if ((AvdInfo) i.getData() == target) {
+                    found = true;
+                    if (index != selIndex) {
+                        mTable.setSelection(index);
+                        modified = true;
+                    }
+                    break;
+                }
+
+                index++;
+            }
+        }
+
+        if (modified && mSelectionListener != null) {
+            mSelectionListener.widgetSelected(null);
+        }
+
+        enableActionButtons();
+
+        return found;
+    }
+
+    /**
+     * Returns the currently selected item. In {@link DisplayMode#SIMPLE_CHECK} mode this will
+     * return the {@link AvdInfo} that is checked instead of the list selection.
+     *
+     * @return The currently selected item or null.
+     */
+    public AvdInfo getSelected() {
+        if (mDisplayMode == DisplayMode.SIMPLE_CHECK) {
+            for (TableItem i : mTable.getItems()) {
+                if (i.getChecked()) {
+                    return (AvdInfo) i.getData();
+                }
+            }
+        } else {
+            int selIndex = mTable.getSelectionIndex();
+            if (selIndex >= 0) {
+                return (AvdInfo) mTable.getItem(selIndex).getData();
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Enables the receiver if the argument is true, and disables it otherwise.
+     * A disabled control is typically not selectable from the user interface
+     * and draws with an inactive or "grayed" look.
+     *
+     * @param enabled the new enabled state.
+     */
+    public void setEnabled(boolean enabled) {
+        // We can only enable widgets if the AVD Manager is defined.
+        mIsEnabled = enabled && mAvdManager != null;
+
+        mTable.setEnabled(mIsEnabled);
+        mRefreshButton.setEnabled(mIsEnabled);
+
+        if (mNewButton != null) {
+            mNewButton.setEnabled(mIsEnabled);
+        }
+        if (mManagerButton != null) {
+            mManagerButton.setEnabled(mIsEnabled);
+        }
+
+        enableActionButtons();
+    }
+
+    public boolean isEnabled() {
+        return mIsEnabled;
+    }
+
+    /**
+     * Adds a listener to adjust the columns width when the parent is resized.
+     * <p/>
+     * If we need something more fancy, we might want to use this:
+     * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet77.java?view=co
+     */
+    private void adjustColumnsWidth(final Table table,
+            final TableColumn column0,
+            final TableColumn column1,
+            final TableColumn column2,
+            final TableColumn column3,
+            final TableColumn column4) {
+        // Add a listener to resize the column to the full width of the table
+        table.addControlListener(new ControlAdapter() {
+            @Override
+            public void controlResized(ControlEvent e) {
+                Rectangle r = table.getClientArea();
+                column0.setWidth(r.width * 20 / 100); // 20%
+                column1.setWidth(r.width * 30 / 100); // 30%
+                column2.setWidth(r.width * 15 / 100); // 15%
+                column3.setWidth(r.width * 15 / 100); // 15%
+                column4.setWidth(r.width * 20 / 100); // 22%
+            }
+        });
+    }
+
+    /**
+     * Creates a selection listener that will check or uncheck the whole line when
+     * double-clicked (aka "the default selection").
+     */
+    private void setupSelectionListener(final Table table) {
+        // Add a selection listener that will check/uncheck items when they are double-clicked
+        table.addSelectionListener(new SelectionListener() {
+
+            /**
+             * Handles single-click selection on the table.
+             * {@inheritDoc}
+             */
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                if (e.item instanceof TableItem) {
+                    TableItem i = (TableItem) e.item;
+                    enforceSingleSelection(i);
+                }
+
+                if (mSelectionListener != null) {
+                    mSelectionListener.widgetSelected(e);
+                }
+
+                enableActionButtons();
+            }
+
+            /**
+             * Handles double-click selection on the table.
+             * Note that the single-click handler will probably already have been called.
+             *
+             * On double-click, <em>always</em> check the table item.
+             *
+             * {@inheritDoc}
+             */
+            @Override
+            public void widgetDefaultSelected(SelectionEvent e) {
+                if (e.item instanceof TableItem) {
+                    TableItem i = (TableItem) e.item;
+                    if (mDisplayMode == DisplayMode.SIMPLE_CHECK) {
+                        i.setChecked(true);
+                    }
+                    enforceSingleSelection(i);
+
+                }
+
+                // whether or not we display details. default: true when not in SIMPLE_CHECK mode.
+                boolean showDetails = mDisplayMode != DisplayMode.SIMPLE_CHECK;
+
+                if (mSelectionListener != null) {
+                    mSelectionListener.widgetDefaultSelected(e);
+                    showDetails &= e.doit; // enforce false in SIMPLE_CHECK
+                }
+
+                if (showDetails) {
+                    onDetails();
+                }
+
+                enableActionButtons();
+            }
+
+            /**
+             * To ensure single selection, uncheck all other items when this one is selected.
+             * This makes the chekboxes act as radio buttons.
+             */
+            private void enforceSingleSelection(TableItem item) {
+                if (mDisplayMode == DisplayMode.SIMPLE_CHECK) {
+                    if (item.getChecked()) {
+                        Table parentTable = item.getParent();
+                        for (TableItem i2 : parentTable.getItems()) {
+                            if (i2 != item && i2.getChecked()) {
+                                i2.setChecked(false);
+                            }
+                        }
+                    }
+                } else {
+                    // pass
+                }
+            }
+        });
+    }
+
+    /**
+     * Fills the table with all AVD.
+     * The table columns are:
+     * <ul>
+     * <li>column 0: sdk name
+     * <li>column 1: sdk vendor
+     * <li>column 2: sdk api name
+     * <li>column 3: sdk version
+     * </ul>
+     */
+    private void fillTable(final Table table) {
+        table.removeAll();
+
+        // get the AVDs
+        AvdInfo avds[] = null;
+        if (mAvdManager != null) {
+            if (mDisplayMode == DisplayMode.MANAGER) {
+                avds = mAvdManager.getAllAvds();
+            } else {
+                avds = mAvdManager.getValidAvds();
+            }
+        }
+
+        if (avds != null && avds.length > 0) {
+            Arrays.sort(avds, new Comparator<AvdInfo>() {
+                @Override
+                public int compare(AvdInfo o1, AvdInfo o2) {
+                    return o1.compareTo(o2);
+                }
+            });
+
+            table.setEnabled(true);
+
+            if (mTargetFilter != null) {
+                mTargetFilter.prepare();
+            }
+
+            for (AvdInfo avd : avds) {
+                if (mTargetFilter == null || mTargetFilter.accept(avd)) {
+                    TableItem item = new TableItem(table, SWT.NONE);
+                    item.setData(avd);
+                    item.setText(0, avd.getName());
+                    if (mDisplayMode == DisplayMode.MANAGER) {
+                        AvdStatus status = avd.getStatus();
+                        item.setImage(0, status == AvdStatus.OK ? mOkImage :
+                            isAvdRepairable(status) ? mBrokenImage : mInvalidImage);
+                    }
+                    IAndroidTarget target = avd.getTarget();
+                    if (target != null) {
+                        item.setText(1, target.getFullName());
+                        item.setText(2, target.getVersionName());
+                        item.setText(3, target.getVersion().getApiString());
+                        item.setText(4, AvdInfo.getPrettyAbiType(avd.getAbiType()));
+                    } else {
+                        item.setText(1, "?");
+                        item.setText(2, "?");
+                        item.setText(3, "?");
+                        item.setText(4, "?");
+                    }
+                }
+            }
+
+            if (mTargetFilter != null) {
+                mTargetFilter.cleanup();
+            }
+        }
+
+        if (table.getItemCount() == 0) {
+            table.setEnabled(false);
+            TableItem item = new TableItem(table, SWT.NONE);
+            item.setData(null);
+            item.setText(0, "--");
+            item.setText(1, "No AVD available");
+            item.setText(2, "--");
+            item.setText(3, "--");
+        }
+    }
+
+    /**
+     * Returns the currently selected AVD in the table.
+     * <p/>
+     * Unlike {@link #getSelected()} this will always return the item being selected
+     * in the list, ignoring the check boxes state in {@link DisplayMode#SIMPLE_CHECK} mode.
+     */
+    private AvdInfo getTableSelection() {
+        int selIndex = mTable.getSelectionIndex();
+        if (selIndex >= 0) {
+            return (AvdInfo) mTable.getItem(selIndex).getData();
+        }
+
+        return null;
+    }
+
+    /**
+     * Updates the enable state of the Details, Start, Delete and Update buttons.
+     */
+    @SuppressWarnings("null")
+    private void enableActionButtons() {
+        if (mIsEnabled == false) {
+            mDetailsButton.setEnabled(false);
+            mStartButton.setEnabled(false);
+
+            if (mEditButton != null) {
+                mEditButton.setEnabled(false);
+            }
+            if (mDeleteButton != null) {
+                mDeleteButton.setEnabled(false);
+            }
+            if (mRepairButton != null) {
+                mRepairButton.setEnabled(false);
+            }
+        } else {
+            AvdInfo selection = getTableSelection();
+            boolean hasSelection = selection != null;
+
+            mDetailsButton.setEnabled(hasSelection);
+            mStartButton.setEnabled(mOsSdkPath != null &&
+                    hasSelection &&
+                    selection.getStatus() == AvdStatus.OK);
+
+            if (mEditButton != null) {
+                mEditButton.setEnabled(hasSelection);
+            }
+            if (mDeleteButton != null) {
+                mDeleteButton.setEnabled(hasSelection);
+            }
+            if (mRepairButton != null) {
+                mRepairButton.setEnabled(hasSelection && isAvdRepairable(selection.getStatus()));
+            }
+        }
+    }
+
+    private void onNew() {
+        AvdCreationDialog dlg = new AvdCreationDialog(mTable.getShell(),
+                mAvdManager,
+                mImageFactory,
+                mSdkLog,
+                null);
+
+        if (dlg.open() == Window.OK) {
+            refresh(false /*reload*/);
+        }
+    }
+
+    private void onEdit() {
+        AvdInfo avdInfo = getTableSelection();
+        GridDialog dlg;
+        if(!avdInfo.getDeviceName().isEmpty()) {
+            dlg = new AvdCreationDialog(mTable.getShell(),
+                    mAvdManager,
+                    mImageFactory,
+                    mSdkLog,
+                    avdInfo);
+        } else {
+            dlg = new LegacyAvdEditDialog(mTable.getShell(),
+                    mAvdManager,
+                    mImageFactory,
+                    mSdkLog,
+                    avdInfo);
+        }
+
+
+        if (dlg.open() == Window.OK) {
+            refresh(false /*reload*/);
+        }
+    }
+
+    private void onDetails() {
+        AvdInfo avdInfo = getTableSelection();
+
+        AvdDetailsDialog dlg = new AvdDetailsDialog(mTable.getShell(), avdInfo);
+        dlg.open();
+    }
+
+    private void onDelete() {
+        final AvdInfo avdInfo = getTableSelection();
+
+        // get the current Display
+        final Display display = mTable.getDisplay();
+
+        // check if the AVD is running
+        if (avdInfo.isRunning()) {
+            display.asyncExec(new Runnable() {
+                @Override
+                public void run() {
+                    Shell shell = display.getActiveShell();
+                    MessageDialog.openError(shell,
+                            "Delete Android Virtual Device",
+                            String.format(
+                                    "The Android Virtual Device '%1$s' is currently running in an emulator and cannot be deleted.",
+                                    avdInfo.getName()));
+                }
+            });
+            return;
+        }
+
+        // Confirm you want to delete this AVD
+        final boolean[] result = new boolean[1];
+        display.syncExec(new Runnable() {
+            @Override
+            public void run() {
+                Shell shell = display.getActiveShell();
+                result[0] = MessageDialog.openQuestion(shell,
+                        "Delete Android Virtual Device",
+                        String.format(
+                                "Please confirm that you want to delete the Android Virtual Device named '%s'. This operation cannot be reverted.",
+                                avdInfo.getName()));
+            }
+        });
+
+        if (result[0] == false) {
+            return;
+        }
+
+        // log for this action.
+        ILogger log = mSdkLog;
+        if (log == null || log instanceof MessageBoxLog) {
+            // If the current logger is a message box, we use our own (to make sure
+            // to display errors right away and customize the title).
+            log = new MessageBoxLog(
+                String.format("Result of deleting AVD '%s':", avdInfo.getName()),
+                display,
+                false /*logErrorsOnly*/);
+        }
+
+        // delete the AVD
+        boolean success = mAvdManager.deleteAvd(avdInfo, log);
+
+        // display the result
+        if (log instanceof MessageBoxLog) {
+            ((MessageBoxLog) log).displayResult(success);
+        }
+
+        if (success) {
+            refresh(false /*reload*/);
+        }
+    }
+
+    /**
+     * Repairs the selected AVD.
+     * <p/>
+     * For now this only supports fixing the wrong value in image.sysdir.*
+     */
+    private void onRepair() {
+        final AvdInfo avdInfo = getTableSelection();
+
+        // get the current Display
+        final Display display = mTable.getDisplay();
+
+        // log for this action.
+        ILogger log = mSdkLog;
+        if (log == null || log instanceof MessageBoxLog) {
+            // If the current logger is a message box, we use our own (to make sure
+            // to display errors right away and customize the title).
+            log = new MessageBoxLog(
+                String.format("Result of updating AVD '%s':", avdInfo.getName()),
+                display,
+                false /*logErrorsOnly*/);
+        }
+
+        boolean success = true;
+
+        if (avdInfo.getStatus() == AvdStatus.ERROR_IMAGE_DIR) {
+            // delete the AVD
+            try {
+                mAvdManager.updateAvd(avdInfo, log);
+                refresh(false /*reload*/);
+            } catch (IOException e) {
+                log.error(e, null);
+                success = false;
+            }
+        } else if (avdInfo.getStatus() == AvdStatus.ERROR_DEVICE_CHANGED) {
+            // Overwrite the properties derived from the device and nothing else
+            Map<String, String> properties = new HashMap<String, String>(avdInfo.getProperties());
+
+            DeviceManager devMan  = DeviceManager.createInstance(mOsSdkPath, mSdkLog);
+            List<Device>  devices = devMan.getDevices(DeviceManager.ALL_DEVICES);
+            String name = properties.get(AvdManager.AVD_INI_DEVICE_NAME);
+            String manufacturer = properties.get(AvdManager.AVD_INI_DEVICE_MANUFACTURER);
+
+            if (properties != null && devices != null && name != null && manufacturer != null) {
+                for (Device d : devices) {
+                    if (d.getName().equals(name) && d.getManufacturer().equals(manufacturer)) {
+                        properties.putAll(DeviceManager.getHardwareProperties(d));
+                        try {
+                            mAvdManager.updateAvd(avdInfo, properties, AvdStatus.OK, log);
+                        } catch (IOException e) {
+                            log.error(e,null);
+                            success = false;
+                        }
+                    }
+                }
+            } else {
+                log.error(null, "Base device information incomplete or missing.");
+                success = false;
+            }
+
+            // display the result
+            if (log instanceof MessageBoxLog) {
+                ((MessageBoxLog) log).displayResult(success);
+            }
+            refresh(false /*reload*/);
+        } else if (avdInfo.getStatus() == AvdStatus.ERROR_DEVICE_MISSING) {
+            onEdit();
+        }
+    }
+
+    private void onAvdManager() {
+
+        // get the current Display
+        Display display = mTable.getDisplay();
+
+        // log for this action.
+        ILogger log = mSdkLog;
+        if (log == null || log instanceof MessageBoxLog) {
+            // If the current logger is a message box, we use our own (to make sure
+            // to display errors right away and customize the title).
+            log = new MessageBoxLog("Result of SDK Manager", display, true /*logErrorsOnly*/);
+        }
+
+        try {
+            AvdManagerWindowImpl1 win = new AvdManagerWindowImpl1(
+                    mTable.getShell(),
+                    log,
+                    mOsSdkPath,
+                    AvdInvocationContext.DIALOG);
+
+            win.open();
+        } catch (Exception ignore) {}
+
+        refresh(true /*reload*/); // UpdaterWindow uses its own AVD manager so this one must reload.
+
+        if (log instanceof MessageBoxLog) {
+            ((MessageBoxLog) log).displayResult(true);
+        }
+    }
+
+    private void onStart() {
+        AvdInfo avdInfo = getTableSelection();
+
+        if (avdInfo == null || mOsSdkPath == null) {
+            return;
+        }
+
+        AvdStartDialog dialog = new AvdStartDialog(mTable.getShell(), avdInfo, mOsSdkPath,
+                mController, mSdkLog);
+        if (dialog.open() == Window.OK) {
+            String path = mOsSdkPath + File.separator
+                    + SdkConstants.OS_SDK_TOOLS_FOLDER
+                    + SdkConstants.FN_EMULATOR;
+
+            final String avdName = avdInfo.getName();
+
+            // build the command line based on the available parameters.
+            ArrayList<String> list = new ArrayList<String>();
+            list.add(path);
+            list.add("-avd");                             //$NON-NLS-1$
+            list.add(avdName);
+            if (dialog.hasWipeData()) {
+                list.add("-wipe-data");                   //$NON-NLS-1$
+            }
+            if (dialog.hasSnapshot()) {
+                if (!dialog.hasSnapshotLaunch()) {
+                    list.add("-no-snapshot-load");
+                }
+                if (!dialog.hasSnapshotSave()) {
+                    list.add("-no-snapshot-save");
+                }
+            }
+            float scale = dialog.getScale();
+            if (scale != 0.f) {
+                // do the rounding ourselves. This is because %.1f will write .4899 as .4
+                scale = Math.round(scale * 100);
+                scale /=  100.f;
+                list.add("-scale");                       //$NON-NLS-1$
+                // because the emulator expects English decimal values, don't use String.format
+                // but a Formatter.
+                Formatter formatter = new Formatter(Locale.US);
+                formatter.format("%.2f", scale);   //$NON-NLS-1$
+                list.add(formatter.toString());
+                formatter.close();
+            }
+
+            // convert the list into an array for the call to exec.
+            final String[] command = list.toArray(new String[list.size()]);
+
+            // launch the emulator
+            final ProgressTask progress = new ProgressTask(mTable.getShell(),
+                                                    "Starting Android Emulator");
+            progress.start(new ITask() {
+                volatile ITaskMonitor mMonitor = null;
+
+                @Override
+                public void run(final ITaskMonitor monitor) {
+                    mMonitor = monitor;
+                    try {
+                        monitor.setDescription(
+                                "Starting emulator for AVD '%1$s'",
+                                avdName);
+                        monitor.log("Starting emulator for AVD '%1$s'", avdName);
+
+                        // we'll wait 100ms*100 = 10s. The emulator sometimes seem to
+                        // start mostly OK just to crash a few seconds later. 10 seconds
+                        // seems a good wait for that case.
+                        int n = 100;
+                        monitor.setProgressMax(n);
+
+                        Process process = Runtime.getRuntime().exec(command);
+                        GrabProcessOutput.grabProcessOutput(
+                                process,
+                                Wait.ASYNC,
+                                new IProcessOutput() {
+                                    @Override
+                                    public void out(@Nullable String line) {
+                                        filterStdOut(line);
+                                    }
+
+                                    @Override
+                                    public void err(@Nullable String line) {
+                                        filterStdErr(line);
+                                    }
+                                });
+
+                        // This small wait prevents the dialog from closing too fast:
+                        // When it works, the emulator returns immediately, even if
+                        // no UI is shown yet. And when it fails (because the AVD is
+                        // locked/running) this allows us to have time to capture the
+                        // error and display it.
+                        for (int i = 0; i < n; i++) {
+                            try {
+                                Thread.sleep(100);
+                                monitor.incProgress(1);
+                            } catch (InterruptedException e) {
+                                // ignore
+                            }
+                        }
+                    } catch (Exception e) {
+                        monitor.logError("Failed to start emulator: %1$s",
+                                e.getMessage());
+                    } finally {
+                        mMonitor = null;
+                    }
+                }
+
+                private void filterStdOut(String line) {
+                    ITaskMonitor m = mMonitor;
+                    if (line == null || m == null) {
+                        return;
+                    }
+
+                    // Skip some non-useful messages.
+                    if (line.indexOf("NSQuickDrawView") != -1) { //$NON-NLS-1$
+                        // Discard the MacOS warning:
+                        // "This application, or a library it uses, is using NSQuickDrawView,
+                        // which has been deprecated. Apps should cease use of QuickDraw and move
+                        // to Quartz."
+                        return;
+                    }
+
+                    if (line.toLowerCase().indexOf("error") != -1 ||                //$NON-NLS-1$
+                            line.indexOf("qemu: fatal") != -1) {                    //$NON-NLS-1$
+                        // Sometimes the emulator seems to output errors on stdout. Catch these.
+                        m.logError("%1$s", line);                                   //$NON-NLS-1$
+                        return;
+                    }
+
+                    m.log("%1$s", line);                                            //$NON-NLS-1$
+                }
+
+                private void filterStdErr(String line) {
+                    ITaskMonitor m = mMonitor;
+                    if (line == null || m == null) {
+                        return;
+                    }
+
+                    if (line.indexOf("emulator: device") != -1 ||                   //$NON-NLS-1$
+                            line.indexOf("HAX is working") != -1) {                 //$NON-NLS-1$
+                        // These are not errors. Output them as regular stdout messages.
+                        m.log("%1$s", line);                                        //$NON-NLS-1$
+                        return;
+                    }
+
+                    m.logError("%1$s", line);                                       //$NON-NLS-1$
+                }
+            });
+        }
+    }
+
+    private boolean isAvdRepairable(AvdStatus avdStatus) {
+        return avdStatus == AvdStatus.ERROR_IMAGE_DIR
+                || avdStatus == AvdStatus.ERROR_DEVICE_CHANGED
+                || avdStatus == AvdStatus.ERROR_DEVICE_MISSING;
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdStartDialog.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdStartDialog.java
new file mode 100644
index 0000000..d796276
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdStartDialog.java
@@ -0,0 +1,642 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.widgets;
+
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.devices.DeviceManager;
+import com.android.sdklib.internal.avd.AvdInfo;
+import com.android.sdklib.internal.avd.AvdManager;
+import com.android.sdklib.internal.repository.updater.SettingsController;
+import com.android.sdkuilib.ui.GridDialog;
+import com.android.utils.ILogger;
+import com.android.utils.SdkUtils;
+
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.VerifyEvent;
+import org.eclipse.swt.events.VerifyListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+import java.awt.Toolkit;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.text.ParseException;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Dialog dealing with emulator launch options. The following options are supported:
+ * <ul>
+ * <li>-wipe-data</li>
+ * <li>-scale</li>
+ * </ul>
+ * Values are stored (in the class as static field) to be reused while the app is still running.
+ * The Monitor dpi is stored in the settings if available.
+ */
+final class AvdStartDialog extends GridDialog {
+    // static field to reuse values during the same session.
+    private static boolean sWipeData = false;
+    private static boolean sSnapshotSave = true;
+    private static boolean sSnapshotLaunch = true;
+    private static int sMonitorDpi = 72; // used if there's no setting controller.
+    private static final Map<String, String> sSkinScaling = new HashMap<String, String>();
+
+    private static final Pattern sScreenSizePattern = Pattern.compile("\\d*(\\.\\d?)?");
+
+    private final AvdInfo mAvd;
+    private final String mSdkLocation;
+    private final SettingsController mSettingsController;
+    private final DeviceManager mDeviceManager;
+
+    private Text mScreenSize;
+    private Text mMonitorDpi;
+    private Button mScaleButton;
+
+    private float mScale = 0.f;
+    private boolean mWipeData = false;
+    private int mDensity = 160; // medium density
+    private int mSize1 = -1;
+    private int mSize2 = -1;
+    private String mSkinDisplay;
+    private boolean mEnableScaling = true;
+    private Label mScaleField;
+    private boolean mHasSnapshot = true;
+    private boolean mSnapshotSave = true;
+    private boolean mSnapshotLaunch = true;
+    private Button mSnapshotLaunchCheckbox;
+
+    AvdStartDialog(Shell parentShell, AvdInfo avd, String sdkLocation,
+            SettingsController settingsController, ILogger sdkLog) {
+        super(parentShell, 2, false);
+        mAvd = avd;
+        mSdkLocation = sdkLocation;
+        mSettingsController = settingsController;
+        mDeviceManager = DeviceManager.createInstance(mSdkLocation, sdkLog);
+        if (mAvd == null) {
+            throw new IllegalArgumentException("avd cannot be null");
+        }
+        if (mSdkLocation == null) {
+            throw new IllegalArgumentException("sdkLocation cannot be null");
+        }
+
+        computeSkinData();
+    }
+
+    public boolean hasWipeData() {
+        return mWipeData;
+    }
+
+    /**
+     * Returns the scaling factor, or 0.f if none are set.
+     */
+    public float getScale() {
+        return mScale;
+    }
+
+    @Override
+    public void createDialogContent(final Composite parent) {
+        GridData gd;
+
+        Label l = new Label(parent, SWT.NONE);
+        l.setText("Skin:");
+
+        l = new Label(parent, SWT.NONE);
+        l.setText(mSkinDisplay == null ? "None" : mSkinDisplay);
+        l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        l = new Label(parent, SWT.NONE);
+        l.setText("Density:");
+
+        l = new Label(parent, SWT.NONE);
+        l.setText(getDensityText());
+        l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        mScaleButton = new Button(parent, SWT.CHECK);
+        mScaleButton.setText("Scale display to real size");
+        mScaleButton.setEnabled(mEnableScaling);
+        boolean defaultState = mEnableScaling && sSkinScaling.get(mAvd.getName()) != null;
+        mScaleButton.setSelection(defaultState);
+        mScaleButton.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+        gd.horizontalSpan = 2;
+        final Group scaleGroup = new Group(parent, SWT.NONE);
+        scaleGroup.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+        gd.horizontalIndent = 30;
+        gd.horizontalSpan = 2;
+        scaleGroup.setLayout(new GridLayout(3, false));
+
+        l = new Label(scaleGroup, SWT.NONE);
+        l.setText("Screen Size (in):");
+        mScreenSize = new Text(scaleGroup, SWT.BORDER);
+        mScreenSize.setText(getScreenSize());
+        mScreenSize.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mScreenSize.addVerifyListener(new VerifyListener() {
+            @Override
+            public void verifyText(VerifyEvent event) {
+                // combine the current content and the new text
+                String text = mScreenSize.getText();
+                text = text.substring(0, event.start) + event.text + text.substring(event.end);
+
+                // now make sure it's a match for the regex
+                event.doit = sScreenSizePattern.matcher(text).matches();
+            }
+        });
+        mScreenSize.addModifyListener(new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent event) {
+                onScaleChange();
+            }
+        });
+
+        // empty composite, only 2 widgets on this line.
+        new Composite(scaleGroup, SWT.NONE).setLayoutData(gd = new GridData());
+        gd.widthHint = gd.heightHint = 0;
+
+        l = new Label(scaleGroup, SWT.NONE);
+        l.setText("Monitor dpi:");
+        mMonitorDpi = new Text(scaleGroup, SWT.BORDER);
+        mMonitorDpi.setText(Integer.toString(getMonitorDpi()));
+        mMonitorDpi.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+        gd.widthHint = 50;
+        mMonitorDpi.addVerifyListener(new VerifyListener() {
+            @Override
+            public void verifyText(VerifyEvent event) {
+                // check for digit only.
+                for (int i = 0 ; i < event.text.length(); i++) {
+                    char letter = event.text.charAt(i);
+                    if (letter < '0' || letter > '9') {
+                        event.doit = false;
+                        return;
+                    }
+                }
+            }
+        });
+        mMonitorDpi.addModifyListener(new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent event) {
+                onScaleChange();
+            }
+        });
+
+        Button button = new Button(scaleGroup, SWT.PUSH | SWT.FLAT);
+        button.setText("?");
+        button.setToolTipText("Click to figure out your monitor's pixel density");
+        button.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                ResolutionChooserDialog dialog = new ResolutionChooserDialog(parent.getShell());
+                if (dialog.open() == Window.OK) {
+                    mMonitorDpi.setText(Integer.toString(dialog.getDensity()));
+                }
+            }
+        });
+
+        l = new Label(scaleGroup, SWT.NONE);
+        l.setText("Scale:");
+        mScaleField = new Label(scaleGroup, SWT.NONE);
+        mScaleField.setLayoutData(new GridData(GridData.FILL, GridData.CENTER,
+                true /*grabExcessHorizontalSpace*/,
+                true /*grabExcessVerticalSpace*/,
+                2 /*horizontalSpan*/,
+                1 /*verticalSpan*/));
+        setScale(mScale); // set initial text value
+
+        enableGroup(scaleGroup, defaultState);
+
+        mScaleButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent event) {
+                boolean enabled = mScaleButton.getSelection();
+                enableGroup(scaleGroup, enabled);
+                if (enabled) {
+                    onScaleChange();
+                } else {
+                    setScale(0);
+                }
+            }
+        });
+
+        final Button wipeButton = new Button(parent, SWT.CHECK);
+        wipeButton.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+        gd.horizontalSpan = 2;
+        wipeButton.setText("Wipe user data");
+        wipeButton.setSelection(mWipeData = sWipeData);
+        wipeButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                mWipeData = wipeButton.getSelection();
+                updateSnapshotLaunchAvailability();
+            }
+        });
+
+        Map<String, String> prop = mAvd.getProperties();
+        String snapshotPresent = prop.get(AvdManager.AVD_INI_SNAPSHOT_PRESENT);
+        mHasSnapshot = (snapshotPresent != null) && snapshotPresent.equals("true");
+
+        mSnapshotLaunchCheckbox = new Button(parent, SWT.CHECK);
+        mSnapshotLaunchCheckbox.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+        gd.horizontalSpan = 2;
+        mSnapshotLaunchCheckbox.setText("Launch from snapshot");
+        updateSnapshotLaunchAvailability();
+        mSnapshotLaunchCheckbox.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                mSnapshotLaunch = mSnapshotLaunchCheckbox.getSelection();
+            }
+        });
+
+        final Button snapshotSaveCheckbox = new Button(parent, SWT.CHECK);
+        snapshotSaveCheckbox.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+        gd.horizontalSpan = 2;
+        snapshotSaveCheckbox.setText("Save to snapshot");
+        snapshotSaveCheckbox.setSelection((mSnapshotSave = sSnapshotSave) && mHasSnapshot);
+        snapshotSaveCheckbox.setEnabled(mHasSnapshot);
+        snapshotSaveCheckbox.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                mSnapshotSave = snapshotSaveCheckbox.getSelection();
+            }
+        });
+
+        l = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL);
+        l.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+        gd.horizontalSpan = 2;
+
+        // if the scaling is enabled by default, we must initialize the value of mScale
+        if (defaultState) {
+            onScaleChange();
+        }
+    }
+
+    /** On Windows we need to manually enable/disable the children of a group */
+    private void enableGroup(final Group group, boolean enabled) {
+        group.setEnabled(enabled);
+        for (Control c : group.getChildren()) {
+            c.setEnabled(enabled);
+        }
+    }
+
+    @Override
+    protected void configureShell(Shell newShell) {
+        super.configureShell(newShell);
+        newShell.setText("Launch Options");
+    }
+
+    @Override
+    protected Button createButton(Composite parent, int id, String label, boolean defaultButton) {
+        if (id == IDialogConstants.OK_ID) {
+            label = "Launch";
+        }
+
+        return super.createButton(parent, id, label, defaultButton);
+    }
+
+    @Override
+    protected void okPressed() {
+        // override ok to store some info
+        // first the monitor dpi
+        String dpi = mMonitorDpi.getText();
+        if (dpi.length() > 0) {
+            sMonitorDpi = Integer.parseInt(dpi);
+
+            // if there is a setting controller, save it
+            if (mSettingsController != null) {
+                mSettingsController.setMonitorDensity(sMonitorDpi);
+                mSettingsController.saveSettings();
+            }
+        }
+
+        // now the scale factor
+        String key = mAvd.getName();
+        sSkinScaling.remove(key);
+        if (mScaleButton.getSelection()) {
+            String size = mScreenSize.getText();
+            if (size.length() > 0) {
+                sSkinScaling.put(key, size);
+            }
+        }
+
+        // and then the wipe-data checkbox
+        sWipeData = mWipeData;
+
+        // and the snapshot handling if those checkboxes are enabled.
+        if (mHasSnapshot) {
+            sSnapshotSave = mSnapshotSave;
+            if (!mWipeData) {
+                sSnapshotLaunch = mSnapshotLaunch;
+            }
+        }
+
+        // finally continue with the ok action
+        super.okPressed();
+    }
+
+    private void computeSkinData() {
+        Map<String, String> prop = mAvd.getProperties();
+        String dpi = prop.get("hw.lcd.density");
+        if (dpi != null && dpi.length() > 0) {
+            mDensity  = Integer.parseInt(dpi);
+        }
+
+        findSkinResolution();
+    }
+
+    private void onScaleChange() {
+        String sizeStr = mScreenSize.getText();
+        if (sizeStr.length() == 0) {
+            setScale(0);
+            return;
+        }
+
+        String dpiStr = mMonitorDpi.getText();
+        if (dpiStr.length() == 0) {
+            setScale(0);
+            return;
+        }
+
+        int dpi = Integer.parseInt(dpiStr);
+
+        // The size number is formatted using String.format (locale formatting)
+        float size;
+        try {
+            size = (float) SdkUtils.parseLocalizedDouble(sizeStr);
+        } catch (ParseException e) {
+            setScale(0);
+            return;
+        }
+
+        /*
+         * We are trying to emulate the following device:
+         * resolution: 'mSize1'x'mSize2'
+         * density: 'mDensity'
+         * screen diagonal: 'size'
+         * ontop a monitor running at 'dpi'
+         */
+        // We start by computing the screen diagonal in pixels, if the density was really mDensity
+        float diagonalPx = (float)Math.sqrt(mSize1*mSize1+mSize2*mSize2);
+        // Now we would convert this in actual inches:
+        //    diagonalIn = diagonal / mDensity
+        // the scale factor is a mix of adapting to the new density and to the new size.
+        //    (size/diagonalIn) * (dpi/mDensity)
+        // this can be simplified to:
+        setScale((size * dpi) / diagonalPx);
+    }
+
+    private void setScale(float scale) {
+        mScale = scale;
+
+        // Do the rounding exactly like AvdSelector will do.
+        scale = Math.round(scale * 100);
+        scale /=  100.f;
+
+        if (scale == 0.f) {
+            mScaleField.setText("default");  //$NON-NLS-1$
+        } else {
+            mScaleField.setText(String.format(Locale.getDefault(), "%.2f", scale));  //$NON-NLS-1$
+        }
+    }
+
+    /**
+     * Returns the monitor dpi to start with.
+     * This can be coming from the settings, the session-based storage, or the from whatever Java
+     * can tell us.
+     */
+    private int getMonitorDpi() {
+        if (mSettingsController != null) {
+            sMonitorDpi = mSettingsController.getSettings().getMonitorDensity();
+        }
+
+        if (sMonitorDpi == -1) { // first time? try to get a value
+            sMonitorDpi = Toolkit.getDefaultToolkit().getScreenResolution();
+        }
+
+        return sMonitorDpi;
+    }
+
+    /**
+     * Returns the screen size to start with.
+     * <p/>If an emulator with the same skin was already launched, scaled, the size used is reused.
+     * <p/>If one hasn't been launched and the AVD is based on a device, use the device's screen
+     * size. Otherwise, use the default (3).
+     */
+    private String getScreenSize() {
+        String size = sSkinScaling.get(mAvd.getName());
+        if (size != null) {
+            return size;
+        }
+
+        Map<String, String> properties = mAvd.getProperties();
+        if (properties != null) {
+            String name = properties.get(AvdManager.AVD_INI_DEVICE_NAME);
+            String mfctr = properties.get(AvdManager.AVD_INI_DEVICE_MANUFACTURER);
+            if (name != null && mfctr != null) {
+                Device d = mDeviceManager.getDevice(name, mfctr);
+                if (d != null) {
+                    double screenSize =
+                        d.getDefaultHardware().getScreen().getDiagonalLength();
+                    return String.format(Locale.getDefault(), "%.1f", screenSize);
+                }
+            }
+        }
+
+        return "3";
+    }
+
+    /**
+     * Returns a display string for the density.
+     */
+    private String getDensityText() {
+        switch (mDensity) {
+            case 120:
+                return "Low (120)";
+            case 160:
+                return "Medium (160)";
+            case 240:
+                return "High (240)";
+        }
+
+        return Integer.toString(mDensity);
+    }
+
+    /**
+     * Finds the skin resolution and sets it in {@link #mSize1} and {@link #mSize2}.
+     */
+    private void findSkinResolution() {
+        Map<String, String> prop = mAvd.getProperties();
+        String skinName = prop.get(AvdManager.AVD_INI_SKIN_NAME);
+
+        if (skinName != null) {
+            Matcher m = AvdManager.NUMERIC_SKIN_SIZE.matcher(skinName);
+            if (m != null && m.matches()) {
+                mSize1 = Integer.parseInt(m.group(1));
+                mSize2 = Integer.parseInt(m.group(2));
+                mSkinDisplay = skinName;
+                mEnableScaling = true;
+                return;
+            }
+        }
+
+        // The resolution is inside the layout file of the skin.
+        mEnableScaling = false; // default to false for now.
+
+        // path to the skin layout file.
+        String skinPath = prop.get(AvdManager.AVD_INI_SKIN_PATH);
+        if (skinPath != null) {
+            File skinFolder = new File(mSdkLocation, skinPath);
+            if (skinFolder.isDirectory()) {
+                File layoutFile = new File(skinFolder, "layout");
+                if (layoutFile.isFile()) {
+                    if (parseLayoutFile(layoutFile)) {
+                        mSkinDisplay = String.format("%1$s (%2$dx%3$d)", skinName, mSize1, mSize2);
+                        mEnableScaling = true;
+                    } else {
+                        mSkinDisplay = skinName;
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Parses a layout file.
+     * <p/>
+     * the format is relatively easy. It's a collection of items defined as
+     * ≶name> {
+     *     ≶content>
+     * }
+     *
+     * content is either 1+ items or 1+ properties
+     * properties are defined as
+     * ≶name>≶whitespace>≶value>
+     *
+     * We're going to look for an item called display, with 2 properties height and width.
+     * This is very basic parser.
+     *
+     * @param layoutFile the file to parse
+     * @return true if both sizes where found.
+     */
+    private boolean parseLayoutFile(File layoutFile) {
+        BufferedReader input = null;
+        try {
+            input = new BufferedReader(new FileReader(layoutFile));
+            String line;
+
+            while ((line = input.readLine()) != null) {
+                // trim to remove whitespace
+                line = line.trim();
+                int len = line.length();
+                if (len == 0) continue;
+
+                // check if this is a new item
+                if (line.charAt(len-1) == '{') {
+                    // this is the start of a node
+                    String[] tokens = line.split(" ");
+                    if ("display".equals(tokens[0])) {
+                        // this is the one we're looking for!
+                        while ((mSize1 == -1 || mSize2 == -1) &&
+                                (line = input.readLine()) != null) {
+                            // trim to remove whitespace
+                            line = line.trim();
+                            len = line.length();
+                            if (len == 0) continue;
+
+                            if ("}".equals(line)) { // looks like we're done with the item.
+                                break;
+                            }
+
+                            tokens = line.split(" ");
+                            if (tokens.length >= 2) {
+                                // there can be multiple space between the name and value
+                                // in which case we'll get an extra empty token in the middle.
+                                if ("width".equals(tokens[0])) {
+                                    mSize1 = Integer.parseInt(tokens[tokens.length-1]);
+                                } else if ("height".equals(tokens[0])) {
+                                    mSize2 = Integer.parseInt(tokens[tokens.length-1]);
+                                }
+                            }
+                        }
+
+                        return mSize1 != -1 && mSize2 != -1;
+                    }
+                }
+
+            }
+            // if it reaches here, display was not found.
+            // false is returned below.
+        } catch (IOException e) {
+            // ignore.
+        } finally {
+            if (input != null) {
+                try {
+                    input.close();
+                } catch (IOException e) {
+                    // ignore
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * @return Whether there's a snapshot file available.
+     */
+    public boolean hasSnapshot() {
+        return mHasSnapshot;
+    }
+
+    /**
+     * @return Whether to launch and load snapshot.
+     */
+    public boolean hasSnapshotLaunch() {
+        return mSnapshotLaunch && !hasWipeData();
+    }
+
+    /**
+     * @return Whether to preserve emulator state to snapshot.
+     */
+    public boolean hasSnapshotSave() {
+        return mSnapshotSave;
+    }
+
+    /**
+     * Updates snapshot launch availability, for when mWipeData value changes.
+     */
+    private void updateSnapshotLaunchAvailability() {
+        boolean enabled = !mWipeData && mHasSnapshot;
+        mSnapshotLaunchCheckbox.setEnabled(enabled);
+        mSnapshotLaunch = enabled && sSnapshotLaunch;
+        mSnapshotLaunchCheckbox.setSelection(mSnapshotLaunch);
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/DeviceCreationDialog.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/DeviceCreationDialog.java
new file mode 100644
index 0000000..68c4fd5
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/DeviceCreationDialog.java
@@ -0,0 +1,1074 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.widgets;
+
+import com.android.annotations.Nullable;
+import com.android.resources.Density;
+import com.android.resources.Keyboard;
+import com.android.resources.KeyboardState;
+import com.android.resources.Navigation;
+import com.android.resources.NavigationState;
+import com.android.resources.ResourceEnum;
+import com.android.resources.ScreenOrientation;
+import com.android.resources.ScreenRatio;
+import com.android.resources.ScreenSize;
+import com.android.resources.TouchScreen;
+import com.android.sdklib.devices.Abi;
+import com.android.sdklib.devices.ButtonType;
+import com.android.sdklib.devices.Camera;
+import com.android.sdklib.devices.CameraLocation;
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.devices.DeviceManager;
+import com.android.sdklib.devices.Hardware;
+import com.android.sdklib.devices.Multitouch;
+import com.android.sdklib.devices.Network;
+import com.android.sdklib.devices.PowerType;
+import com.android.sdklib.devices.Screen;
+import com.android.sdklib.devices.ScreenType;
+import com.android.sdklib.devices.Sensor;
+import com.android.sdklib.devices.Software;
+import com.android.sdklib.devices.State;
+import com.android.sdklib.devices.Storage;
+import com.android.sdkuilib.internal.repository.icons.ImageFactory;
+import com.android.sdkuilib.ui.GridDataBuilder;
+import com.android.sdkuilib.ui.GridDialog;
+import com.android.sdkuilib.ui.GridLayoutBuilder;
+
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.List;
+
+public class DeviceCreationDialog extends GridDialog {
+
+    private static final String MANUFACTURER = "User";
+
+    private final ImageFactory mImageFactory;
+    private final DeviceManager mManager;
+    private List<Device> mUserDevices;
+
+    private Device mDevice;
+
+    private Text mDeviceName;
+    private Text mDiagonalLength;
+    private Text mXDimension;
+    private Text mYDimension;
+    private Button mKeyboard;
+    private Button mDpad;
+    private Button mTrackball;
+    private Button mNoNav;
+    private Text mRam;
+    private Combo mRamCombo;
+    private Combo mButtons;
+    private Combo mSize;
+    private Combo mDensity;
+    private Combo mRatio;
+    private Button mAccelerometer; // hw.accelerometer
+    private Button mGyro; // hw.sensors.orientation
+    private Button mGps; // hw.sensors.gps
+    private Button mProximitySensor; // hw.sensors.proximity
+    private Button mCameraFront;
+    private Button mCameraRear;
+    private Group mStateGroup;
+    private Button mPortrait;
+    private Label mPortraitLabel;
+    private Button mPortraitNav;
+    private Button mLandscape;
+    private Label mLandscapeLabel;
+    private Button mLandscapeNav;
+    private Button mPortraitKeys;
+    private Label mPortraitKeysLabel;
+    private Button mPortraitKeysNav;
+    private Button mLandscapeKeys;
+    private Label mLandscapeKeysLabel;
+    private Button mLandscapeKeysNav;
+
+    private Button mForceCreation;
+    private Label mStatusIcon;
+    private Label mStatusLabel;
+
+    private Button mOkButton;
+
+    /** The hardware instance attached to each of the states of the created device. */
+    private Hardware mHardware;
+    /** The instance of the Device created by the dialog, if the user pressed {@code mOkButton}. */
+    private Device mCreatedDevice;
+
+    /**
+     * This contains the Software for the device. Since it has no effect on the
+     * emulator whatsoever, we just use a single instance with reasonable
+     * defaults. */
+    private static final Software mSoftware;
+
+    static {
+        mSoftware = new Software();
+        mSoftware.setLiveWallpaperSupport(true);
+        mSoftware.setGlVersion("2.0");
+    }
+
+    public DeviceCreationDialog(Shell parentShell,
+            DeviceManager manager,
+            ImageFactory imageFactory,
+            @Nullable Device device) {
+        super(parentShell, 3, false);
+        mImageFactory = imageFactory;
+        mDevice = device;
+        mManager = manager;
+        mUserDevices = mManager.getDevices(DeviceManager.USER_DEVICES);
+    }
+
+    /**
+     * Returns the instance of the Device created by the dialog,
+     * if the user pressed the OK|create|edit|clone button.
+     * Typically only non-null if the dialog returns OK.
+     */
+    public Device getCreatedDevice() {
+        return mCreatedDevice;
+    }
+
+    @Override
+    protected Control createContents(Composite parent) {
+        Control control = super.createContents(parent);
+
+        mOkButton = getButton(IDialogConstants.OK_ID);
+
+        if (mDevice == null) {
+            getShell().setText("Create New Device");
+        } else {
+            if (mUserDevices.contains(mDevice)) {
+                getShell().setText("Edit Device");
+            } else {
+                getShell().setText("Clone Device");
+            }
+        }
+
+        Object ld = mOkButton.getLayoutData();
+        if (ld instanceof GridData) {
+            ((GridData) ld).widthHint = 100;
+        }
+
+        validatePage();
+
+        return control;
+    }
+
+    @Override
+    public void createDialogContent(Composite parent) {
+
+        ValidationListener validator = new ValidationListener();
+        SizeListener sizeListener    = new SizeListener();
+        NavStateListener navListener = new NavStateListener();
+
+        Composite column1 = new Composite(parent, SWT.NONE);
+        GridDataBuilder.create(column1).hFill().vTop();
+        GridLayoutBuilder.create(column1).columns(2);
+
+        // vertical separator between column 1 and 2
+        Label label = new Label(parent, SWT.SEPARATOR | SWT.VERTICAL);
+        GridDataBuilder.create(label).vFill().vGrab();
+
+        Composite column2 = new Composite(parent, SWT.NONE);
+        GridDataBuilder.create(column2).hFill().vTop();
+        GridLayoutBuilder.create(column2).columns(2);
+
+        // Column 1
+
+        String tooltip = "Name of the new device";
+        generateLabel("Name:", tooltip, column1);
+        mDeviceName = generateText(column1, tooltip, new CreateNameModifyListener());
+
+        tooltip = "Diagonal length of the screen in inches";
+        generateLabel("Screen Size (in):", tooltip, column1);
+        mDiagonalLength = generateText(column1, tooltip, sizeListener);
+
+        tooltip = "The resolution of the device in pixels";
+        generateLabel("Resolution (px):", tooltip, column1);
+        Composite dimensionGroup = new Composite(column1, SWT.NONE); // Like a Group with no border
+        GridDataBuilder.create(dimensionGroup).hFill();
+        GridLayoutBuilder.create(dimensionGroup).columns(3).noMargins();
+        mXDimension = generateText(dimensionGroup, tooltip, sizeListener);
+        new Label(dimensionGroup, SWT.NONE).setText("x");
+        mYDimension = generateText(dimensionGroup, tooltip, sizeListener);
+
+        label = new Label(column1, SWT.None);   // empty space holder
+        GridDataBuilder.create(label).hFill().hGrab().hSpan(2);
+
+        // Column 2
+
+        tooltip = "The screen size bucket that the device falls into";
+        generateLabel("Size:", tooltip, column2);
+        mSize = generateCombo(column2, tooltip, ScreenSize.values(), 1, validator);
+
+        tooltip = "The aspect ratio bucket the screen falls into. A \"long\" screen is wider.";
+        generateLabel("Screen Ratio:", tooltip, column2);
+        mRatio = generateCombo(column2, tooltip, ScreenRatio.values(), 1, validator);
+
+        tooltip = "The pixel density bucket the device falls in";
+        generateLabel("Density:", tooltip, column2);
+        mDensity = generateCombo(column2, tooltip, Density.values(), 3, validator);
+
+        label = new Label(column2, SWT.None);   // empty space holder
+        GridDataBuilder.create(label).hFill().hGrab().hSpan(2);
+
+
+        // Column 1, second row
+
+        generateLabel("Sensors:", "The sensors available on the device", column1);
+        Group sensorGroup = new Group(column1, SWT.NONE);
+        sensorGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        sensorGroup.setLayout(new GridLayout(2, false));
+        mAccelerometer = generateButton(sensorGroup, "Accelerometer",
+                "Presence of an accelerometer", SWT.CHECK, true, validator);
+        mGyro = generateButton(sensorGroup, "Gyroscope",
+                "Presence of a gyroscope", SWT.CHECK, true, validator);
+        mGps = generateButton(sensorGroup, "GPS", "Presence of a GPS", SWT.CHECK, true, validator);
+        mProximitySensor = generateButton(sensorGroup, "Proximity Sensor",
+                "Presence of a proximity sensor", SWT.CHECK, true, validator);
+
+        generateLabel("Cameras", "The cameras available on the device", column1);
+        Group cameraGroup = new Group(column1, SWT.NONE);
+        cameraGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        cameraGroup.setLayout(new GridLayout(2, false));
+        mCameraFront = generateButton(cameraGroup, "Front", "Presence of a front camera",
+                SWT.CHECK, false, validator);
+        mCameraRear = generateButton(cameraGroup, "Rear", "Presence of a rear camera",
+                SWT.CHECK, true, validator);
+
+        generateLabel("Input:", "The input hardware on the given device", column1);
+        Group inputGroup = new Group(column1, SWT.NONE);
+        inputGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        inputGroup.setLayout(new GridLayout(3, false));
+        mKeyboard = generateButton(inputGroup, "Keyboard", "Presence of a hardware keyboard",
+                SWT.CHECK, false,
+                new KeyboardListener());
+        GridData gridData = new GridData(GridData.FILL_HORIZONTAL);
+        gridData.horizontalSpan = 3;
+        mKeyboard.setLayoutData(gridData);
+        mNoNav = generateButton(inputGroup, "No Nav", "No hardware navigation",
+                SWT.RADIO, true, navListener);
+        mDpad = generateButton(inputGroup, "DPad", "The device has a DPad navigation element",
+                SWT.RADIO, false, navListener);
+        mTrackball = generateButton(inputGroup, "Trackball",
+                "The device has a trackball navigation element", SWT.RADIO, false, navListener);
+
+        tooltip = "The amount of RAM on the device";
+        generateLabel("RAM:", tooltip, column1);
+        Group ramGroup = new Group(column1, SWT.NONE);
+        ramGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        ramGroup.setLayout(new GridLayout(2, false));
+        mRam = generateText(ramGroup, tooltip, validator);
+        mRamCombo = new Combo(ramGroup, SWT.DROP_DOWN | SWT.READ_ONLY);
+        mRamCombo.setToolTipText(tooltip);
+        mRamCombo.add("MiB");
+        mRamCombo.add("GiB");
+        mRamCombo.select(0);
+        mRamCombo.addModifyListener(validator);
+
+        // Column 2, second row
+
+        tooltip = "Type of buttons (Home, Menu, etc.) on the device. "
+                + "This can be software buttons like on the Galaxy Nexus, or hardware buttons like "
+                + "the capacitive buttons on the Nexus S.";
+        generateLabel("Buttons:", tooltip, column2);
+        mButtons = new Combo(column2, SWT.DROP_DOWN | SWT.READ_ONLY);
+        mButtons.setToolTipText(tooltip);
+        mButtons.add(ButtonType.SOFT.getDescription());
+        mButtons.add(ButtonType.HARD.getDescription());
+        mButtons.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mButtons.select(0);
+        mButtons.addModifyListener(validator);
+
+        generateLabel("Device States:", "The available states for the given device", column2);
+
+        mStateGroup = new Group(column2, SWT.NONE);
+        mStateGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mStateGroup.setLayout(new GridLayout(2, true));
+
+        tooltip = "The device has a portait position with no keyboard available";
+        mPortraitLabel = generateLabel("Portrait:", tooltip, mStateGroup);
+        gridData = new GridData(GridData.FILL_HORIZONTAL);
+        gridData.horizontalSpan = 2;
+        mPortraitLabel.setLayoutData(gridData);
+        mPortrait = generateButton(mStateGroup, "Enabled", tooltip, SWT.CHECK, true,
+                navListener);
+        mPortraitNav = generateButton(mStateGroup, "Navigation",
+                "Hardware navigation is available in this state", SWT.CHECK, true, validator);
+        mPortraitNav.setEnabled(false);
+
+        tooltip = "The device has a landscape position with no keyboard available";
+        mLandscapeLabel = generateLabel("Landscape:", tooltip, mStateGroup);
+        gridData = new GridData(GridData.FILL_HORIZONTAL);
+        gridData.horizontalSpan = 2;
+        mLandscapeLabel.setLayoutData(gridData);
+        mLandscape = generateButton(mStateGroup, "Enabled", tooltip, SWT.CHECK, true,
+                navListener);
+        mLandscapeNav = generateButton(mStateGroup, "Navigation",
+                "Hardware navigation is available in this state", SWT.CHECK, true, validator);
+        mLandscapeNav.setEnabled(false);
+
+        tooltip = "The device has a portait position with a keyboard available";
+        mPortraitKeysLabel = generateLabel("Portrait with keyboard:", tooltip, mStateGroup);
+        gridData = new GridData(GridData.FILL_HORIZONTAL);
+        gridData.horizontalSpan = 2;
+        mPortraitKeysLabel.setLayoutData(gridData);
+        mPortraitKeysLabel.setEnabled(false);
+        mPortraitKeys = generateButton(mStateGroup, "Enabled", tooltip, SWT.CHECK, true,
+                navListener);
+        mPortraitKeys.setEnabled(false);
+        mPortraitKeysNav = generateButton(mStateGroup, "Navigation",
+                "Hardware navigation is available in this state", SWT.CHECK, true, validator);
+        mPortraitKeysNav.setEnabled(false);
+
+        tooltip = "The device has a landscape position with the keyboard open";
+        mLandscapeKeysLabel = generateLabel("Landscape with keyboard:", tooltip, mStateGroup);
+        gridData = new GridData(GridData.FILL_HORIZONTAL);
+        gridData.horizontalSpan = 2;
+        mLandscapeKeysLabel.setLayoutData(gridData);
+        mLandscapeKeysLabel.setEnabled(false);
+        mLandscapeKeys = generateButton(mStateGroup, "Enabled", tooltip, SWT.CHECK, true,
+                navListener);
+        mLandscapeKeys.setEnabled(false);
+        mLandscapeKeysNav = generateButton(mStateGroup, "Navigation",
+                "Hardware navigation is available in this state", SWT.CHECK, true, validator);
+        mLandscapeKeysNav.setEnabled(false);
+
+
+        mForceCreation = new Button(column2, SWT.CHECK);
+        mForceCreation.setText("Override the existing device with the same name");
+        mForceCreation.setToolTipText("There's already an AVD with the same name. Check this to delete it and replace it by the new AVD.");
+        mForceCreation.setLayoutData(new GridData(GridData.BEGINNING, GridData.CENTER,
+                true, false, 2, 1));
+        mForceCreation.setEnabled(false);
+        mForceCreation.addSelectionListener(validator);
+
+
+        // -- third row
+
+        // add a separator to separate from the ok/cancel button
+        label = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL);
+        label.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, true, false, 3, 1));
+
+        // add stuff for the error display
+        Composite statusComposite = new Composite(parent, SWT.NONE);
+        GridLayout gl;
+        statusComposite.setLayoutData(
+                new GridData(GridData.FILL, GridData.CENTER, true, false, 3, 1));
+        statusComposite.setLayout(gl = new GridLayout(2, false));
+        gl.marginHeight = gl.marginWidth = 0;
+
+        mStatusIcon = new Label(statusComposite, SWT.NONE);
+        mStatusIcon.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING,
+                false, false));
+        mStatusLabel = new Label(statusComposite, SWT.NONE);
+        mStatusLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mStatusLabel.setText(""); //$NON-NLS-1$
+
+        prefillWithDevice(mDevice);
+
+        validatePage();
+    }
+
+    private Button generateButton(Composite parent, String text, String tooltip, int type,
+            boolean selected, SelectionListener listener) {
+        Button b = new Button(parent, type);
+        b.setText(text);
+        b.setToolTipText(tooltip);
+        b.setSelection(selected);
+        b.addSelectionListener(listener);
+        b.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        return b;
+    }
+
+    /**
+     * Generates a combo widget attached to the given parent, then sets the
+     * tooltip, adds all of the {@link String}s returned by
+     * {@link ResourceEnum#getResourceValue()} for each {@link ResourceEnum},
+     * sets the combo to the given index and adds the given
+     * {@link ModifyListener}.
+     */
+    private Combo generateCombo(Composite parent, String tooltip, ResourceEnum[] values,
+            int selection,
+            ModifyListener validator) {
+        Combo c = new Combo(parent, SWT.DROP_DOWN | SWT.READ_ONLY);
+        c.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        c.setToolTipText(tooltip);
+        for (ResourceEnum r : values) {
+            c.add(r.getResourceValue());
+        }
+        c.select(selection);
+        c.addModifyListener(validator);
+        return c;
+    }
+
+    /** Generates a text widget with the given tooltip, parent and listener */
+    private Text generateText(Composite parent, String tooltip, ModifyListener listener) {
+        Text t = new Text(parent, SWT.BORDER);
+        t.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        t.setToolTipText(tooltip);
+        t.addModifyListener(listener);
+        return t;
+    }
+
+    /** Generates a label and attaches it to the given parent */
+    private Label generateLabel(String text, String tooltip, Composite parent) {
+        Label label = new Label(parent, SWT.NONE);
+        label.setText(text);
+        label.setToolTipText(tooltip);
+        label.setLayoutData(new GridData(GridData.VERTICAL_ALIGN_CENTER));
+        return label;
+    }
+
+    /**
+     * Callback when the device name is changed. Enforces that device names
+     * don't conflict with already existing devices unless we're editing that
+     * device.
+     */
+    private class CreateNameModifyListener implements ModifyListener {
+        @Override
+        public void modifyText(ModifyEvent e) {
+            String name = mDeviceName.getText();
+            boolean nameCollision = false;
+            for (Device d : mUserDevices) {
+                if (MANUFACTURER.equals(d.getManufacturer()) && name.equals(d.getName())) {
+                    nameCollision = true;
+                    break;
+                }
+            }
+            mForceCreation.setEnabled(nameCollision);
+            mForceCreation.setSelection(!nameCollision);
+
+            validatePage();
+        }
+    }
+
+    /**
+     * Callback attached to the diagonal length and resolution text boxes. Sets
+     * the screen size and display density based on their values, then validates
+     * the page.
+     */
+    private class SizeListener implements ModifyListener {
+        @Override
+        public void modifyText(ModifyEvent e) {
+
+            if (!mDiagonalLength.getText().isEmpty()) {
+                try {
+                    double diagonal = Double.parseDouble(mDiagonalLength.getText());
+                    double diagonalDp = 160.0 * diagonal;
+
+                    // Set the Screen Size
+                    if (diagonalDp >= 1200) {
+                        mSize.select(ScreenSize.getIndex(ScreenSize.getEnum("xlarge")));
+                    } else if (diagonalDp >= 800) {
+                        mSize.select(ScreenSize.getIndex(ScreenSize.getEnum("large")));
+                    } else if (diagonalDp >= 568) {
+                        mSize.select(ScreenSize.getIndex(ScreenSize.getEnum("normal")));
+                    } else {
+                        mSize.select(ScreenSize.getIndex(ScreenSize.getEnum("small")));
+                    }
+                    if (!mXDimension.getText().isEmpty() && !mYDimension.getText().isEmpty()) {
+
+                        // Set the density based on which bucket it's closest to
+                        double x = Double.parseDouble(mXDimension.getText());
+                        double y = Double.parseDouble(mYDimension.getText());
+                        double dpi = Math.sqrt(x * x + y * y) / diagonal;
+                        double difference = Double.MAX_VALUE;
+                        Density bucket = Density.MEDIUM;
+                        for (Density d : Density.values()) {
+                            if (Math.abs(d.getDpiValue() - dpi) < difference) {
+                                difference = Math.abs(d.getDpiValue() - dpi);
+                                bucket = d;
+                            }
+                        }
+                        mDensity.select(Density.getIndex(bucket));
+                    }
+                } catch (NumberFormatException ignore) {}
+            }
+        }
+    }
+
+
+    /**
+     * Callback attached to the keyboard checkbox.Enables / disables device
+     * states based on the keyboard presence and then validates the page.
+     */
+    private class KeyboardListener extends SelectionAdapter {
+        @Override
+        public void widgetSelected(SelectionEvent event) {
+            super.widgetSelected(event);
+            if (mKeyboard.getSelection()) {
+                mPortraitKeys.setEnabled(true);
+                mPortraitKeysLabel.setEnabled(true);
+                mLandscapeKeys.setEnabled(true);
+                mLandscapeKeysLabel.setEnabled(true);
+            } else {
+                mPortraitKeys.setEnabled(false);
+                mPortraitKeysLabel.setEnabled(false);
+                mLandscapeKeys.setEnabled(false);
+                mLandscapeKeysLabel.setEnabled(false);
+            }
+            toggleNav();
+            validatePage();
+        }
+
+    }
+
+    /**
+     * Listens for changes on widgets that affect nav availability and toggles
+     * the nav checkboxes for device states based on them.
+     */
+    private class NavStateListener extends SelectionAdapter {
+        @Override
+        public void widgetSelected(SelectionEvent event) {
+            super.widgetSelected(event);
+            toggleNav();
+            validatePage();
+        }
+    }
+
+    /**
+     * Method that inspects all of the relevant dialog state and enables or disables the nav
+     * elements accordingly.
+     */
+    private void toggleNav() {
+        mPortraitNav.setEnabled(mPortrait.getSelection() && !mNoNav.getSelection());
+        mLandscapeNav.setEnabled(mLandscape.getSelection() && !mNoNav.getSelection());
+        mPortraitKeysNav.setEnabled(mPortraitKeys.getSelection() && mPortraitKeys.getEnabled()
+                && !mNoNav.getSelection());
+        mLandscapeKeysNav.setEnabled(mLandscapeKeys.getSelection()
+                && mLandscapeKeys.getEnabled() && !mNoNav.getSelection());
+        validatePage();
+    }
+
+    /**
+     * Callback that validates the page on modification events or widget
+     * selections
+     */
+    private class ValidationListener extends SelectionAdapter implements ModifyListener {
+        @Override
+        public void modifyText(ModifyEvent e) {
+            validatePage();
+        }
+
+        @Override
+        public void widgetSelected(SelectionEvent e) {
+            super.widgetSelected(e);
+            validatePage();
+        }
+    }
+
+    /**
+     * Validates all of the config options to ensure a valid device can be
+     * created from them.
+     *
+     * @return Whether the config options will result in a valid device.
+     */
+    private boolean validatePage() {
+        boolean valid = true;
+        String error = null;
+        String warning = null;
+        setError(null);
+
+        String name = mDeviceName.getText();
+
+        /* If we're editing / cloning a device, this will get called when the name gets pre-filled
+         * but the ok button won't be populated yet, so we need to skip the initial setting.
+         */
+        if (mOkButton != null) {
+            if (mDevice == null) {
+                getShell().setText("Create New Device");
+                mOkButton.setText("Create Device");
+            } else {
+                if (mDevice.getName().equals(name)){
+                    if (mUserDevices.contains(mDevice)) {
+                        getShell().setText("Edit Device");
+                        mOkButton.setText("Edit Device");
+                    } else {
+                        warning = "Only user created devices are editable.\nA clone of it will be created under " +
+                        "the \"User\" category.";
+                        getShell().setText("Clone Device");
+                        mOkButton.setText("Clone Device");
+                    }
+                } else {
+                    warning = "The device \"" + mDevice.getName() +"\" will be duplicated into\n" +
+                            "\"" + name + "\" under the \"User\" category";
+                    getShell().setText("Clone Device");
+                    mOkButton.setText("Clone Device");
+                }
+            }
+        }
+
+        if (valid && name.isEmpty()) {
+            warning = "Please enter a name for the device.";
+            valid = false;
+        }
+        if (valid && !validateFloat("Diagonal Length", mDiagonalLength.getText())) {
+            warning = "Please enter a screen size.";
+            valid = false;
+        }
+        if (valid && !validateInt("Resolution", mXDimension.getText())) {
+            warning = "Please enter the screen resolution.";
+            valid = false;
+        }
+        if (valid && !validateInt("Resolution", mYDimension.getText())) {
+            warning = "Please enter the screen resolution.";
+            valid = false;
+        }
+        if (valid && mSize.getSelectionIndex() < 0) {
+            error = "A size bucket must be selected.";
+            valid = false;
+        }
+        if (valid && mDensity.getSelectionIndex() < 0) {
+            error = "A screen density bucket must be selected";
+            valid = false;
+        }
+        if (valid && mRatio.getSelectionIndex() < 0) {
+            error = "A screen ratio must be selected.";
+            valid = false;
+        }
+        if (valid && !mNoNav.getSelection() && !mTrackball.getSelection() && !mDpad.getSelection()) {
+            error = "A mode of hardware navigation, or no navigation, has to be selected.";
+            valid = false;
+        }
+        if (valid && !validateInt("RAM", mRam.getText())) {
+            warning = "Please enter a RAM amount.";
+            valid = false;
+        }
+        if (valid && mRamCombo.getSelectionIndex() < 0) {
+            error = "RAM must have a selected unit.";
+            valid = false;
+        }
+        if (valid && mButtons.getSelectionIndex() < 0) {
+            error = "A button type must be selected.";
+            valid = false;
+        }
+        if (valid) {
+            if (mKeyboard.getSelection()) {
+                if (!mPortraitKeys.getSelection()
+                        && !mPortrait.getSelection()
+                        && !mLandscapeKeys.getSelection()
+                        && !mLandscape.getSelection()) {
+                    error = "At least one device state must be enabled.";
+                    valid = false;
+                }
+            } else {
+                if (!mPortrait.getSelection() && !mLandscape.getSelection()) {
+                    error = "At least one device state must be enabled";
+                    valid = false;
+                }
+            }
+        }
+        if (mForceCreation.isEnabled() && !mForceCreation.getSelection()) {
+            error = "Name conflicts with an existing device.";
+            valid = false;
+        }
+
+        if (mOkButton != null) {
+            mOkButton.setEnabled(valid);
+        }
+
+        if (error != null) {
+            setError(error);
+        } else if (warning != null) {
+            setWarning(warning);
+        }
+
+        return valid;
+    }
+
+    /**
+     * Validates the string is a valid, positive float. If not, it sets the
+     * error at the bottom of the dialog and returns false. Note this does
+     * <b>not</b> unset the error message, it's up to the caller to unset it iff
+     * it knows there are no errors on the page.
+     */
+    private boolean validateFloat(String box, String value) {
+        if (value == null || value.isEmpty()) {
+            return false;
+        }
+        boolean ret = true;
+        try {
+            double val = Double.parseDouble(value);
+            if (val <= 0) {
+                ret = false;
+            }
+        } catch (NumberFormatException e) {
+            ret = false;
+        }
+        if (!ret) {
+            setError(box + " must be a valid, positive number.");
+        }
+        return ret;
+    }
+
+    /**
+     * Validates the string is a valid, positive integer. If not, it sets the
+     * error at the bottom of the dialog and returns false. Note this does
+     * <b>not</b> unset the error message, it's up to the caller to unset it iff
+     * it knows there are no errors on the page.
+     */
+    private boolean validateInt(String box, String value) {
+        if (value == null || value.isEmpty()) {
+            return false;
+        }
+        boolean ret = true;
+        try {
+            int val = Integer.parseInt(value);
+            if (val <= 0) {
+                ret = false;
+            }
+        } catch (NumberFormatException e) {
+            ret = false;
+        }
+
+        if (!ret) {
+            setError(box + " must be a valid, positive integer.");
+        }
+
+        return ret;
+    }
+
+    /**
+     * Sets the error to the given string. If null, removes the error message.
+     */
+    private void setError(@Nullable String error) {
+        if (error == null) {
+            mStatusIcon.setImage(null);
+            mStatusLabel.setText("");
+        } else {
+            mStatusIcon.setImage(mImageFactory.getImageByName("reject_icon16.png"));
+            mStatusLabel.setText(error);
+        }
+    }
+
+    /**
+     * Sets the warning message to the given string. If null, removes the
+     * warning message.
+     */
+    private void setWarning(@Nullable String warning) {
+        if (warning == null) {
+            mStatusIcon.setImage(null);
+            mStatusLabel.setText("");
+        } else {
+            mStatusIcon.setImage(mImageFactory.getImageByName("warning_icon16.png"));
+            mStatusLabel.setText(warning);
+        }
+    }
+
+    /** Sets the hardware for the new device */
+    private void prefillWithDevice(@Nullable Device device) {
+        if (device == null) {
+
+            // Setup the default hardware instance with reasonable values for
+            // the things which are configurable via this dialog.
+            mHardware = new Hardware();
+
+            Screen s = new Screen();
+            s.setXdpi(316);
+            s.setYdpi(316);
+            s.setMultitouch(Multitouch.JAZZ_HANDS);
+            s.setMechanism(TouchScreen.FINGER);
+            s.setScreenType(ScreenType.CAPACITIVE);
+            mHardware.setScreen(s);
+
+            mHardware.addNetwork(Network.BLUETOOTH);
+            mHardware.addNetwork(Network.WIFI);
+            mHardware.addNetwork(Network.NFC);
+
+            mHardware.addSensor(Sensor.BAROMETER);
+            mHardware.addSensor(Sensor.COMPASS);
+            mHardware.addSensor(Sensor.LIGHT_SENSOR);
+
+            mHardware.setHasMic(true);
+            mHardware.addInternalStorage(new Storage(4, Storage.Unit.GiB));
+            mHardware.setCpu("Generic CPU");
+            mHardware.setGpu("Generic GPU");
+
+            mHardware.addSupportedAbi(Abi.ARMEABI);
+            mHardware.addSupportedAbi(Abi.ARMEABI_V7A);
+            mHardware.addSupportedAbi(Abi.MIPS);
+            mHardware.addSupportedAbi(Abi.X86);
+
+            mHardware.setChargeType(PowerType.BATTERY);
+            return;
+        }
+        mHardware = device.getDefaultHardware().deepCopy();
+        mDeviceName.setText(device.getName());
+        mForceCreation.setSelection(true);
+        Screen s = mHardware.getScreen();
+        mDiagonalLength.setText(Double.toString(s.getDiagonalLength()));
+        mXDimension.setText(Integer.toString(s.getXDimension()));
+        mYDimension.setText(Integer.toString(s.getYDimension()));
+        String size = s.getSize().getResourceValue();
+        for (int i = 0; i < mSize.getItemCount(); i++) {
+            if (size.equals(mSize.getItem(i))) {
+                mSize.select(i);
+                break;
+            }
+        }
+        String ratio = s.getRatio().getResourceValue();
+        for (int i = 0; i < mRatio.getItemCount(); i++) {
+            if (ratio.equals(mRatio.getItem(i))) {
+                mRatio.select(i);
+                break;
+            }
+        }
+        String density = s.getPixelDensity().getResourceValue();
+        for (int i = 0; i < mDensity.getItemCount(); i++) {
+            if (density.equals(mDensity.getItem(i))) {
+                mDensity.select(i);
+                break;
+            }
+        }
+        mKeyboard.setSelection(!Keyboard.NOKEY.equals(mHardware.getKeyboard()));
+        mDpad.setSelection(Navigation.DPAD.equals(mHardware.getNav()));
+        mTrackball.setSelection(Navigation.TRACKBALL.equals(mHardware.getNav()));
+        mNoNav.setSelection(Navigation.NONAV.equals(mHardware.getNav()));
+        mAccelerometer.setSelection(mHardware.getSensors().contains(Sensor.ACCELEROMETER));
+        mGyro.setSelection(mHardware.getSensors().contains(Sensor.GYROSCOPE));
+        mGps.setSelection(mHardware.getSensors().contains(Sensor.GPS));
+        mProximitySensor.setSelection(mHardware.getSensors().contains(Sensor.PROXIMITY_SENSOR));
+        mCameraFront.setSelection(false);
+        mCameraRear.setSelection(false);
+        for (Camera c : mHardware.getCameras()) {
+            if (CameraLocation.FRONT.equals(c.getLocation())) {
+                mCameraFront.setSelection(true);
+            } else if (CameraLocation.BACK.equals(c.getLocation())) {
+                mCameraRear.setSelection(true);
+            }
+        }
+        mRam.setText(Long.toString(mHardware.getRam().getSizeAsUnit(Storage.Unit.MiB)));
+        mRamCombo.select(0);
+
+        for (int i = 0; i < mButtons.getItemCount(); i++) {
+            if (mButtons.getItem(i).equals(mHardware.getButtonType().getDescription())) {
+                mButtons.select(i);
+                break;
+            }
+        }
+
+        for (State state : device.getAllStates()) {
+            Button nav = null;
+            if (state.getOrientation().equals(ScreenOrientation.PORTRAIT)) {
+                if (state.getKeyState().equals(KeyboardState.EXPOSED)) {
+                    mPortraitKeys.setSelection(true);
+                    nav = mPortraitKeysNav;
+                } else {
+                    mPortrait.setSelection(true);
+                    nav = mPortraitNav;
+                }
+            } else {
+                if (state.getKeyState().equals(KeyboardState.EXPOSED)) {
+                    mLandscapeKeys.setSelection(true);
+                    nav = mLandscapeKeysNav;
+                } else {
+                    mLandscape.setSelection(true);
+                    nav = mLandscapeNav;
+                }
+            }
+            nav.setSelection(state.getNavState().equals(NavigationState.EXPOSED)
+                    && !mHardware.getNav().equals(Navigation.NONAV));
+        }
+    }
+
+    /**
+     * If given a valid page, generates the corresponding device. The device is
+     * then added to the user device list, replacing any previous device with
+     * its given name and manufacturer, and the list is saved out to disk.
+     */
+    @Override
+    protected void okPressed() {
+        if (validatePage()) {
+            Device.Builder builder = new Device.Builder();
+            builder.setManufacturer("User");
+            builder.setName(mDeviceName.getText());
+            builder.addSoftware(mSoftware);
+            Screen s = mHardware.getScreen();
+            double diagonal = Double.parseDouble(mDiagonalLength.getText());
+            int x = Integer.parseInt(mXDimension.getText());
+            int y = Integer.parseInt(mYDimension.getText());
+            s.setDiagonalLength(diagonal);
+            s.setXDimension(x);
+            s.setYDimension(y);
+            // The diagonal DPI will be somewhere in between the X and Y dpi if
+            // they differ
+            double dpi = Math.sqrt(x * x + y * y) / diagonal;
+            s.setXdpi(dpi);
+            s.setYdpi(dpi);
+            s.setPixelDensity(Density.getEnum(mDensity.getText()));
+            s.setSize(ScreenSize.getEnum(mSize.getText()));
+            s.setRatio(ScreenRatio.getEnum(mRatio.getText()));
+            if (mAccelerometer.getSelection()) {
+                mHardware.addSensor(Sensor.ACCELEROMETER);
+            }
+            if (mGyro.getSelection()) {
+                mHardware.addSensor(Sensor.GYROSCOPE);
+            }
+            if (mGps.getSelection()) {
+                mHardware.addSensor(Sensor.GPS);
+            }
+            if (mProximitySensor.getSelection()) {
+                mHardware.addSensor(Sensor.PROXIMITY_SENSOR);
+            }
+            if (mCameraFront.getSelection()) {
+                Camera c = new Camera();
+                c.setAutofocus(true);
+                c.setFlash(true);
+                c.setLocation(CameraLocation.FRONT);
+                mHardware.addCamera(c);
+            }
+            if (mCameraRear.getSelection()) {
+                Camera c = new Camera();
+                c.setAutofocus(true);
+                c.setFlash(true);
+                c.setLocation(CameraLocation.BACK);
+                mHardware.addCamera(c);
+            }
+            if (mKeyboard.getSelection()) {
+                mHardware.setKeyboard(Keyboard.QWERTY);
+            } else {
+                mHardware.setKeyboard(Keyboard.NOKEY);
+            }
+            if (mDpad.getSelection()) {
+                mHardware.setNav(Navigation.DPAD);
+            } else if (mTrackball.getSelection()) {
+                mHardware.setNav(Navigation.TRACKBALL);
+            } else {
+                mHardware.setNav(Navigation.NONAV);
+            }
+            long ram = Long.parseLong(mRam.getText());
+            Storage.Unit unit = Storage.Unit.getEnum(mRamCombo.getText());
+            mHardware.setRam(new Storage(ram, unit));
+            if (mButtons.getText().equals(ButtonType.HARD.getDescription())) {
+                mHardware.setButtonType(ButtonType.HARD);
+            } else {
+                mHardware.setButtonType(ButtonType.SOFT);
+            }
+
+            // Set the first enabled state to the default state
+            boolean defaultSelected = false;
+            if (mPortrait.getSelection()) {
+                State state = new State();
+                state.setName("Portrait");
+                state.setDescription("The device in portrait orientation");
+                state.setOrientation(ScreenOrientation.PORTRAIT);
+                if (mHardware.getNav().equals(Navigation.NONAV) || !mPortraitNav.getSelection()) {
+                    state.setNavState(NavigationState.HIDDEN);
+                } else {
+                    state.setNavState(NavigationState.EXPOSED);
+                }
+                if (mHardware.getKeyboard().equals(Keyboard.NOKEY)) {
+                    state.setKeyState(KeyboardState.SOFT);
+                } else {
+                    state.setKeyState(KeyboardState.HIDDEN);
+                }
+                state.setHardware(mHardware);
+                if (!defaultSelected) {
+                    state.setDefaultState(true);
+                    defaultSelected = true;
+                }
+                builder.addState(state);
+            }
+            if (mLandscape.getSelection()) {
+                State state = new State();
+                state.setName("Landscape");
+                state.setDescription("The device in landscape orientation");
+                state.setOrientation(ScreenOrientation.LANDSCAPE);
+                if (mHardware.getNav().equals(Navigation.NONAV) || !mLandscapeNav.getSelection()) {
+                    state.setNavState(NavigationState.HIDDEN);
+                } else {
+                    state.setNavState(NavigationState.EXPOSED);
+                }
+                if (mHardware.getKeyboard().equals(Keyboard.NOKEY)) {
+                    state.setKeyState(KeyboardState.SOFT);
+                } else {
+                    state.setKeyState(KeyboardState.HIDDEN);
+                }
+                state.setHardware(mHardware);
+                if (!defaultSelected) {
+                    state.setDefaultState(true);
+                    defaultSelected = true;
+                }
+                builder.addState(state);
+            }
+            if (mKeyboard.getSelection()) {
+                if (mPortraitKeys.getSelection()) {
+                    State state = new State();
+                    state.setName("Portrait with keyboard");
+                    state.setDescription("The device in portrait orientation with a keyboard open");
+                    state.setOrientation(ScreenOrientation.LANDSCAPE);
+                    if (mHardware.getNav().equals(Navigation.NONAV)
+                            || !mPortraitKeysNav.getSelection()) {
+                        state.setNavState(NavigationState.HIDDEN);
+                    } else {
+                        state.setNavState(NavigationState.EXPOSED);
+                    }
+                    state.setKeyState(KeyboardState.EXPOSED);
+                    state.setHardware(mHardware);
+                    if (!defaultSelected) {
+                        state.setDefaultState(true);
+                        defaultSelected = true;
+                    }
+                    builder.addState(state);
+                }
+                if (mLandscapeKeys.getSelection()) {
+                    State state = new State();
+                    state.setName("Landscape with keyboard");
+                    state.setDescription("The device in landscape orientation with a keyboard open");
+                    state.setOrientation(ScreenOrientation.LANDSCAPE);
+                    if (mHardware.getNav().equals(Navigation.NONAV)
+                            || !mLandscapeKeysNav.getSelection()) {
+                        state.setNavState(NavigationState.HIDDEN);
+                    } else {
+                        state.setNavState(NavigationState.EXPOSED);
+                    }
+                    state.setKeyState(KeyboardState.EXPOSED);
+                    state.setHardware(mHardware);
+                    if (!defaultSelected) {
+                        state.setDefaultState(true);
+                        defaultSelected = true;
+                    }
+                    builder.addState(state);
+                }
+            }
+            Device d = builder.build();
+            if (mForceCreation.isEnabled() && mForceCreation.getSelection()) {
+                mManager.replaceUserDevice(d);
+            } else {
+                mManager.addUserDevice(d);
+            }
+            mManager.saveUserDevices();
+            mCreatedDevice = d;
+            super.okPressed();
+        }
+    }
+
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/HardwarePropertyChooser.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/HardwarePropertyChooser.java
new file mode 100644
index 0000000..a07768c
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/HardwarePropertyChooser.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.widgets;
+
+import com.android.sdklib.internal.avd.HardwareProperties.HardwareProperty;
+import com.android.sdklib.internal.avd.HardwareProperties.HardwarePropertyType;
+import com.android.sdkuilib.ui.GridDialog;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * Dialog to choose a hardware property
+ */
+class HardwarePropertyChooser extends GridDialog {
+
+    private final Map<String, HardwareProperty> mProperties;
+    private final Collection<String> mExceptProperties;
+    private HardwareProperty mChosenProperty;
+    private Label mTypeLabel;
+    private Label mDescriptionLabel;
+
+    HardwarePropertyChooser(Shell parentShell,
+            Map<String, HardwareProperty> properties,
+            Collection<String> exceptProperties) {
+        super(parentShell, 2, false);
+        mProperties = properties;
+        mExceptProperties = exceptProperties;
+    }
+
+    public HardwareProperty getProperty() {
+        return mChosenProperty;
+    }
+
+    @Override
+    public void createDialogContent(Composite parent) {
+        Label l = new Label(parent, SWT.NONE);
+        l.setText("Property:");
+
+        final Combo c = new Combo(parent, SWT.DROP_DOWN | SWT.READ_ONLY);
+        // simple list for index->name resolution.
+        final ArrayList<String> indexToName = new ArrayList<String>();
+
+        // Sort the combo entries by display name if available, otherwise by hardware name.
+        Set<Entry<String, HardwareProperty>> entries =
+            new TreeSet<Map.Entry<String,HardwareProperty>>(
+                    new Comparator<Map.Entry<String,HardwareProperty>>() {
+                @Override
+                public int compare(Entry<String, HardwareProperty> entry0,
+                                   Entry<String, HardwareProperty> entry1) {
+                    String s0 = entry0.getValue().getAbstract();
+                    String s1 = entry1.getValue().getAbstract();
+                    if (s0 != null && s1 != null) {
+                        return s0.compareTo(s1);
+                    }
+                    return entry0.getKey().compareTo(entry1.getKey());
+                }
+            });
+        entries.addAll(mProperties.entrySet());
+
+        for (Entry<String, HardwareProperty> entry : entries) {
+            if (entry.getValue().isValidForUi() &&
+                    mExceptProperties.contains(entry.getKey()) == false) {
+                c.add(entry.getValue().getAbstract());
+                indexToName.add(entry.getKey());
+            }
+        }
+        boolean hasValues = true;
+        if (indexToName.size() == 0) {
+            hasValues = false;
+            c.add("No properties");
+            c.select(0);
+            c.setEnabled(false);
+        }
+
+        c.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent event) {
+                int index = c.getSelectionIndex();
+                String name = indexToName.get(index);
+                processSelection(name, true /* pack */);
+            }
+        });
+
+        l = new Label(parent, SWT.NONE);
+        l.setText("Type:");
+
+        mTypeLabel = new Label(parent, SWT.NONE);
+
+        l = new Label(parent, SWT.NONE);
+        l.setText("Description:");
+
+        mDescriptionLabel = new Label(parent, SWT.NONE);
+
+        if (hasValues) {
+            c.select(0);
+            processSelection(indexToName.get(0), false /* pack */);
+        }
+    }
+
+    private void processSelection(String name, boolean pack) {
+        mChosenProperty = name == null ? null : mProperties.get(name);
+
+        String type = "Unknown";
+        String desc = "Unknown";
+
+        if (mChosenProperty != null) {
+            desc = mChosenProperty.getDescription();
+            HardwarePropertyType vt = mChosenProperty.getType();
+            if (vt != null) {
+                type = vt.getName();
+            }
+        }
+
+        mTypeLabel.setText(type);
+        mDescriptionLabel.setText(desc);
+
+        if (pack) {
+            getShell().pack();
+        }
+    }
+
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ImgDisabledButton.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ImgDisabledButton.java
new file mode 100755
index 0000000..62973a4
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ImgDisabledButton.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.widgets;
+
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * A label that can display 2 images depending on its enabled/disabled state.
+ * This acts as a button by firing the {@link SWT#Selection} listener.
+ */
+public class ImgDisabledButton extends ToggleButton {
+    public ImgDisabledButton(
+            Composite parent,
+            int style,
+            Image imageEnabled,
+            Image imageDisabled,
+            String tooltipEnabled,
+            String tooltipDisabled) {
+        super(parent,
+                style,
+                imageEnabled,
+                imageDisabled,
+                tooltipEnabled,
+                tooltipDisabled);
+    }
+
+    @Override
+    public void setEnabled(boolean enabled) {
+        super.setEnabled(enabled);
+        updateImageAndTooltip();
+        redraw();
+    }
+
+    @Override
+    public void setState(int state) {
+        throw new UnsupportedOperationException(); // not available for this type of button
+    }
+
+    @Override
+    public int getState() {
+        return (isDisposed() || !isEnabled()) ? 1 : 0;
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/LegacyAvdEditDialog.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/LegacyAvdEditDialog.java
new file mode 100644
index 0000000..91f45c8
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/LegacyAvdEditDialog.java
@@ -0,0 +1,1425 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.widgets;
+
+import com.android.SdkConstants;
+import com.android.io.FileWrapper;
+import com.android.prefs.AndroidLocation.AndroidLocationException;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.ISystemImage;
+import com.android.sdklib.SdkManager;
+import com.android.sdklib.internal.avd.AvdInfo;
+import com.android.sdklib.internal.avd.AvdManager;
+import com.android.sdklib.internal.avd.AvdManager.AvdConflict;
+import com.android.sdklib.internal.avd.HardwareProperties;
+import com.android.sdklib.internal.avd.HardwareProperties.HardwareProperty;
+import com.android.sdklib.internal.project.ProjectProperties;
+import com.android.sdkuilib.internal.repository.icons.ImageFactory;
+import com.android.sdkuilib.ui.GridDialog;
+import com.android.utils.ILogger;
+import com.android.utils.Pair;
+
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.viewers.CellEditor;
+import org.eclipse.jface.viewers.CellLabelProvider;
+import org.eclipse.jface.viewers.ComboBoxCellEditor;
+import org.eclipse.jface.viewers.EditingSupport;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.TextCellEditor;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerCell;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.VerifyEvent;
+import org.eclipse.swt.events.VerifyListener;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.Text;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+import java.util.regex.Matcher;
+
+/**
+ * AVD creation or edit dialog.
+ *
+ * TODO:
+ * - use SdkTargetSelector instead of Combo
+ * - tooltips on widgets.
+ *
+ */
+final class LegacyAvdEditDialog extends GridDialog {
+
+    private final AvdManager mAvdManager;
+    private final TreeMap<String, IAndroidTarget> mCurrentTargets =
+        new TreeMap<String, IAndroidTarget>();
+
+    private final Map<String, HardwareProperty> mHardwareMap;
+    private final Map<String, String> mProperties = new HashMap<String, String>();
+    // a list of user-edited properties.
+    private final ArrayList<String> mEditedProperties = new ArrayList<String>();
+    private final ImageFactory mImageFactory;
+    private final ILogger mSdkLog;
+    /**
+     * The original AvdInfo if we're editing an existing AVD.
+     * Null when we're creating a new AVD.
+     */
+    private final AvdInfo mEditAvdInfo;
+
+    private Text mAvdName;
+    private Combo mTargetCombo;
+
+    private Combo mAbiTypeCombo;
+    private String mAbiType;
+
+    private Button mSdCardSizeRadio;
+    private Text mSdCardSize;
+    private Combo mSdCardSizeCombo;
+
+    private Text mSdCardFile;
+    private Button mBrowseSdCard;
+    private Button mSdCardFileRadio;
+
+    private Button mSnapshotCheck;
+
+    private Button mSkinListRadio;
+    private Combo mSkinCombo;
+
+    private Button mSkinSizeRadio;
+    private Text mSkinSizeWidth;
+    private Text mSkinSizeHeight;
+
+    private TableViewer mHardwareViewer;
+    private Button mDeleteHardwareProp;
+
+    private Button mForceCreation;
+    private Button mOkButton;
+    private Label mStatusIcon;
+    private Label mStatusLabel;
+    private Composite mStatusComposite;
+
+    /**
+     * {@link VerifyListener} for {@link Text} widgets that should only contains numbers.
+     */
+    private final VerifyListener mDigitVerifier = new VerifyListener() {
+        @Override
+        public void verifyText(VerifyEvent event) {
+            int count = event.text.length();
+            for (int i = 0 ; i < count ; i++) {
+                char c = event.text.charAt(i);
+                if (c < '0' || c > '9') {
+                    event.doit = false;
+                    return;
+                }
+            }
+        }
+    };
+
+    /**
+     * Callback when the AVD name is changed.
+     * When creating a new AVD, enables the force checkbox if the name is a duplicate.
+     * When editing an existing AVD, it's OK for the name to match the existing AVD.
+     */
+    private class CreateNameModifyListener implements ModifyListener {
+        @Override
+        public void modifyText(ModifyEvent e) {
+            String name = mAvdName.getText().trim();
+            if (mEditAvdInfo == null || !name.equals(mEditAvdInfo.getName())) {
+                // Case where we're creating a new AVD or editing an existing one
+                // and the AVD name has been changed... check for name uniqueness.
+
+                Pair<AvdConflict, String> conflict = mAvdManager.isAvdNameConflicting(name);
+                if (conflict.getFirst() != AvdManager.AvdConflict.NO_CONFLICT) {
+                    // If we're changing the state from disabled to enabled, make sure
+                    // to uncheck the button, to force the user to voluntarily re-enforce it.
+                    // This happens when editing an existing AVD and changing the name from
+                    // the existing AVD to another different existing AVD.
+                    if (!mForceCreation.isEnabled()) {
+                        mForceCreation.setEnabled(true);
+                        mForceCreation.setSelection(false);
+                    }
+                } else {
+                    mForceCreation.setEnabled(false);
+                    mForceCreation.setSelection(false);
+                }
+            } else {
+                // Case where we're editing an existing AVD with the name unchanged.
+
+                mForceCreation.setEnabled(false);
+                mForceCreation.setSelection(false);
+            }
+            validatePage();
+        }
+    }
+
+    /**
+     * {@link ModifyListener} used for live-validation of the fields content.
+     */
+    private class ValidateListener extends SelectionAdapter implements ModifyListener {
+        @Override
+        public void modifyText(ModifyEvent e) {
+            validatePage();
+        }
+
+        @Override
+        public void widgetSelected(SelectionEvent e) {
+            super.widgetSelected(e);
+            validatePage();
+        }
+    }
+
+    /**
+     * Creates the dialog. Caller should then use {@link Window#open()} and
+     * refresh if the status is {@link Window#OK}.
+     *
+     * @param parentShell The parent shell.
+     * @param avdManager The existing {@link AvdManager} to use. Must not be null.
+     * @param imageFactory An existing {@link ImageFactory} to use. Must not be null.
+     * @param log An existing {@link ILogger} where output will go. Must not be null.
+     * @param editAvdInfo An optional {@link AvdInfo}. When null, the dialog is used
+     *   to create a new AVD. When non-null, the dialog is used to <em>edit</em> this AVD.
+     */
+    protected LegacyAvdEditDialog(Shell parentShell,
+            AvdManager avdManager,
+            ImageFactory imageFactory,
+            ILogger log,
+            AvdInfo editAvdInfo) {
+        super(parentShell, 2, false);
+        mAvdManager = avdManager;
+        mImageFactory = imageFactory;
+        mSdkLog = log;
+        mEditAvdInfo = editAvdInfo;
+
+        File hardwareDefs = null;
+
+        SdkManager sdkMan = avdManager.getSdkManager();
+        if (sdkMan != null) {
+            String sdkPath = sdkMan.getLocation();
+            if (sdkPath != null) {
+                hardwareDefs = new File (sdkPath + File.separator +
+                        SdkConstants.OS_SDK_TOOLS_LIB_FOLDER, SdkConstants.FN_HARDWARE_INI);
+            }
+        }
+
+        if (hardwareDefs == null) {
+            log.error(null, "Failed to load file %s from SDK", SdkConstants.FN_HARDWARE_INI);
+            mHardwareMap = new HashMap<String, HardwareProperty>();
+        } else {
+            mHardwareMap = HardwareProperties.parseHardwareDefinitions(
+                hardwareDefs, null /*sdkLog*/);
+        }
+    }
+
+    @Override
+    public void create() {
+        super.create();
+
+        Point p = getShell().getSize();
+        if (p.x < 400) {
+            p.x = 400;
+        }
+        getShell().setSize(p);
+    }
+
+    @Override
+    protected Control createContents(Composite parent) {
+        Control control = super.createContents(parent);
+        getShell().setText(mEditAvdInfo == null ? "Create new Android Virtual Device (AVD)"
+                                                : "Edit Android Virtual Device (AVD)");
+
+        mOkButton = getButton(IDialogConstants.OK_ID);
+
+        fillExistingAvdInfo();
+        validatePage();
+
+        return control;
+    }
+
+    @Override
+    public void createDialogContent(final Composite parent) {
+        GridData gd;
+        GridLayout gl;
+
+        Label label = new Label(parent, SWT.NONE);
+        label.setText("Name:");
+        String tooltip = "Name of the new Android Virtual Device";
+        label.setToolTipText(tooltip);
+
+        mAvdName = new Text(parent, SWT.BORDER);
+        mAvdName.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mAvdName.addModifyListener(new CreateNameModifyListener());
+        mAvdName.setToolTipText(tooltip);
+
+        label = new Label(parent, SWT.NONE);
+        label.setText("Target:");
+        tooltip = "The version of Android to use in the virtual device";
+        label.setToolTipText(tooltip);
+
+        mTargetCombo = new Combo(parent, SWT.READ_ONLY | SWT.DROP_DOWN);
+        mTargetCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mTargetCombo.setToolTipText(tooltip);
+        mTargetCombo.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                super.widgetSelected(e);
+                reloadSkinCombo();
+                reloadAbiTypeCombo();
+                validatePage();
+            }
+        });
+
+        //ABI group
+        label = new Label(parent, SWT.NONE);
+        label.setText("CPU/ABI:");
+        tooltip = "The CPU/ABI to use in the virtual device";
+        label.setToolTipText(tooltip);
+
+         mAbiTypeCombo = new Combo(parent, SWT.READ_ONLY | SWT.DROP_DOWN);
+         mAbiTypeCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+         mAbiTypeCombo.setToolTipText(tooltip);
+         mAbiTypeCombo.addSelectionListener(new SelectionAdapter() {
+         @Override
+         public void widgetSelected(SelectionEvent e) {
+                     super.widgetSelected(e);
+                     validatePage();
+                 }
+         });
+         mAbiTypeCombo.setEnabled(false);
+
+        // --- sd card group
+        label = new Label(parent, SWT.NONE);
+        label.setText("SD Card:");
+        label.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING,
+                false, false));
+
+        final Group sdCardGroup = new Group(parent, SWT.NONE);
+        sdCardGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        sdCardGroup.setLayout(new GridLayout(3, false));
+
+        mSdCardSizeRadio = new Button(sdCardGroup, SWT.RADIO);
+        mSdCardSizeRadio.setText("Size:");
+        mSdCardSizeRadio.setToolTipText("Create a new SD Card file");
+        mSdCardSizeRadio.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                boolean sizeMode = mSdCardSizeRadio.getSelection();
+                enableSdCardWidgets(sizeMode);
+                validatePage();
+            }
+        });
+
+        ValidateListener validateListener = new ValidateListener();
+
+        mSdCardSize = new Text(sdCardGroup, SWT.BORDER);
+        mSdCardSize.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mSdCardSize.addVerifyListener(mDigitVerifier);
+        mSdCardSize.addModifyListener(validateListener);
+        mSdCardSize.setToolTipText("Size of the new SD Card file (must be at least 9 MiB)");
+
+        mSdCardSizeCombo = new Combo(sdCardGroup, SWT.DROP_DOWN | SWT.READ_ONLY);
+        mSdCardSizeCombo.add("KiB");
+        mSdCardSizeCombo.add("MiB");
+        mSdCardSizeCombo.add("GiB");
+        mSdCardSizeCombo.select(1);
+        mSdCardSizeCombo.addSelectionListener(validateListener);
+
+        mSdCardFileRadio = new Button(sdCardGroup, SWT.RADIO);
+        mSdCardFileRadio.setText("File:");
+        mSdCardFileRadio.setToolTipText("Use an existing file for the SD Card");
+
+        mSdCardFile = new Text(sdCardGroup, SWT.BORDER);
+        mSdCardFile.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mSdCardFile.addModifyListener(validateListener);
+        mSdCardFile.setToolTipText("File to use for the SD Card");
+
+        mBrowseSdCard = new Button(sdCardGroup, SWT.PUSH);
+        mBrowseSdCard.setText("Browse...");
+        mBrowseSdCard.setToolTipText("Select the file to use for the SD Card");
+        mBrowseSdCard.addSelectionListener(new SelectionAdapter() {
+           @Override
+            public void widgetSelected(SelectionEvent arg0) {
+               onBrowseSdCard();
+               validatePage();
+            }
+        });
+
+        mSdCardSizeRadio.setSelection(true);
+        enableSdCardWidgets(true);
+
+        // --- snapshot group
+
+        label = new Label(parent, SWT.NONE);
+        label.setText("Snapshot:");
+        label.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING,
+            false, false));
+
+        final Group snapshotGroup = new Group(parent, SWT.NONE);
+        snapshotGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        snapshotGroup.setLayout(new GridLayout(3, false));
+
+        mSnapshotCheck = new Button(snapshotGroup, SWT.CHECK);
+        mSnapshotCheck.setText("Enabled");
+        mSnapshotCheck.setToolTipText(
+                "Emulator's state will be persisted between emulator executions");
+
+        // --- skin group
+        label = new Label(parent, SWT.NONE);
+        label.setText("Skin:");
+        label.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING,
+                false, false));
+
+        final Group skinGroup = new Group(parent, SWT.NONE);
+        skinGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        skinGroup.setLayout(new GridLayout(4, false));
+
+        mSkinListRadio = new Button(skinGroup, SWT.RADIO);
+        mSkinListRadio.setText("Built-in:");
+        mSkinListRadio.setToolTipText("Select an emulated screen size provided by the current Android target");
+        mSkinListRadio.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                boolean listMode = mSkinListRadio.getSelection();
+                enableSkinWidgets(listMode);
+                validatePage();
+            }
+        });
+
+        mSkinCombo = new Combo(skinGroup, SWT.READ_ONLY | SWT.DROP_DOWN);
+        mSkinCombo.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+        gd.horizontalSpan = 3;
+        mSkinCombo.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                // get the skin info
+                loadSkin();
+            }
+        });
+
+        mSkinSizeRadio = new Button(skinGroup, SWT.RADIO);
+        mSkinSizeRadio.setText("Resolution:");
+        mSkinSizeRadio.setToolTipText("Select a custom emulated screen size");
+
+        mSkinSizeWidth = new Text(skinGroup, SWT.BORDER);
+        mSkinSizeWidth.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mSkinSizeWidth.addVerifyListener(mDigitVerifier);
+        mSkinSizeWidth.addModifyListener(validateListener);
+        mSkinSizeWidth.setToolTipText("Width in pixels of the emulated screen size");
+
+        new Label(skinGroup, SWT.NONE).setText("x");
+
+        mSkinSizeHeight = new Text(skinGroup, SWT.BORDER);
+        mSkinSizeHeight.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mSkinSizeHeight.addVerifyListener(mDigitVerifier);
+        mSkinSizeHeight.addModifyListener(validateListener);
+        mSkinSizeHeight.setToolTipText("Height in pixels of the emulated screen size");
+
+        mSkinListRadio.setSelection(true);
+        enableSkinWidgets(true);
+
+        // --- hardware group
+        label = new Label(parent, SWT.NONE);
+        label.setText("Hardware:");
+        label.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING,
+                false, false));
+
+        final Group hwGroup = new Group(parent, SWT.NONE);
+        hwGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        hwGroup.setLayout(new GridLayout(2, false));
+
+        createHardwareTable(hwGroup);
+
+        // composite for the side buttons
+        Composite hwButtons = new Composite(hwGroup, SWT.NONE);
+        hwButtons.setLayoutData(new GridData(GridData.FILL_VERTICAL));
+        hwButtons.setLayout(gl = new GridLayout(1, false));
+        gl.marginHeight = gl.marginWidth = 0;
+
+        Button b = new Button(hwButtons, SWT.PUSH | SWT.FLAT);
+        b.setText("New...");
+        b.setToolTipText("Add a new hardware property");
+        b.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        b.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent event) {
+                HardwarePropertyChooser dialog = new HardwarePropertyChooser(parent.getShell(),
+                        mHardwareMap, mProperties.keySet());
+                if (dialog.open() == Window.OK) {
+                    HardwareProperty choice = dialog.getProperty();
+                    if (choice != null) {
+                        mProperties.put(choice.getName(), choice.getDefault());
+                        mHardwareViewer.refresh();
+                    }
+                }
+            }
+        });
+        mDeleteHardwareProp = new Button(hwButtons, SWT.PUSH | SWT.FLAT);
+        mDeleteHardwareProp.setText("Delete");
+        mDeleteHardwareProp.setToolTipText("Delete the selected hardware property");
+        mDeleteHardwareProp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mDeleteHardwareProp.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                ISelection selection = mHardwareViewer.getSelection();
+                if (selection instanceof IStructuredSelection) {
+                    String hwName = (String)((IStructuredSelection)selection).getFirstElement();
+                    mProperties.remove(hwName);
+                    mHardwareViewer.refresh();
+                }
+            }
+        });
+        mDeleteHardwareProp.setEnabled(false);
+
+        // --- end hardware group
+
+        mForceCreation = new Button(parent, SWT.CHECK);
+        mForceCreation.setText("Override the existing AVD with the same name");
+        mForceCreation.setToolTipText("There's already an AVD with the same name. Check this to delete it and replace it by the new AVD.");
+        mForceCreation.setLayoutData(new GridData(GridData.BEGINNING, GridData.CENTER,
+                true, false, 2, 1));
+        mForceCreation.setEnabled(false);
+        mForceCreation.addSelectionListener(validateListener);
+
+        // add a separator to separate from the ok/cancel button
+        label = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL);
+        label.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, true, false, 3, 1));
+
+        // add stuff for the error display
+        mStatusComposite = new Composite(parent, SWT.NONE);
+        mStatusComposite.setLayoutData(new GridData(GridData.FILL, GridData.CENTER,
+                true, false, 3, 1));
+        mStatusComposite.setLayout(gl = new GridLayout(2, false));
+        gl.marginHeight = gl.marginWidth = 0;
+
+        mStatusIcon = new Label(mStatusComposite, SWT.NONE);
+        mStatusIcon.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING,
+                false, false));
+        mStatusLabel = new Label(mStatusComposite, SWT.NONE);
+        mStatusLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mStatusLabel.setText(" \n "); //$NON-NLS-1$
+
+        reloadTargetCombo();
+    }
+
+    /**
+     * Creates the UI for the hardware properties table.
+     * This creates the {@link Table}, and several viewers ({@link TableViewer},
+     * {@link TableViewerColumn}) and adds edit support for the 2nd column
+     */
+    private void createHardwareTable(Composite parent) {
+        final Table hardwareTable = new Table(parent, SWT.SINGLE | SWT.FULL_SELECTION);
+        GridData gd = new GridData(GridData.FILL_HORIZONTAL | GridData.FILL_VERTICAL);
+        gd.widthHint = 200;
+        gd.heightHint = 100;
+        hardwareTable.setLayoutData(gd);
+        hardwareTable.setHeaderVisible(true);
+        hardwareTable.setLinesVisible(true);
+
+        // -- Table viewer
+        mHardwareViewer = new TableViewer(hardwareTable);
+        mHardwareViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+            @Override
+            public void selectionChanged(SelectionChangedEvent event) {
+                // it's a single selection mode, we can just access the selection index
+                // from the table directly.
+                mDeleteHardwareProp.setEnabled(hardwareTable.getSelectionIndex() != -1);
+            }
+        });
+
+        // only a content provider. Use viewers per column below (for editing support)
+        mHardwareViewer.setContentProvider(new IStructuredContentProvider() {
+            @Override
+            public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+                // we can just ignore this. we just use mProperties directly.
+            }
+
+            @Override
+            public Object[] getElements(Object arg0) {
+                return mProperties.keySet().toArray();
+            }
+
+            @Override
+            public void dispose() {
+                // pass
+            }
+        });
+
+        // -- column 1: prop abstract name
+        TableColumn col1 = new TableColumn(hardwareTable, SWT.LEFT);
+        col1.setText("Property");
+        col1.setWidth(150);
+        TableViewerColumn tvc1 = new TableViewerColumn(mHardwareViewer, col1);
+        tvc1.setLabelProvider(new CellLabelProvider() {
+            @Override
+            public void update(ViewerCell cell) {
+                String name = cell.getElement().toString();
+                HardwareProperty prop = mHardwareMap.get(name);
+                if (prop != null) {
+                    cell.setText(prop.getAbstract());
+                } else {
+                    cell.setText(String.format("%1$s (Unknown)", name));
+                }
+            }
+        });
+
+        // -- column 2: prop value
+        TableColumn col2 = new TableColumn(hardwareTable, SWT.LEFT);
+        col2.setText("Value");
+        col2.setWidth(50);
+        TableViewerColumn tvc2 = new TableViewerColumn(mHardwareViewer, col2);
+        tvc2.setLabelProvider(new CellLabelProvider() {
+            @Override
+            public void update(ViewerCell cell) {
+                String value = mProperties.get(cell.getElement());
+                cell.setText(value != null ? value : "");
+            }
+        });
+
+        // add editing support to the 2nd column
+        tvc2.setEditingSupport(new EditingSupport(mHardwareViewer) {
+            @Override
+            protected void setValue(Object element, Object value) {
+                String hardwareName = (String)element;
+                HardwareProperty property = mHardwareMap.get(hardwareName);
+                int index;
+                switch (property.getType()) {
+                    case INTEGER:
+                        mProperties.put((String)element, (String)value);
+                        break;
+                    case DISKSIZE:
+                        if (HardwareProperties.DISKSIZE_PATTERN.matcher((String)value).matches()) {
+                            mProperties.put((String)element, (String)value);
+                        }
+                        break;
+                    case BOOLEAN:
+                        index = (Integer)value;
+                        mProperties.put((String)element, HardwareProperties.BOOLEAN_VALUES[index]);
+                        break;
+                    case STRING_ENUM:
+                    case INTEGER_ENUM:
+                        // For a combo, value is the index of the enum to use.
+                        index = (Integer)value;
+                        String[] values = property.getEnum();
+                        if (values != null && values.length > index) {
+                            mProperties.put((String)element, values[index]);
+                        }
+                        break;
+                }
+                mHardwareViewer.refresh(element);
+            }
+
+            @Override
+            protected Object getValue(Object element) {
+                String hardwareName = (String)element;
+                HardwareProperty property = mHardwareMap.get(hardwareName);
+                String value = mProperties.get(hardwareName);
+                switch (property.getType()) {
+                    case INTEGER:
+                        // intended fall-through.
+                    case DISKSIZE:
+                        return value;
+                    case BOOLEAN:
+                        return HardwareProperties.getBooleanValueIndex(value);
+                    case STRING_ENUM:
+                    case INTEGER_ENUM:
+                        // For a combo, we need to return the index of the value in the enum
+                        String[] values = property.getEnum();
+                        if (values != null) {
+                            for (int i = 0; i < values.length; i++) {
+                                if (values[i].equals(value)) {
+                                    return i;
+                                }
+                            }
+                        }
+                }
+
+                return null;
+            }
+
+            @Override
+            protected CellEditor getCellEditor(Object element) {
+                String hardwareName = (String)element;
+                HardwareProperty property = mHardwareMap.get(hardwareName);
+                switch (property.getType()) {
+                    // TODO: custom TextCellEditor that restrict input.
+                    case INTEGER:
+                        // intended fall-through.
+                    case DISKSIZE:
+                        return new TextCellEditor(hardwareTable);
+                    case BOOLEAN:
+                        return new ComboBoxCellEditor(hardwareTable,
+                                HardwareProperties.BOOLEAN_VALUES,
+                                SWT.READ_ONLY | SWT.DROP_DOWN);
+                    case STRING_ENUM:
+                    case INTEGER_ENUM:
+                        String[] values = property.getEnum();
+                        if (values != null && values.length > 0) {
+                            return new ComboBoxCellEditor(hardwareTable,
+                                    values,
+                                    SWT.READ_ONLY | SWT.DROP_DOWN);
+                        }
+                }
+                return null;
+            }
+
+            @Override
+            protected boolean canEdit(Object element) {
+                String hardwareName = (String)element;
+                HardwareProperty property = mHardwareMap.get(hardwareName);
+                return property != null;
+            }
+        });
+
+
+        mHardwareViewer.setInput(mProperties);
+    }
+
+    // -- Start of internal part ----------
+    // Hide everything down-below from SWT designer
+    //$hide>>$
+
+    /**
+     * When editing an existing AVD info, fill the UI that has just been created with
+     * the values from the AVD.
+     */
+    public void fillExistingAvdInfo() {
+        if (mEditAvdInfo == null) {
+            return;
+        }
+
+        mAvdName.setText(mEditAvdInfo.getName());
+
+        Map<String, String> props = mEditAvdInfo.getProperties();
+
+        IAndroidTarget target = mEditAvdInfo.getTarget();
+        if (target != null && !mCurrentTargets.isEmpty()) {
+            // Try to select the target in the target combo.
+            // This will fail if the AVD needs to be repaired.
+            //
+            // This is a linear search but the list is always
+            // small enough and we only do this once.
+            int n = mTargetCombo.getItemCount();
+            for (int i = 0;i < n; i++) {
+                if (target.equals(mCurrentTargets.get(mTargetCombo.getItem(i)))) {
+                    mTargetCombo.select(i);
+                    reloadAbiTypeCombo();
+                    reloadSkinCombo();
+                    break;
+                }
+            }
+        }
+
+        // select the abi type
+        ISystemImage[] systemImages = getSystemImages(target);
+        if (target != null && systemImages.length > 0) {
+            mAbiTypeCombo.setEnabled(systemImages.length > 1);
+            String abiType = AvdInfo.getPrettyAbiType(mEditAvdInfo.getAbiType());
+            int n = mAbiTypeCombo.getItemCount();
+            for (int i = 0; i < n; i++) {
+                if (abiType.equals(mAbiTypeCombo.getItem(i))) {
+                    mAbiTypeCombo.select(i);
+                    reloadSkinCombo();
+                    break;
+                }
+            }
+        }
+
+        if (props != null) {
+
+            // First try the skin name and if it doesn't work fallback on the skin path
+            nextSkin: for (int s = 0; s < 2; s++) {
+                String skin = props.get(s == 0 ? AvdManager.AVD_INI_SKIN_NAME
+                                               : AvdManager.AVD_INI_SKIN_PATH);
+                if (skin != null && skin.length() > 0) {
+                    Matcher m = AvdManager.NUMERIC_SKIN_SIZE.matcher(skin);
+                    if (m.matches() && m.groupCount() == 2) {
+                        enableSkinWidgets(false);
+                        mSkinListRadio.setSelection(false);
+                        mSkinSizeRadio.setSelection(true);
+                        mSkinSizeWidth.setText(m.group(1));
+                        mSkinSizeHeight.setText(m.group(2));
+                        break nextSkin;
+                    } else {
+                        enableSkinWidgets(true);
+                        mSkinSizeRadio.setSelection(false);
+                        mSkinListRadio.setSelection(true);
+
+                        int n = mSkinCombo.getItemCount();
+                        for (int i = 0; i < n; i++) {
+                            if (skin.equals(mSkinCombo.getItem(i))) {
+                                mSkinCombo.select(i);
+                                break nextSkin;
+                            }
+                        }
+                    }
+                }
+            }
+
+            String sdcard = props.get(AvdManager.AVD_INI_SDCARD_PATH);
+            if (sdcard != null && sdcard.length() > 0) {
+                enableSdCardWidgets(false);
+                mSdCardSizeRadio.setSelection(false);
+                mSdCardFileRadio.setSelection(true);
+                mSdCardFile.setText(sdcard);
+            }
+
+            sdcard = props.get(AvdManager.AVD_INI_SDCARD_SIZE);
+            if (sdcard != null && sdcard.length() > 0) {
+                String[] values = new String[2];
+                long sdcardSize = AvdManager.parseSdcardSize(sdcard, values);
+
+                if (sdcardSize != AvdManager.SDCARD_NOT_SIZE_PATTERN) {
+                    enableSdCardWidgets(true);
+                    mSdCardFileRadio.setSelection(false);
+                    mSdCardSizeRadio.setSelection(true);
+
+                    mSdCardSize.setText(values[0]);
+
+                    String suffix = values[1];
+                    int n = mSdCardSizeCombo.getItemCount();
+                    for (int i = 0; i < n; i++) {
+                        if (mSdCardSizeCombo.getItem(i).startsWith(suffix)) {
+                            mSdCardSizeCombo.select(i);
+                        }
+                    }
+                }
+            }
+
+            String snapshots = props.get(AvdManager.AVD_INI_SNAPSHOT_PRESENT);
+            if (snapshots != null && snapshots.length() > 0) {
+                mSnapshotCheck.setSelection(snapshots.equals("true"));
+            }
+        }
+
+        mProperties.clear();
+
+        if (props != null) {
+            for (Entry<String, String> entry : props.entrySet()) {
+                HardwareProperty prop = mHardwareMap.get(entry.getKey());
+                if (prop != null && prop.isValidForUi()) {
+                    mProperties.put(entry.getKey(), entry.getValue());
+                }
+            }
+        }
+
+        // Cleanup known non-hardware properties
+        mProperties.remove(AvdManager.AVD_INI_ABI_TYPE);
+        mProperties.remove(AvdManager.AVD_INI_CPU_ARCH);
+        mProperties.remove(AvdManager.AVD_INI_SKIN_PATH);
+        mProperties.remove(AvdManager.AVD_INI_SKIN_NAME);
+        mProperties.remove(AvdManager.AVD_INI_SDCARD_SIZE);
+        mProperties.remove(AvdManager.AVD_INI_SDCARD_PATH);
+        mProperties.remove(AvdManager.AVD_INI_SNAPSHOT_PRESENT);
+        mProperties.remove(AvdManager.AVD_INI_IMAGES_1);
+        mProperties.remove(AvdManager.AVD_INI_IMAGES_2);
+
+        mHardwareViewer.refresh();
+    }
+
+    @Override
+    protected void okPressed() {
+        if (createAvd()) {
+            super.okPressed();
+        }
+    }
+
+    /**
+     * Enable or disable the sd card widgets.
+     * @param sizeMode if true the size-based widgets are to be enabled, and the file-based ones
+     * disabled.
+     */
+    private void enableSdCardWidgets(boolean sizeMode) {
+        mSdCardSize.setEnabled(sizeMode);
+        mSdCardSizeCombo.setEnabled(sizeMode);
+
+        mSdCardFile.setEnabled(!sizeMode);
+        mBrowseSdCard.setEnabled(!sizeMode);
+    }
+
+    /**
+     * Enable or disable the skin widgets.
+     * @param listMode if true the list-based widgets are to be enabled, and the size-based ones
+     * disabled.
+     */
+    private void enableSkinWidgets(boolean listMode) {
+        mSkinCombo.setEnabled(listMode);
+
+        mSkinSizeWidth.setEnabled(!listMode);
+        mSkinSizeHeight.setEnabled(!listMode);
+    }
+
+
+    private void onBrowseSdCard() {
+        FileDialog dlg = new FileDialog(getContents().getShell(), SWT.OPEN);
+        dlg.setText("Choose SD Card image file.");
+
+        String fileName = dlg.open();
+        if (fileName != null) {
+            mSdCardFile.setText(fileName);
+        }
+    }
+
+
+
+    private void reloadTargetCombo() {
+        String selected = null;
+        int index = mTargetCombo.getSelectionIndex();
+        if (index >= 0) {
+            selected = mTargetCombo.getItem(index);
+        }
+
+        mCurrentTargets.clear();
+        mTargetCombo.removeAll();
+
+        boolean found = false;
+        index = -1;
+
+        SdkManager sdkManager = mAvdManager.getSdkManager();
+        if (sdkManager != null) {
+            for (IAndroidTarget target : sdkManager.getTargets()) {
+                String name;
+                if (target.isPlatform()) {
+                    name = String.format("%s - API Level %s",
+                            target.getName(),
+                            target.getVersion().getApiString());
+                } else {
+                    name = String.format("%s (%s) - API Level %s",
+                            target.getName(),
+                            target.getVendor(),
+                            target.getVersion().getApiString());
+                }
+                mCurrentTargets.put(name, target);
+                mTargetCombo.add(name);
+                if (!found) {
+                    index++;
+                    found = name.equals(selected);
+                }
+            }
+        }
+
+        mTargetCombo.setEnabled(mCurrentTargets.size() > 0);
+
+        if (found) {
+            mTargetCombo.select(index);
+        }
+
+        reloadSkinCombo();
+    }
+
+    private void reloadSkinCombo() {
+        String selected = null;
+        int index = mSkinCombo.getSelectionIndex();
+        if (index >= 0) {
+            selected = mSkinCombo.getItem(index);
+        }
+
+        mSkinCombo.removeAll();
+        mSkinCombo.setEnabled(false);
+
+        index = mTargetCombo.getSelectionIndex();
+        if (index >= 0) {
+
+            String targetName = mTargetCombo.getItem(index);
+
+            boolean found = false;
+            IAndroidTarget target = mCurrentTargets.get(targetName);
+            if (target != null) {
+                mSkinCombo.add(String.format("Default (%s)", target.getDefaultSkin()));
+
+                index = -1;
+                for (String skin : target.getSkins()) {
+                    mSkinCombo.add(skin);
+                    if (!found) {
+                        index++;
+                        found = skin.equals(selected);
+                    }
+                }
+
+                mSkinCombo.setEnabled(true);
+
+                if (found) {
+                    mSkinCombo.select(index);
+                } else {
+                    mSkinCombo.select(0);  // default
+                    loadSkin();
+                }
+            }
+        }
+    }
+
+    /**
+    * Reload all the abi types in the selection list
+    */
+    private void reloadAbiTypeCombo() {
+       String selected = null;
+       boolean found = false;
+
+       int index = mTargetCombo.getSelectionIndex();
+       if (index >= 0) {
+           String targetName = mTargetCombo.getItem(index);
+           IAndroidTarget target = mCurrentTargets.get(targetName);
+
+           ISystemImage[] systemImages = getSystemImages(target);
+
+           mAbiTypeCombo.setEnabled(systemImages.length > 1);
+
+           // If user explicitly selected an ABI before, preserve that option
+           // If user did not explicitly select before (only one option before)
+           // force them to select
+           index = mAbiTypeCombo.getSelectionIndex();
+           if (index >= 0 && mAbiTypeCombo.getItemCount() > 1) {
+               selected = mAbiTypeCombo.getItem(index);
+           }
+
+           mAbiTypeCombo.removeAll();
+
+           int i;
+           for ( i = 0; i < systemImages.length ; i++ ) {
+               String prettyAbiType = AvdInfo.getPrettyAbiType(systemImages[i].getAbiType());
+               mAbiTypeCombo.add(prettyAbiType);
+               if (!found) {
+                   found = prettyAbiType.equals(selected);
+                   if (found) {
+                       mAbiTypeCombo.select(i);
+                   }
+               }
+           }
+
+           if (systemImages.length == 1) {
+               mAbiTypeCombo.select(0);
+           }
+       }
+    }
+
+    /**
+     * Validates the fields, displays errors and warnings.
+     * Enables the finish button if there are no errors.
+     */
+    private void validatePage() {
+        String error = null;
+        String warning = null;
+
+        // Validate AVD name
+        String avdName = mAvdName.getText().trim();
+        boolean hasAvdName = avdName.length() > 0;
+        boolean isCreate = mEditAvdInfo == null || !avdName.equals(mEditAvdInfo.getName());
+
+        if (hasAvdName && !AvdManager.RE_AVD_NAME.matcher(avdName).matches()) {
+            error = String.format(
+                "AVD name '%1$s' contains invalid characters.\nAllowed characters are: %2$s",
+                avdName, AvdManager.CHARS_AVD_NAME);
+        }
+
+        // Validate target
+        if (hasAvdName && error == null && mTargetCombo.getSelectionIndex() < 0) {
+            error = "A target must be selected in order to create an AVD.";
+        }
+
+        // validate abi type if the selected target supports multi archs.
+        if (hasAvdName && error == null && mTargetCombo.getSelectionIndex() > 0) {
+            int index = mTargetCombo.getSelectionIndex();
+            String targetName = mTargetCombo.getItem(index);
+            IAndroidTarget target = mCurrentTargets.get(targetName);
+            ISystemImage[] systemImages = getSystemImages(target);
+            if (systemImages.length > 1 && mAbiTypeCombo.getSelectionIndex() < 0) {
+               error = "An ABI type must be selected in order to create an AVD.";
+            }
+        }
+
+        // Validate SDCard path or value
+        if (error == null) {
+            // get the mode. We only need to check the file since the
+            // verifier on the size Text will prevent invalid input
+            boolean sdcardFileMode = mSdCardFileRadio.getSelection();
+            if (sdcardFileMode) {
+                String sdName = mSdCardFile.getText().trim();
+                if (sdName.length() > 0 && !new File(sdName).isFile()) {
+                    error = "SD Card path isn't valid.";
+                }
+            } else {
+                String valueString = mSdCardSize.getText();
+                if (valueString.length() > 0) {
+                    long value = 0;
+                    try {
+                        value = Long.parseLong(valueString);
+
+                        int sizeIndex = mSdCardSizeCombo.getSelectionIndex();
+                        if (sizeIndex >= 0) {
+                            // index 0 shifts by 10 (1024=K), index 1 by 20, etc.
+                            value <<= 10*(1 + sizeIndex);
+                        }
+
+                        if (value < AvdManager.SDCARD_MIN_BYTE_SIZE ||
+                                value > AvdManager.SDCARD_MAX_BYTE_SIZE) {
+                            value = 0;
+                        }
+                    } catch (Exception e) {
+                        // ignore, we'll test value below.
+                    }
+                    if (value <= 0) {
+                        error = "SD Card size is invalid. Range is 9 MiB..1023 GiB.";
+                    } else if (mEditAvdInfo != null) {
+                        // When editing an existing AVD, compare with the existing
+                        // sdcard size, if any. It only matters if there was an sdcard setting
+                        // before.
+                        Map<String, String> props = mEditAvdInfo.getProperties();
+                        if (props != null) {
+                            String original =
+                                mEditAvdInfo.getProperties().get(AvdManager.AVD_INI_SDCARD_SIZE);
+                            if (original != null && original.length() > 0) {
+                                long originalSize =
+                                    AvdManager.parseSdcardSize(original, null/*parsedStrings*/);
+                                if (originalSize > 0 && value != originalSize) {
+                                    warning = "A new SD Card file will be created.\nThe current SD Card file will be lost.";
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        // validate the skin
+        if (error == null) {
+            // get the mode, we should only check if it's in size mode since
+            // the built-in list mode is always valid.
+            if (mSkinSizeRadio.getSelection()) {
+                // need both with and heigh to be non null
+                String width = mSkinSizeWidth.getText();   // no need for trim, since the verifier
+                String height = mSkinSizeHeight.getText(); // rejects non digit.
+
+                if (width.length() == 0 || height.length() == 0) {
+                    error = "Skin size is incorrect.\nBoth dimensions must be > 0.";
+                }
+            }
+        }
+
+        // Check for duplicate AVD name
+        if (isCreate && hasAvdName && error == null && !mForceCreation.getSelection()) {
+            Pair<AvdConflict, String> conflict = mAvdManager.isAvdNameConflicting(avdName);
+            assert conflict != null;
+            switch(conflict.getFirst()) {
+            case NO_CONFLICT:
+                break;
+            case CONFLICT_EXISTING_AVD:
+            case CONFLICT_INVALID_AVD:
+                error = String.format(
+                        "The AVD name '%s' is already used.\n" +
+                        "Check \"Override the existing AVD\" to delete the existing one.",
+                        avdName);
+                break;
+            case CONFLICT_EXISTING_PATH:
+                error = String.format(
+                        "Conflict with %s\n" +
+                        "Check \"Override the existing AVD\" to delete the existing one.",
+                        conflict.getSecond());
+                break;
+            default:
+                // Hmm not supposed to happen... probably someone expanded the
+                // enum without adding something here. In this case just do an
+                // assert and use a generic error message.
+                error = String.format(
+                        "Conflict %s with %s.\n" +
+                        "Check \"Override the existing AVD\" to delete the existing one.",
+                        conflict.getFirst().toString(),
+                        conflict.getSecond());
+                assert false;
+                break;
+            }
+        }
+
+        if (error == null && mEditAvdInfo != null && isCreate) {
+            warning = String.format("The AVD '%1$s' will be duplicated into '%2$s'.",
+                    mEditAvdInfo.getName(),
+                    avdName);
+        }
+
+        // Validate the create button
+        boolean can_create = hasAvdName && error == null;
+        if (can_create) {
+            can_create &= mTargetCombo.getSelectionIndex() >= 0;
+        }
+        mOkButton.setEnabled(can_create);
+
+        // Adjust the create button label as needed
+        if (isCreate) {
+            mOkButton.setText("Create AVD");
+        } else {
+            mOkButton.setText("Edit AVD");
+        }
+
+        // -- update UI
+        if (error != null) {
+            mStatusIcon.setImage(mImageFactory.getImageByName("reject_icon16.png"));  //$NON-NLS-1$
+            mStatusLabel.setText(error);
+        } else if (warning != null) {
+            mStatusIcon.setImage(mImageFactory.getImageByName("warning_icon16.png"));  //$NON-NLS-1$
+            mStatusLabel.setText(warning);
+        } else {
+            mStatusIcon.setImage(null);
+            mStatusLabel.setText(" \n "); //$NON-NLS-1$
+        }
+
+        mStatusComposite.pack(true);
+    }
+
+    private void loadSkin() {
+        int targetIndex = mTargetCombo.getSelectionIndex();
+        if (targetIndex < 0) {
+            return;
+        }
+
+        // resolve the target.
+        String targetName = mTargetCombo.getItem(targetIndex);
+        IAndroidTarget target = mCurrentTargets.get(targetName);
+        if (target == null) {
+            return;
+        }
+
+        // get the skin name
+        String skinName = null;
+        int skinIndex = mSkinCombo.getSelectionIndex();
+        if (skinIndex < 0) {
+            return;
+        } else if (skinIndex == 0) { // default skin for the target
+            skinName = target.getDefaultSkin();
+        } else {
+            skinName = mSkinCombo.getItem(skinIndex);
+        }
+
+        // load the skin properties
+        String path = target.getPath(IAndroidTarget.SKINS);
+        File skin = new File(path, skinName);
+        if (skin.isDirectory() == false && target.isPlatform() == false) {
+            // it's possible the skin is in the parent target
+            path = target.getParent().getPath(IAndroidTarget.SKINS);
+            skin = new File(path, skinName);
+        }
+
+        if (skin.isDirectory() == false) {
+            return;
+        }
+
+        // now get the hardware.ini from the add-on (if applicable) and from the skin
+        // (if applicable)
+        HashMap<String, String> hardwareValues = new HashMap<String, String>();
+        if (target.isPlatform() == false) {
+            FileWrapper targetHardwareFile = new FileWrapper(target.getLocation(),
+                    AvdManager.HARDWARE_INI);
+            if (targetHardwareFile.isFile()) {
+                Map<String, String> targetHardwareConfig = ProjectProperties.parsePropertyFile(
+                        targetHardwareFile, null /*log*/);
+
+                if (targetHardwareConfig != null) {
+                    hardwareValues.putAll(targetHardwareConfig);
+                }
+            }
+        }
+
+        // from the skin
+        FileWrapper skinHardwareFile = new FileWrapper(skin, AvdManager.HARDWARE_INI);
+        if (skinHardwareFile.isFile()) {
+            Map<String, String> skinHardwareConfig = ProjectProperties.parsePropertyFile(
+                    skinHardwareFile, null /*log*/);
+
+            if (skinHardwareConfig != null) {
+                hardwareValues.putAll(skinHardwareConfig);
+            }
+        }
+
+        // now set those values in the list of properties for the AVD.
+        // We just check that none of those properties have been edited by the user yet.
+        for (Entry<String, String> entry : hardwareValues.entrySet()) {
+            if (mEditedProperties.contains(entry.getKey()) == false) {
+                mProperties.put(entry.getKey(), entry.getValue());
+            }
+        }
+
+        mHardwareViewer.refresh();
+    }
+
+    /**
+     * Creates an AVD from the values in the UI. Called when the user presses the OK button.
+     */
+    private boolean createAvd() {
+        String avdName = mAvdName.getText().trim();
+        int index = mTargetCombo.getSelectionIndex();
+
+        // quick check on the name and the target selection
+        if (avdName.length() == 0 || index < 0) {
+            return false;
+        }
+
+        // resolve the target.
+        String targetName = mTargetCombo.getItem(index);
+        IAndroidTarget target = mCurrentTargets.get(targetName);
+        if (target == null) {
+            return false;
+        }
+
+        // get the abi type
+        mAbiType = SdkConstants.ABI_ARMEABI;
+        ISystemImage[] systemImages = getSystemImages(target);
+        if (systemImages.length > 0) {
+            int abiIndex = mAbiTypeCombo.getSelectionIndex();
+            if (abiIndex >= 0) {
+                String prettyname = mAbiTypeCombo.getItem(abiIndex);
+                //Extract the abi type
+                int firstIndex = prettyname.indexOf("(");
+                int lastIndex = prettyname.indexOf(")");
+                mAbiType = prettyname.substring(firstIndex+1, lastIndex);
+            }
+        }
+
+        // get the SD card data from the UI.
+        String sdName = null;
+        if (mSdCardSizeRadio.getSelection()) {
+            // size mode
+            String value = mSdCardSize.getText().trim();
+            if (value.length() > 0) {
+                sdName = value;
+                // add the unit
+                switch (mSdCardSizeCombo.getSelectionIndex()) {
+                    case 0:
+                        sdName += "K";  //$NON-NLS-1$
+                        break;
+                    case 1:
+                        sdName += "M";  //$NON-NLS-1$
+                        break;
+                    case 2:
+                        sdName += "G";  //$NON-NLS-1$
+                        break;
+                    default:
+                        // shouldn't be here
+                        assert false;
+                }
+            }
+        } else {
+            // file mode.
+            sdName = mSdCardFile.getText().trim();
+        }
+
+        // get the Skin data from the UI
+        String skinName = null;
+        if (mSkinListRadio.getSelection()) {
+            // built-in list provides the skin
+            int skinIndex = mSkinCombo.getSelectionIndex();
+            if (skinIndex > 0) {
+                // index 0 is the default, we don't use it
+                skinName = mSkinCombo.getItem(skinIndex);
+            }
+        } else {
+            // size mode. get both size and writes it as a skin name
+            // thanks to validatePage() we know the content of the fields is correct
+            skinName = mSkinSizeWidth.getText() + "x" + mSkinSizeHeight.getText(); //$NON-NLS-1$
+        }
+
+        ILogger log = mSdkLog;
+        if (log == null || log instanceof MessageBoxLog) {
+            // If the current logger is a message box, we use our own (to make sure
+            // to display errors right away and customize the title).
+            log = new MessageBoxLog(
+                    String.format("Result of creating AVD '%s':", avdName),
+                    getContents().getDisplay(),
+                    false /*logErrorsOnly*/);
+        }
+
+        File avdFolder = null;
+        try {
+            avdFolder = AvdInfo.getDefaultAvdFolder(mAvdManager, avdName);
+        } catch (AndroidLocationException e) {
+            return false;
+        }
+
+        boolean force = mForceCreation.getSelection();
+        boolean snapshot = mSnapshotCheck.getSelection();
+
+        boolean success = false;
+        AvdInfo avdInfo = mAvdManager.createAvd(
+                avdFolder,
+                avdName,
+                target,
+                mAbiType,
+                skinName,
+                sdName,
+                mProperties,
+                snapshot,
+                force,
+                mEditAvdInfo != null, //edit existing
+                log);
+
+        success = avdInfo != null;
+
+        if (log instanceof MessageBoxLog) {
+            ((MessageBoxLog) log).displayResult(success);
+        }
+        return success;
+    }
+
+    /**
+     * Returns the list of system images of a target.
+     * <p/>
+     * If target is null, returns an empty list.
+     * If target is an add-on with no system images, return the list from its parent platform.
+     *
+     * @param target An IAndroidTarget. Can be null.
+     * @return A non-null ISystemImage array. Can be empty.
+     */
+    private ISystemImage[] getSystemImages(IAndroidTarget target) {
+        if (target != null) {
+            ISystemImage[] images = target.getSystemImages();
+
+            if ((images == null || images.length == 0) && !target.isPlatform()) {
+                // If an add-on does not provide any system images, use the ones from the parent.
+                images = target.getParent().getSystemImages();
+            }
+
+            if (images != null) {
+                return images;
+            }
+        }
+
+        return new ISystemImage[0];
+    }
+
+    // End of hiding from SWT Designer
+    //$hide<<$
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/MessageBoxLog.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/MessageBoxLog.java
new file mode 100755
index 0000000..f5b75e0
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/MessageBoxLog.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.widgets;
+
+import com.android.annotations.NonNull;
+import com.android.utils.ILogger;
+
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+import java.util.ArrayList;
+
+
+/**
+ * Collects all log and displays it in a message box dialog.
+ * <p/>
+ * This is good if only a few lines of log are expected.
+ * If you pass <var>logErrorsOnly</var> to the constructor, the message box
+ * will be shown only if errors were generated, which is the typical use-case.
+ * <p/>
+ * To use this: </br>
+ * - Construct a new {@link MessageBoxLog}. </br>
+ * - Pass the logger to the action. </br>
+ * - Once the action is completed, call {@link #displayResult(boolean)}
+ *   indicating whether the operation was successful or not.
+ *
+ * When <var>logErrorsOnly</var> is true, if the operation was not successful or errors
+ * were generated, this will display the message box.
+ */
+public final class MessageBoxLog implements ILogger {
+
+    final ArrayList<String> logMessages = new ArrayList<String>();
+    private final String mMessage;
+    private final Display mDisplay;
+    private final boolean mLogErrorsOnly;
+
+    /**
+     * Creates a logger that will capture all logs and eventually display them
+     * in a simple message box.
+     *
+     * @param message
+     * @param display
+     * @param logErrorsOnly
+     */
+    public MessageBoxLog(String message, Display display, boolean logErrorsOnly) {
+        mMessage = message;
+        mDisplay = display;
+        mLogErrorsOnly = logErrorsOnly;
+    }
+
+    @Override
+    public void error(Throwable throwable, String errorFormat, Object... arg) {
+        if (errorFormat != null) {
+            logMessages.add(String.format("Error: " + errorFormat, arg));
+        }
+
+        if (throwable != null) {
+            logMessages.add(throwable.getMessage());
+        }
+    }
+
+    @Override
+    public void warning(@NonNull String warningFormat, Object... arg) {
+        if (!mLogErrorsOnly) {
+            logMessages.add(String.format("Warning: " + warningFormat, arg));
+        }
+    }
+
+    @Override
+    public void info(@NonNull String msgFormat, Object... arg) {
+        if (!mLogErrorsOnly) {
+            logMessages.add(String.format(msgFormat, arg));
+        }
+    }
+
+    @Override
+    public void verbose(@NonNull String msgFormat, Object... arg) {
+        if (!mLogErrorsOnly) {
+            logMessages.add(String.format(msgFormat, arg));
+        }
+    }
+
+    /**
+     * Displays the log if anything was captured.
+     * <p/>
+     * @param success Used only when the logger was constructed with <var>logErrorsOnly</var>==true.
+     * In this case the dialog will only be shown either if success if false or some errors
+     * where captured.
+     */
+    public void displayResult(final boolean success) {
+        if (logMessages.size() > 0) {
+            final StringBuilder sb = new StringBuilder(mMessage + "\n\n");
+            for (String msg : logMessages) {
+                if (msg.length() > 0) {
+                    if (msg.charAt(0) != '\n') {
+                        int n = sb.length();
+                        if (n > 0 && sb.charAt(n-1) != '\n') {
+                            sb.append('\n');
+                        }
+                    }
+                    sb.append(msg);
+                }
+            }
+
+            // display the message
+            // dialog box only run in ui thread..
+            if (mDisplay != null && !mDisplay.isDisposed()) {
+                mDisplay.asyncExec(new Runnable() {
+                    @Override
+                    public void run() {
+                        // This is typically displayed at the end, so make sure the UI
+                        // instances are not disposed.
+                        Shell shell = null;
+                        if (mDisplay != null && !mDisplay.isDisposed()) {
+                            shell = mDisplay.getActiveShell();
+                        }
+                        if (shell == null || shell.isDisposed()) {
+                            return;
+                        }
+                        // Use the success icon if the call indicates success.
+                        // However just use the error icon if the logger was only recording errors.
+                        if (success && !mLogErrorsOnly) {
+                            MessageDialog.openInformation(shell, "Android Virtual Devices Manager",
+                                    sb.toString());
+                        } else {
+                            MessageDialog.openError(shell, "Android Virtual Devices Manager",
+                                        sb.toString());
+
+                        }
+                    }
+                });
+            }
+        }
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ResolutionChooserDialog.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ResolutionChooserDialog.java
new file mode 100644
index 0000000..7454437
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ResolutionChooserDialog.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.widgets;
+
+import com.android.sdkuilib.ui.GridDialog;
+
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Monitor;
+import org.eclipse.swt.widgets.Shell;
+
+/**
+ * Small dialog to let a user choose a screen size (from a fixed list) and a resolution
+ * (as returned by {@link Display#getMonitors()}).
+
+ * After the dialog as returned, one can query {@link #getDensity()} to get the chosen monitor
+ * pixel density.
+ */
+public class ResolutionChooserDialog extends GridDialog {
+    public final static float[] MONITOR_SIZES = new float[] {
+            13.3f, 14, 15.4f, 15.6f, 17, 19, 20, 21, 24, 30,
+    };
+
+    private Button mButton;
+    private Combo mScreenSizeCombo;
+    private Combo mMonitorCombo;
+
+    private Monitor[] mMonitors;
+    private int mScreenSizeIndex = -1;
+    private int mMonitorIndex = 0;
+
+    public ResolutionChooserDialog(Shell parentShell) {
+        super(parentShell, 2, false);
+    }
+
+    /**
+     * Returns the pixel density of the user-chosen monitor.
+     */
+    public int getDensity() {
+        float size = MONITOR_SIZES[mScreenSizeIndex];
+        Rectangle rect = mMonitors[mMonitorIndex].getBounds();
+
+        // compute the density
+        double d = Math.sqrt(rect.width * rect.width + rect.height * rect.height) / size;
+        return (int)Math.round(d);
+    }
+
+    @Override
+    protected void configureShell(Shell newShell) {
+        newShell.setText("Monitor Density");
+        super.configureShell(newShell);
+    }
+
+    @Override
+    protected Control createContents(Composite parent) {
+        Control control = super.createContents(parent);
+        mButton = getButton(IDialogConstants.OK_ID);
+        mButton.setEnabled(false);
+        return control;
+    }
+
+    @Override
+    public void createDialogContent(Composite parent) {
+        Label l = new Label(parent, SWT.NONE);
+        l.setText("Screen Size:");
+
+        mScreenSizeCombo = new Combo(parent, SWT.DROP_DOWN | SWT.READ_ONLY);
+        for (float size : MONITOR_SIZES) {
+            if (Math.round(size) == size) {
+                mScreenSizeCombo.add(String.format("%.0f\"", size));
+            } else {
+                mScreenSizeCombo.add(String.format("%.1f\"", size));
+            }
+        }
+        mScreenSizeCombo.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                mScreenSizeIndex = mScreenSizeCombo.getSelectionIndex();
+                mButton.setEnabled(mScreenSizeIndex != -1);
+            }
+        });
+
+        l = new Label(parent, SWT.NONE);
+        l.setText("Resolution:");
+
+        mMonitorCombo = new Combo(parent, SWT.DROP_DOWN | SWT.READ_ONLY);
+        mMonitors = parent.getDisplay().getMonitors();
+        for (Monitor m : mMonitors) {
+            Rectangle r = m.getBounds();
+            mMonitorCombo.add(String.format("%d x %d", r.width, r.height));
+        }
+        mMonitorCombo.select(mMonitorIndex);
+        mMonitorCombo.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetDefaultSelected(SelectionEvent arg0) {
+                mMonitorIndex = mMonitorCombo.getSelectionIndex();
+            }
+        });
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/SdkTargetSelector.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/SdkTargetSelector.java
new file mode 100644
index 0000000..7d2e90f
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/SdkTargetSelector.java
@@ -0,0 +1,460 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.widgets;
+
+import com.android.SdkConstants;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.TableItem;
+
+import java.util.Arrays;
+import java.util.Comparator;
+
+
+/**
+ * The SDK target selector is a table that is added to the given parent composite.
+ * <p/>
+ * To use, create it using {@link #SdkTargetSelector(Composite, IAndroidTarget[], boolean)} then
+ * call {@link #setSelection(IAndroidTarget)}, {@link #setSelectionListener(SelectionListener)}
+ * and finally use {@link #getSelected()} to retrieve the
+ * selection.
+ */
+public class SdkTargetSelector {
+
+    private IAndroidTarget[] mTargets;
+    private final boolean mAllowSelection;
+    private SelectionListener mSelectionListener;
+    private Table mTable;
+    private Label mDescription;
+    private Composite mInnerGroup;
+
+    /** Cache for {@link #getCheckboxWidth()} */
+    private static int sCheckboxWidth = -1;
+
+    /**
+     * Creates a new SDK Target Selector.
+     *
+     * @param parent The parent composite where the selector will be added.
+     * @param targets The list of targets. This is <em>not</em> copied, the caller must not modify.
+     *                Targets can be null or an empty array, in which case the table is disabled.
+     */
+    public SdkTargetSelector(Composite parent, IAndroidTarget[] targets) {
+        this(parent, targets, true /*allowSelection*/);
+    }
+
+    /**
+     * Creates a new SDK Target Selector.
+     *
+     * @param parent The parent composite where the selector will be added.
+     * @param targets The list of targets. This is <em>not</em> copied, the caller must not modify.
+     *                Targets can be null or an empty array, in which case the table is disabled.
+     * @param allowSelection True if selection is enabled.
+     */
+    public SdkTargetSelector(Composite parent, IAndroidTarget[] targets, boolean allowSelection) {
+        // Layout has 1 column
+        mInnerGroup = new Composite(parent, SWT.NONE);
+        mInnerGroup.setLayout(new GridLayout());
+        mInnerGroup.setLayoutData(new GridData(GridData.FILL_BOTH));
+        mInnerGroup.setFont(parent.getFont());
+
+        mAllowSelection = allowSelection;
+        int style = SWT.BORDER | SWT.SINGLE | SWT.FULL_SELECTION;
+        if (allowSelection) {
+            style |= SWT.CHECK;
+        }
+        mTable = new Table(mInnerGroup, style);
+        mTable.setHeaderVisible(true);
+        mTable.setLinesVisible(false);
+
+        GridData data = new GridData();
+        data.grabExcessVerticalSpace = true;
+        data.grabExcessHorizontalSpace = true;
+        data.horizontalAlignment = GridData.FILL;
+        data.verticalAlignment = GridData.FILL;
+        mTable.setLayoutData(data);
+
+        mDescription = new Label(mInnerGroup, SWT.WRAP);
+        mDescription.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        // create the table columns
+        final TableColumn column0 = new TableColumn(mTable, SWT.NONE);
+        column0.setText("Target Name");
+        final TableColumn column1 = new TableColumn(mTable, SWT.NONE);
+        column1.setText("Vendor");
+        final TableColumn column2 = new TableColumn(mTable, SWT.NONE);
+        column2.setText("Platform");
+        final TableColumn column3 = new TableColumn(mTable, SWT.NONE);
+        column3.setText("API Level");
+
+        adjustColumnsWidth(mTable, column0, column1, column2, column3);
+        setupSelectionListener(mTable);
+        setTargets(targets);
+        setupTooltip(mTable);
+    }
+
+    /**
+     * Returns the layout data of the inner composite widget that contains the target selector.
+     * By default the layout data is set to a {@link GridData} with a {@link GridData#FILL_BOTH}
+     * mode.
+     * <p/>
+     * This can be useful if you want to change the {@link GridData#horizontalSpan} for example.
+     */
+    public Object getLayoutData() {
+        return mInnerGroup.getLayoutData();
+    }
+
+    /**
+     * Returns the list of known targets.
+     * <p/>
+     * This is not a copy. Callers must <em>not</em> modify this array.
+     */
+    public IAndroidTarget[] getTargets() {
+        return mTargets;
+    }
+
+    /**
+     * Changes the targets of the SDK Target Selector.
+     *
+     * @param targets The list of targets. This is <em>not</em> copied, the caller must not modify.
+     */
+    public void setTargets(IAndroidTarget[] targets) {
+        mTargets = targets;
+        if (mTargets != null) {
+            Arrays.sort(mTargets, new Comparator<IAndroidTarget>() {
+                @Override
+                public int compare(IAndroidTarget o1, IAndroidTarget o2) {
+                    return o1.compareTo(o2);
+                }
+            });
+        }
+
+        fillTable(mTable);
+    }
+
+    /**
+     * Sets a selection listener. Set it to null to remove it.
+     * The listener will be called <em>after</em> this table processed its selection
+     * events so that the caller can see the updated state.
+     * <p/>
+     * The event's item contains a {@link TableItem}.
+     * The {@link TableItem#getData()} contains an {@link IAndroidTarget}.
+     * <p/>
+     * It is recommended that the caller uses the {@link #getSelected()} method instead.
+     *
+     * @param selectionListener The new listener or null to remove it.
+     */
+    public void setSelectionListener(SelectionListener selectionListener) {
+        mSelectionListener = selectionListener;
+    }
+
+    /**
+     * Sets the current target selection.
+     * <p/>
+     * If the selection is actually changed, this will invoke the selection listener
+     * (if any) with a null event.
+     *
+     * @param target the target to be selection
+     * @return true if the target could be selected, false otherwise.
+     */
+    public boolean setSelection(IAndroidTarget target) {
+        if (!mAllowSelection) {
+            return false;
+        }
+
+        boolean found = false;
+        boolean modified = false;
+
+        if (mTable != null && !mTable.isDisposed()) {
+            for (TableItem i : mTable.getItems()) {
+                if ((IAndroidTarget) i.getData() == target) {
+                    found = true;
+                    if (!i.getChecked()) {
+                        modified = true;
+                        i.setChecked(true);
+                    }
+                } else if (i.getChecked()) {
+                    modified = true;
+                    i.setChecked(false);
+                }
+            }
+        }
+
+        if (modified && mSelectionListener != null) {
+            mSelectionListener.widgetSelected(null);
+        }
+
+        return found;
+    }
+
+    /**
+     * Returns the selected item.
+     *
+     * @return The selected item or null.
+     */
+    public IAndroidTarget getSelected() {
+        if (mTable == null || mTable.isDisposed()) {
+            return null;
+        }
+
+        for (TableItem i : mTable.getItems()) {
+            if (i.getChecked()) {
+                return (IAndroidTarget) i.getData();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Adds a listener to adjust the columns width when the parent is resized.
+     * <p/>
+     * If we need something more fancy, we might want to use this:
+     * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet77.java?view=co
+     */
+    private void adjustColumnsWidth(final Table table,
+            final TableColumn column0,
+            final TableColumn column1,
+            final TableColumn column2,
+            final TableColumn column3) {
+        // Add a listener to resize the column to the full width of the table
+        table.addControlListener(new ControlAdapter() {
+            @Override
+            public void controlResized(ControlEvent e) {
+                Rectangle r = table.getClientArea();
+                int width = r.width;
+
+                // On the Mac, the width of the checkbox column is not included (and checkboxes
+                // are shown if mAllowSelection=true). Subtract this size from the available
+                // width to be distributed among the columns.
+                if (mAllowSelection
+                        && SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) {
+                    width -= getCheckboxWidth();
+                }
+
+                column0.setWidth(width * 30 / 100); // 30%
+                column1.setWidth(width * 45 / 100); // 45%
+                column2.setWidth(width * 15 / 100); // 15%
+                column3.setWidth(width * 10 / 100); // 10%
+            }
+        });
+    }
+
+
+    /**
+     * Creates a selection listener that will check or uncheck the whole line when
+     * double-clicked (aka "the default selection").
+     */
+    private void setupSelectionListener(final Table table) {
+        if (!mAllowSelection) {
+            return;
+        }
+
+        // Add a selection listener that will check/uncheck items when they are double-clicked
+        table.addSelectionListener(new SelectionListener() {
+            /** Default selection means double-click on "most" platforms */
+            @Override
+            public void widgetDefaultSelected(SelectionEvent e) {
+                if (e.item instanceof TableItem) {
+                    TableItem i = (TableItem) e.item;
+                    i.setChecked(!i.getChecked());
+                    enforceSingleSelection(i);
+                    updateDescription(i);
+                }
+
+                if (mSelectionListener != null) {
+                    mSelectionListener.widgetDefaultSelected(e);
+                }
+            }
+
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                if (e.item instanceof TableItem) {
+                    TableItem i = (TableItem) e.item;
+                    enforceSingleSelection(i);
+                    updateDescription(i);
+                }
+
+                if (mSelectionListener != null) {
+                    mSelectionListener.widgetSelected(e);
+                }
+            }
+
+            /**
+             * If we're not in multiple selection mode, uncheck all other
+             * items when this one is selected.
+             */
+            private void enforceSingleSelection(TableItem item) {
+                if (item.getChecked()) {
+                    Table parentTable = item.getParent();
+                    for (TableItem i2 : parentTable.getItems()) {
+                        if (i2 != item && i2.getChecked()) {
+                            i2.setChecked(false);
+                        }
+                    }
+                }
+            }
+        });
+    }
+
+
+    /**
+     * Fills the table with all SDK targets.
+     * The table columns are:
+     * <ul>
+     * <li>column 0: sdk name
+     * <li>column 1: sdk vendor
+     * <li>column 2: sdk api name
+     * <li>column 3: sdk version
+     * </ul>
+     */
+    private void fillTable(final Table table) {
+
+        if (table == null || table.isDisposed()) {
+            return;
+        }
+
+        table.removeAll();
+
+        if (mTargets != null && mTargets.length > 0) {
+            table.setEnabled(true);
+            for (IAndroidTarget target : mTargets) {
+                TableItem item = new TableItem(table, SWT.NONE);
+                item.setData(target);
+                item.setText(0, target.getName());
+                item.setText(1, target.getVendor());
+                item.setText(2, target.getVersionName());
+                item.setText(3, target.getVersion().getApiString());
+            }
+        } else {
+            table.setEnabled(false);
+            TableItem item = new TableItem(table, SWT.NONE);
+            item.setData(null);
+            item.setText(0, "--");
+            item.setText(1, "No target available");
+            item.setText(2, "--");
+            item.setText(3, "--");
+        }
+    }
+
+    /**
+     * Sets up a tooltip that displays the current item description.
+     * <p/>
+     * Displaying a tooltip over the table looks kind of odd here. Instead we actually
+     * display the description in a label under the table.
+     */
+    private void setupTooltip(final Table table) {
+
+        if (table == null || table.isDisposed()) {
+            return;
+        }
+
+        /*
+         * Reference:
+         * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet125.java?view=markup
+         */
+
+        final Listener listener = new Listener() {
+            @Override
+            public void handleEvent(Event event) {
+
+                switch(event.type) {
+                case SWT.KeyDown:
+                case SWT.MouseExit:
+                case SWT.MouseDown:
+                    return;
+
+                case SWT.MouseHover:
+                    updateDescription(table.getItem(new Point(event.x, event.y)));
+                    break;
+
+                case SWT.Selection:
+                    if (event.item instanceof TableItem) {
+                        updateDescription((TableItem) event.item);
+                    }
+                    break;
+
+                default:
+                    return;
+                }
+
+            }
+        };
+
+        table.addListener(SWT.Dispose, listener);
+        table.addListener(SWT.KeyDown, listener);
+        table.addListener(SWT.MouseMove, listener);
+        table.addListener(SWT.MouseHover, listener);
+    }
+
+    /**
+     * Updates the description label with the description of the item's android target, if any.
+     */
+    private void updateDescription(TableItem item) {
+        if (item != null) {
+            Object data = item.getData();
+            if (data instanceof IAndroidTarget) {
+                String newTooltip = ((IAndroidTarget) data).getDescription();
+                mDescription.setText(newTooltip == null ? "" : newTooltip);  //$NON-NLS-1$
+            }
+        }
+    }
+
+    /** Enables or disables the controls. */
+    public void setEnabled(boolean enabled) {
+        if (mInnerGroup != null && mTable != null && !mTable.isDisposed()) {
+            enableControl(mInnerGroup, enabled);
+        }
+    }
+
+    /** Enables or disables controls; recursive for composite controls. */
+    private void enableControl(Control c, boolean enabled) {
+        c.setEnabled(enabled);
+        if (c instanceof Composite)
+        for (Control c2 : ((Composite) c).getChildren()) {
+            enableControl(c2, enabled);
+        }
+    }
+
+    /** Computes the width of a checkbox */
+    private int getCheckboxWidth() {
+        if (sCheckboxWidth == -1) {
+            Shell shell = new Shell(mTable.getShell(), SWT.NO_TRIM);
+            Button checkBox = new Button(shell, SWT.CHECK);
+            sCheckboxWidth = checkBox.computeSize(SWT.DEFAULT, SWT.DEFAULT).x;
+            shell.dispose();
+        }
+
+        return sCheckboxWidth;
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ToggleButton.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ToggleButton.java
new file mode 100755
index 0000000..7c66bcf
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ToggleButton.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.widgets;
+
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CLabel;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.events.MouseTrackListener;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Event;
+
+/**
+ * A label that can display 2 images depending on its internal state.
+ * This acts as a button by firing the {@link SWT#Selection} listener.
+ */
+public class ToggleButton extends CLabel {
+    private Image[] mImage = new Image[2];
+    private String[] mTooltip = new String[2];
+    private boolean mMouseIn;
+    private int mState = 0;
+
+
+    public ToggleButton(
+            Composite parent,
+            int style,
+            Image image1,
+            Image image2,
+            String tooltip1,
+            String tooltip2) {
+        super(parent, style);
+        mImage[0] = image1;
+        mImage[1] = image2;
+        mTooltip[0] = tooltip1;
+        mTooltip[1] = tooltip2;
+        updateImageAndTooltip();
+
+        addMouseListener(new MouseListener() {
+            @Override
+            public void mouseDown(MouseEvent e) {
+                // pass
+            }
+
+            @Override
+            public void mouseUp(MouseEvent e) {
+                // We select on mouse-up, as it should be properly done since this is the
+                // only way a user can cancel a button click by moving out of the button.
+                if (mMouseIn && e.button == 1) {
+                    notifyListeners(SWT.Selection, new Event());
+                }
+            }
+
+            @Override
+            public void mouseDoubleClick(MouseEvent e) {
+                if (mMouseIn && e.button == 1) {
+                    notifyListeners(SWT.DefaultSelection, new Event());
+                }
+            }
+        });
+
+        addMouseTrackListener(new MouseTrackListener() {
+            @Override
+            public void mouseExit(MouseEvent e) {
+                if (mMouseIn) {
+                    mMouseIn = false;
+                    redraw();
+                }
+            }
+
+            @Override
+            public void mouseEnter(MouseEvent e) {
+                if (!mMouseIn) {
+                    mMouseIn = true;
+                    redraw();
+                }
+            }
+
+            @Override
+            public void mouseHover(MouseEvent e) {
+                // pass
+            }
+        });
+    }
+
+    @Override
+    public int getStyle() {
+        int style = super.getStyle();
+        if (mMouseIn) {
+            style |= SWT.SHADOW_IN;
+        }
+        return style;
+    }
+
+    /**
+     * Sets current state.
+     * @param state A value 0 or 1.
+     */
+    public void setState(int state) {
+        assert state == 0 || state == 1;
+        mState = state;
+        updateImageAndTooltip();
+        redraw();
+    }
+
+    /**
+     * Returns the current state
+     * @return Returns the current state, either 0 or 1.
+     */
+    public int getState() {
+        return mState;
+    }
+
+    protected void updateImageAndTooltip() {
+        setImage(mImage[getState()]);
+        setToolTipText(mTooltip[getState()]);
+    }
+}
+
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/repository/AvdManagerWindow.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/repository/AvdManagerWindow.java
new file mode 100755
index 0000000..dd34bef
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/repository/AvdManagerWindow.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.repository;
+
+import com.android.sdkuilib.internal.repository.ui.AvdManagerWindowImpl1;
+import com.android.sdkuilib.internal.widgets.AvdSelector;
+import com.android.utils.ILogger;
+
+import org.eclipse.swt.widgets.Shell;
+
+/**
+ * Opens an AVD Manager Window.
+ *
+ * This is the public entry point for using the window.
+ */
+public class AvdManagerWindow {
+
+    /** The actual window implementation to which this class delegates. */
+    private AvdManagerWindowImpl1 mWindow;
+
+    /**
+     * Enum giving some indication of what is invoking this window.
+     * The behavior and UI will change slightly depending on the context.
+     * <p/>
+     * Note: if you add Android support to your specific IDE, you might want
+     * to specialize this context enum.
+     */
+    public enum AvdInvocationContext {
+        /**
+         * The AVD Manager is invoked from the stand-alone 'android' tool.
+         * In this mode, we present an about box, a settings page.
+         * For SdkMan2, we also have a menu bar and link to the SDK Manager 2.
+         */
+        STANDALONE,
+
+        /**
+         * The AVD Manager is embedded as a dialog in the SDK Manager
+         * or in the {@link AvdSelector}.
+         * This is similar to the {@link #STANDALONE} mode except we don't need
+         * to display a menu bar at all since we don't want a menu item linking
+         * back to the SDK Manager and we don't need to redisplay the options
+         * and about which are already on the root window.
+         */
+        DIALOG,
+
+        /**
+         * The AVD Manager is invoked from an IDE.
+         * In this mode, we do not modify the menu bar.
+         * There is no about box and no settings.
+         */
+        IDE,
+    }
+
+
+    /**
+     * Creates a new window. Caller must call open(), which will block.
+     *
+     * @param parentShell Parent shell.
+     * @param sdkLog Logger. Cannot be null.
+     * @param osSdkRoot The OS path to the SDK root.
+     * @param context The {@link AvdInvocationContext} to change the behavior depending on who's
+     *  opening the SDK Manager.
+     */
+    public AvdManagerWindow(
+            Shell parentShell,
+            ILogger sdkLog,
+            String osSdkRoot,
+            AvdInvocationContext context) {
+        mWindow = new AvdManagerWindowImpl1(
+                parentShell,
+                sdkLog,
+                osSdkRoot,
+                context);
+    }
+
+    /**
+     * Opens the window.
+     */
+    public void open() {
+        mWindow.open();
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/repository/SdkUpdaterWindow.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/repository/SdkUpdaterWindow.java
new file mode 100755
index 0000000..343acc9
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/repository/SdkUpdaterWindow.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.repository;
+
+import com.android.sdklib.repository.ISdkChangeListener;
+import com.android.sdkuilib.internal.repository.ISdkUpdaterWindow;
+import com.android.sdkuilib.internal.repository.ui.SdkUpdaterWindowImpl2;
+import com.android.utils.ILogger;
+
+import org.eclipse.swt.widgets.Shell;
+
+/**
+ * Opens an SDK Manager Window.
+ *
+ * This is the public entry point for using the window.
+ */
+public class SdkUpdaterWindow {
+
+    /** The actual window implementation to which this class delegates. */
+    private ISdkUpdaterWindow mWindow;
+
+    /**
+     * Enum giving some indication of what is invoking this window.
+     * The behavior and UI will change slightly depending on the context.
+     * <p/>
+     * Note: if you add Android support to your specific IDE, you might want
+     * to specialize this context enum.
+     */
+    public enum SdkInvocationContext {
+        /**
+         * The SDK Manager is invoked from the stand-alone 'android' tool.
+         * In this mode, we present an about box, a settings page.
+         * For SdkMan2, we also have a menu bar and link to the AVD manager.
+         */
+        STANDALONE,
+
+        /**
+         * The SDK Manager is invoked from the standalone AVD Manager.
+         * This is similar to the standalone mode except that in this case we
+         * don't display a menu item linking to the AVD Manager.
+         */
+        AVD_MANAGER,
+
+        /**
+         * The SDK Manager is invoked from an IDE.
+         * In this mode, we do not modify the menu bar. There is no about box
+         * and no settings (e.g. HTTP proxy settings are inherited from Eclipse.)
+         */
+        IDE,
+
+        /**
+         * The SDK Manager is invoked from the AVD Selector.
+         * For SdkMan1, this means the AVD page will be displayed first.
+         * For SdkMan2, we won't be using this.
+         */
+        AVD_SELECTOR
+    }
+
+    /**
+     * Creates a new window. Caller must call open(), which will block.
+     *
+     * @param parentShell Parent shell.
+     * @param sdkLog Logger. Cannot be null.
+     * @param osSdkRoot The OS path to the SDK root.
+     * @param context The {@link SdkInvocationContext} to change the behavior depending on who's
+     *  opening the SDK Manager.
+     */
+    public SdkUpdaterWindow(
+            Shell parentShell,
+            ILogger sdkLog,
+            String osSdkRoot,
+            SdkInvocationContext context) {
+
+        mWindow = new SdkUpdaterWindowImpl2(parentShell, sdkLog, osSdkRoot, context);
+    }
+
+    /**
+     * Adds a new listener to be notified when a change is made to the content of the SDK.
+     * This should be called before {@link #open()}.
+     */
+    public void addListener(ISdkChangeListener listener) {
+        mWindow.addListener(listener);
+    }
+
+    /**
+     * Removes a new listener to be notified anymore when a change is made to the content of
+     * the SDK.
+     */
+    public void removeListener(ISdkChangeListener listener) {
+        mWindow.removeListener(listener);
+    }
+
+    /**
+     * Opens the window.
+     */
+    public void open() {
+        mWindow.open();
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/ui/AuthenticationDialog.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/ui/AuthenticationDialog.java
new file mode 100644
index 0000000..07e65b7
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/ui/AuthenticationDialog.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.ui;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+/**
+ * Dialog which collects from the user his/her login and password.
+ */
+public class AuthenticationDialog extends GridDialog {
+    private Text mTxtLogin;
+    private Text mTxtPassword;
+    private Text mTxtWorkstation;
+    private Text mTxtDomain;
+
+    private String mTitle;
+    private String mMessage;
+
+    private static String sLogin = "";
+    private static String sPassword = "";
+    private static String sWorkstation = "";
+    private static String sDomain = "";
+
+    /**
+     * Constructor which retrieves the parent {@link Shell} and the message to
+     * be displayed in this dialog.
+     *
+     * @param parentShell Parent Shell
+     * @param title Title of the window.
+     * @param message Message the be displayed in this dialog.
+     */
+    public AuthenticationDialog(Shell parentShell, String title, String message) {
+        super(parentShell, 1, false);
+        // assign fields
+        mTitle = title;
+        mMessage = message;
+    }
+
+    @Override
+    public void createDialogContent(Composite parent) {
+        // Configure Dialog
+        getShell().setText(mTitle);
+        GridData data = new GridData(SWT.FILL, SWT.FILL, true, true);
+        parent.setLayoutData(data);
+
+        // Upper Composite
+        Composite upperComposite = new Composite(parent, SWT.NONE);
+        GridLayout layout = new GridLayout(2, false);
+        layout.verticalSpacing = 10;
+        upperComposite.setLayout(layout);
+        data = new GridData(SWT.FILL, SWT.CENTER, true, true);
+        upperComposite.setLayoutData(data);
+
+        // add message label
+        Label lblMessage = new Label(upperComposite, SWT.WRAP);
+        lblMessage.setText(mMessage);
+        data = new GridData(SWT.FILL, SWT.CENTER, true, true, 2, 1);
+        data.widthHint = 500;
+        lblMessage.setLayoutData(data);
+
+        // add user name label and text field
+        Label lblUserName = new Label(upperComposite, SWT.NONE);
+        lblUserName.setText("Login:");
+        data = new GridData(SWT.LEFT, SWT.CENTER, false, false);
+        lblUserName.setLayoutData(data);
+
+        mTxtLogin = new Text(upperComposite, SWT.SINGLE | SWT.BORDER);
+        data = new GridData(SWT.FILL, SWT.CENTER, true, false);
+        mTxtLogin.setLayoutData(data);
+        mTxtLogin.setFocus();
+        mTxtLogin.setText(sLogin);
+        mTxtLogin.addModifyListener(new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent arg0) {
+                sLogin = mTxtLogin.getText().trim();
+            }
+        });
+
+        // add password label and text field
+        Label lblPassword = new Label(upperComposite, SWT.NONE);
+        lblPassword.setText("Password:");
+        data = new GridData(SWT.LEFT, SWT.CENTER, false, false);
+        lblPassword.setLayoutData(data);
+
+        mTxtPassword = new Text(upperComposite, SWT.SINGLE | SWT.PASSWORD | SWT.BORDER);
+        data = new GridData(SWT.FILL, SWT.CENTER, true, false);
+        mTxtPassword.setLayoutData(data);
+        mTxtPassword.setText(sPassword);
+        mTxtPassword.addModifyListener(new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent arg0) {
+                sPassword = mTxtPassword.getText();
+            }
+        });
+
+        // add a label indicating that the following two fields are optional
+        Label lblInfo = new Label(upperComposite, SWT.NONE);
+        lblInfo.setText("Provide the following info if your proxy uses NTLM authentication. Leave blank otherwise.");
+        data = new GridData();
+        data.horizontalSpan = 2;
+        lblInfo.setLayoutData(data);
+
+        // add workstation label and text field
+        Label lblWorkstation = new Label(upperComposite, SWT.NONE);
+        lblWorkstation.setText("Workstation:");
+        data = new GridData(SWT.LEFT, SWT.CENTER, false, false);
+        lblWorkstation.setLayoutData(data);
+
+        mTxtWorkstation = new Text(upperComposite, SWT.SINGLE | SWT.BORDER);
+        data = new GridData(SWT.FILL, SWT.CENTER, true, false);
+        mTxtWorkstation.setLayoutData(data);
+        mTxtWorkstation.setText(sWorkstation);
+        mTxtWorkstation.addModifyListener(new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent arg0) {
+                sWorkstation = mTxtWorkstation.getText().trim();
+            }
+        });
+
+        // add domain label and text field
+        Label lblDomain = new Label(upperComposite, SWT.NONE);
+        lblDomain.setText("Domain:");
+        data = new GridData(SWT.LEFT, SWT.CENTER, false, false);
+        lblDomain.setLayoutData(data);
+
+        mTxtDomain = new Text(upperComposite, SWT.SINGLE | SWT.BORDER);
+        data = new GridData(SWT.FILL, SWT.CENTER, true, false);
+        mTxtDomain.setLayoutData(data);
+        mTxtDomain.setText(sDomain);
+        mTxtDomain.addModifyListener(new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent arg0) {
+                sDomain = mTxtDomain.getText().trim();
+            }
+        });
+    }
+
+    /**
+     * Retrieves the Login field information
+     *
+     * @return Login field value or empty String. Return value is never null
+     */
+    public String getLogin() {
+        return sLogin;
+    }
+
+    /**
+     * Retrieves the Password field information
+     *
+     * @return Password field value or empty String. Return value is never null
+     */
+    public String getPassword() {
+        return sPassword;
+    }
+
+    /**
+     * Retrieves the workstation field information
+     *
+     * @return Workstation field value or empty String. Return value is never null
+     */
+    public String getWorkstation() {
+        return sWorkstation;
+    }
+
+    /**
+     * Retrieves the domain field information
+     *
+     * @return Domain field value or empty String. Return value is never null
+     */
+    public String getDomain() {
+        return sDomain;
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/ui/GridDataBuilder.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/ui/GridDataBuilder.java
new file mode 100755
index 0000000..381dea0
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/ui/GridDataBuilder.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.ui;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Control;
+
+/**
+ * A little helper to create a new {@link GridData} and set its properties.
+ * <p/>
+ * Example of usage: <br/>
+ * <code>
+ *   GridDataHelper.create(myControl).hSpan(2).hAlignCenter().fill();
+ * </code>
+ */
+public final class GridDataBuilder {
+
+    private GridData mGD;
+
+    private GridDataBuilder() {
+        mGD = new GridData();
+    }
+
+    /**
+     * Creates new {@link GridData} and associates it on the <code>control</code> composite.
+     */
+    static public GridDataBuilder create(Control control) {
+        GridDataBuilder gdh = new GridDataBuilder();
+        control.setLayoutData(gdh.mGD);
+        return gdh;
+    }
+
+    /** Sets <code>widthHint</code> to <code>w</code>. */
+    public GridDataBuilder wHint(int w) {
+        mGD.widthHint = w;
+        return this;
+    }
+
+    /** Sets <code>heightHint</code> to <code>h</code>. */
+    public GridDataBuilder hHint(int h) {
+        mGD.heightHint = h;
+        return this;
+    }
+
+    /** Sets <code>horizontalIndent</code> to <code>h</code>. */
+    public GridDataBuilder hIndent(int h) {
+        mGD.horizontalIndent = h;
+        return this;
+    }
+
+    /** Sets <code>horizontalSpan</code> to <code>h</code>. */
+    public GridDataBuilder hSpan(int h) {
+        mGD.horizontalSpan = h;
+        return this;
+    }
+
+    /** Sets <code>verticalSpan</code> to <code>v</code>. */
+    public GridDataBuilder vSpan(int v) {
+        mGD.verticalSpan = v;
+        return this;
+    }
+
+    /** Sets <code>horizontalAlignment</code> to {@link SWT#CENTER}. */
+    public GridDataBuilder hCenter() {
+        mGD.horizontalAlignment = SWT.CENTER;
+        return this;
+    }
+
+    /** Sets <code>verticalAlignment</code> to {@link SWT#CENTER}. */
+    public GridDataBuilder vCenter() {
+        mGD.verticalAlignment = SWT.CENTER;
+        return this;
+    }
+
+    /** Sets <code>verticalAlignment</code> to {@link SWT#TOP}. */
+    public GridDataBuilder vTop() {
+        mGD.verticalAlignment = SWT.TOP;
+        return this;
+    }
+
+    /** Sets <code>verticalAlignment</code> to {@link SWT#BOTTOM}. */
+    public GridDataBuilder vBottom() {
+        mGD.verticalAlignment = SWT.BOTTOM;
+        return this;
+    }
+
+    /** Sets <code>horizontalAlignment</code> to {@link SWT#LEFT}. */
+    public GridDataBuilder hLeft() {
+        mGD.horizontalAlignment = SWT.LEFT;
+        return this;
+    }
+
+    /** Sets <code>horizontalAlignment</code> to {@link SWT#RIGHT}. */
+    public GridDataBuilder hRight() {
+        mGD.horizontalAlignment = SWT.RIGHT;
+        return this;
+    }
+
+    /** Sets <code>horizontalAlignment</code> to {@link GridData#FILL}. */
+    public GridDataBuilder hFill() {
+        mGD.horizontalAlignment = GridData.FILL;
+        return this;
+    }
+
+    /** Sets <code>verticalAlignment</code> to {@link GridData#FILL}. */
+    public GridDataBuilder vFill() {
+        mGD.verticalAlignment = GridData.FILL;
+        return this;
+    }
+
+    /**
+     * Sets both <code>horizontalAlignment</code> and <code>verticalAlignment</code>
+     * to {@link GridData#FILL}.
+     */
+    public GridDataBuilder fill() {
+        mGD.horizontalAlignment = GridData.FILL;
+        mGD.verticalAlignment = GridData.FILL;
+        return this;
+    }
+
+    /** Sets <code>grabExcessHorizontalSpace</code> to true. */
+    public GridDataBuilder hGrab() {
+        mGD.grabExcessHorizontalSpace = true;
+        return this;
+    }
+
+    /** Sets <code>grabExcessVerticalSpace</code> to true. */
+    public GridDataBuilder vGrab() {
+        mGD.grabExcessVerticalSpace = true;
+        return this;
+    }
+
+    /**
+     * Sets both <code>grabExcessHorizontalSpace</code> and
+     * <code>grabExcessVerticalSpace</code> to true.
+     */
+    public GridDataBuilder grab() {
+        mGD.grabExcessHorizontalSpace = true;
+        mGD.grabExcessVerticalSpace = true;
+        return this;
+    }
+
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/ui/GridDialog.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/ui/GridDialog.java
new file mode 100644
index 0000000..9bf9c29
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/ui/GridDialog.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.ui;
+
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Shell;
+
+/**
+ * JFace-based dialog that properly sets up a {@link GridLayout} top composite with the proper
+ * margin.
+ * <p/>
+ * Implementing dialog must create the content of the dialog in
+ * {@link #createDialogContent(Composite)}.
+ * <p/>
+ * A JFace dialog is perfect if you want a typical "OK | cancel" workflow, with the OK and
+ * cancel things all handled for you using a predefined layout. If you want a different set
+ * of buttons or a different layout, consider {@link SwtBaseDialog} instead.
+ */
+public abstract class GridDialog extends Dialog {
+
+    private final int mNumColumns;
+    private final boolean mMakeColumnsEqualWidth;
+
+    /**
+     * Creates the dialog
+     * @param parentShell the parent {@link Shell}.
+     * @param numColumns the number of columns in the grid
+     * @param makeColumnsEqualWidth whether or not the columns will have equal width
+     */
+    public GridDialog(Shell parentShell, int numColumns, boolean makeColumnsEqualWidth) {
+        super(parentShell);
+        mNumColumns = numColumns;
+        mMakeColumnsEqualWidth = makeColumnsEqualWidth;
+    }
+
+    /**
+     * Creates the content of the dialog. The <var>parent</var> composite is a {@link GridLayout}
+     * created with the <var>numColumn</var> and <var>makeColumnsEqualWidth</var> parameters
+     * passed to {@link #GridDialog(Shell, int, boolean)}.
+     * @param parent the parent composite.
+     */
+    public abstract void createDialogContent(Composite parent);
+
+    @Override
+    protected Control createDialogArea(Composite parent) {
+        Composite top = new Composite(parent, SWT.NONE);
+        GridLayout layout = new GridLayout(mNumColumns, mMakeColumnsEqualWidth);
+        layout.marginHeight = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_MARGIN);
+        layout.marginWidth = convertHorizontalDLUsToPixels(IDialogConstants.HORIZONTAL_MARGIN);
+        layout.verticalSpacing = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_SPACING);
+        layout.horizontalSpacing = convertHorizontalDLUsToPixels(
+                IDialogConstants.HORIZONTAL_SPACING);
+        top.setLayout(layout);
+        top.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        createDialogContent(top);
+
+        applyDialogFont(top);
+        return top;
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/ui/GridLayoutBuilder.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/ui/GridLayoutBuilder.java
new file mode 100755
index 0000000..7e8c161
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/ui/GridLayoutBuilder.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.ui;
+
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * A little helper to create a new {@link GridLayout}, associate to a {@link Composite}
+ * and set its common attributes.
+ * <p/>
+ * Example of usage: <br/>
+ * <code>
+ *    GridLayoutHelper.create(myComposite).noMargins().vSpacing(0).columns(2);
+ * </code>
+ */
+public final class GridLayoutBuilder {
+
+    private GridLayout mGL;
+
+    private GridLayoutBuilder() {
+        mGL = new GridLayout();
+    }
+
+    /**
+     * Creates new {@link GridLayout} and associates it on the <code>parent</code> composite.
+     */
+    static public GridLayoutBuilder create(Composite parent) {
+        GridLayoutBuilder glh = new GridLayoutBuilder();
+        parent.setLayout(glh.mGL);
+        return glh;
+    }
+
+    /** Sets all margins to 0. */
+    public GridLayoutBuilder noMargins() {
+        mGL.marginHeight = 0;
+        mGL.marginWidth = 0;
+        mGL.marginLeft = 0;
+        mGL.marginTop = 0;
+        mGL.marginRight = 0;
+        mGL.marginBottom = 0;
+        return this;
+    }
+
+    /** Sets all margins to <code>n</code>. */
+    public GridLayoutBuilder margins(int n) {
+        mGL.marginHeight = n;
+        mGL.marginWidth = n;
+        mGL.marginLeft = n;
+        mGL.marginTop = n;
+        mGL.marginRight = n;
+        mGL.marginBottom = n;
+        return this;
+    }
+
+    /** Sets <code>numColumns</code> to <code>n</code>. */
+    public GridLayoutBuilder columns(int n) {
+        mGL.numColumns = n;
+        return this;
+    }
+
+    /** Sets <code>makeColumnsEqualWidth</code> to true. */
+    public GridLayoutBuilder columnsEqual() {
+        mGL.makeColumnsEqualWidth = true;
+        return this;
+    }
+
+    /** Sets <code>verticalSpacing</code> to <code>v</code>. */
+    public GridLayoutBuilder vSpacing(int v) {
+        mGL.verticalSpacing = v;
+        return this;
+    }
+
+    /** Sets <code>horizontalSpacing</code> to <code>h</code>. */
+    public GridLayoutBuilder hSpacing(int h) {
+        mGL.horizontalSpacing = h;
+        return this;
+    }
+
+    /**
+     * Sets <code>horizontalSpacing</code> and <code>verticalSpacing</code>
+     * to <code>s</code>.
+     */
+    public GridLayoutBuilder spacing(int s) {
+        mGL.verticalSpacing = s;
+        mGL.horizontalSpacing = s;
+        return this;
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/ui/SwtBaseDialog.java b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/ui/SwtBaseDialog.java
new file mode 100755
index 0000000..bb0210b
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/main/java/com/android/sdkuilib/ui/SwtBaseDialog.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.ui;
+
+import com.android.SdkConstants;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A base class for an SWT Dialog.
+ * <p/>
+ * The base class offers the following goodies: <br/>
+ * - Dialog is automatically centered on its parent. <br/>
+ * - Dialog size is reused during the session. <br/>
+ * - A simple API with an {@link #open()} method that returns a boolean. <br/>
+ * <p/>
+ * A typical usage is:
+ * <pre>
+ *   MyDialog extends SwtBaseDialog { ... }
+ *   MyDialog d = new MyDialog(parentShell, "My Dialog Title");
+ *   if (d.open()) {
+ *      ...do something like refresh parent list view
+ *   }
+ * </pre>
+ * We also have a JFace-base {@link GridDialog}.
+ * The JFace dialog is good when you just want a typical OK/Cancel layout with the
+ * buttons all managed for you.
+ * This SWT base dialog has little decoration.
+ * It's up to you to manage whatever buttons you want, if any.
+ */
+public abstract class SwtBaseDialog extends Dialog {
+
+    /**
+     * Min Y location for dialog. Need to deal with the menu bar on mac os.
+     */
+    private final static int MIN_Y =
+        SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN ? 20 : 0;
+
+    /** Last dialog size for this session, different for each dialog class. */
+    private static Map<Class<?>, Point> sLastSizeMap = new HashMap<Class<?>, Point>();
+
+    private volatile boolean mQuitRequested = false;
+    private boolean mReturnValue;
+    private Shell mShell;
+
+    /**
+     * Create the dialog.
+     *
+     * @param parent The parent's shell
+     * @param title The dialog title. Can be null.
+     */
+    public SwtBaseDialog(Shell parent, int swtStyle, String title) {
+        super(parent, swtStyle);
+        if (title != null) {
+            setText(title);
+        }
+    }
+
+    /**
+     * Open the dialog.
+     *
+     * @return The last value set using {@link #setReturnValue(boolean)} or false by default.
+     */
+    public boolean open() {
+        if (!mQuitRequested) {
+            createShell();
+        }
+        if (!mQuitRequested) {
+            createContents();
+        }
+        if (!mQuitRequested) {
+            positionShell();
+        }
+        if (!mQuitRequested) {
+            postCreate();
+        }
+        if (!mQuitRequested) {
+            mShell.open();
+            mShell.layout();
+            eventLoop();
+        }
+
+        return mReturnValue;
+    }
+
+    /**
+     * Creates the shell for this dialog.
+     * The default shell has a size of 450x300, which is also its minimum size.
+     * You might want to override these values.
+     * <p/>
+     * Called before {@link #createContents()}.
+     */
+    protected void createShell() {
+        mShell = new Shell(getParent(), SWT.DIALOG_TRIM | SWT.RESIZE | SWT.APPLICATION_MODAL);
+        mShell.setMinimumSize(new Point(450, 300));
+        mShell.setSize(450, 300);
+        if (getText() != null) {
+            mShell.setText(getText());
+        }
+        mShell.addDisposeListener(new DisposeListener() {
+            @Override
+            public void widgetDisposed(DisposeEvent e) {
+                saveSize();
+            }
+        });
+    }
+
+    /**
+     * Creates the content and attaches it to the current shell (cf. {@link #getShell()}).
+     * <p/>
+     * Derived classes should consider creating the UI here and initializing their
+     * state in {@link #postCreate()}.
+     */
+    protected abstract void createContents();
+
+    /**
+     * Called after {@link #createContents()} and after {@link #positionShell()}
+     * just before the dialog is actually shown on screen.
+     * <p/>
+     * Derived classes should consider creating the UI in {@link #createContents()} and
+     * initialize it here.
+     */
+    protected abstract void postCreate();
+
+    /**
+     * Run the event loop.
+     * This is called from {@link #open()} after {@link #postCreate()} and
+     * after the window has been shown on screen.
+     * Derived classes might want to use this as a place to start automated
+     * tasks that will update the UI.
+     */
+    protected void eventLoop() {
+        Display display = getParent().getDisplay();
+        while (!mQuitRequested && !mShell.isDisposed()) {
+            if (!display.readAndDispatch()) {
+                display.sleep();
+            }
+        }
+    }
+
+    /**
+     * Returns the current value that {@link #open()} will return to the caller.
+     * Default is false.
+     */
+    protected boolean getReturnValue() {
+        return mReturnValue;
+    }
+
+    /**
+     * Sets the value that {@link #open()} will return to the caller.
+     * @param returnValue The new value to be returned by {@link #open()}.
+     */
+    protected void setReturnValue(boolean returnValue) {
+        mReturnValue = returnValue;
+    }
+
+    /**
+     * Returns the shell created by {@link #createShell()}.
+     * @return The current {@link Shell}.
+     */
+    protected Shell getShell() {
+        return mShell;
+    }
+
+    /**
+     * Saves the dialog size and close the dialog.
+     * The {@link #open()} method will given return value (see {@link #setReturnValue(boolean)}.
+     * <p/>
+     * It's safe to call this method before the shell is initialized,
+     * in which case the dialog will close as soon as possible.
+     */
+    protected void close() {
+        if (mShell != null && !mShell.isDisposed()) {
+            saveSize();
+            getShell().close();
+        }
+        mQuitRequested = true;
+    }
+
+    //-------
+
+    /**
+     * Centers the dialog in its parent shell.
+     */
+    private void positionShell() {
+        // Centers the dialog in its parent shell
+        Shell child = mShell;
+        Shell parent = getParent();
+        if (child != null && parent != null) {
+            // get the parent client area with a location relative to the display
+            Rectangle parentArea = parent.getClientArea();
+            Point parentLoc = parent.getLocation();
+            int px = parentLoc.x;
+            int py = parentLoc.y;
+            int pw = parentArea.width;
+            int ph = parentArea.height;
+
+            // Reuse the last size if there's one, otherwise use the default
+            Point childSize = sLastSizeMap.get(this.getClass());
+            if (childSize == null) {
+                childSize = child.getSize();
+            }
+            int cw = childSize.x;
+            int ch = childSize.y;
+
+            int x = px + (pw - cw) / 2;
+            if (x < 0) x = 0;
+
+            int y = py + (ph - ch) / 2;
+            if (y < MIN_Y) y = MIN_Y;
+
+            child.setLocation(x, y);
+            child.setSize(cw, ch);
+        }
+    }
+
+    private void saveSize() {
+        if (mShell != null && !mShell.isDisposed()) {
+            sLastSizeMap.put(this.getClass(), mShell.getSize());
+        }
+    }
+
+}
diff --git a/sdkmanager/sdkuilib/src/test/java/com/android/sdkuilib/internal/repository/MockSwtUpdaterData.java b/sdkmanager/sdkuilib/src/test/java/com/android/sdkuilib/internal/repository/MockSwtUpdaterData.java
new file mode 100755
index 0000000..e54a78c
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/test/java/com/android/sdkuilib/internal/repository/MockSwtUpdaterData.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository;
+
+import com.android.sdklib.SdkManager;
+import com.android.sdklib.internal.repository.DownloadCache;
+import com.android.sdklib.internal.repository.ITask;
+import com.android.sdklib.internal.repository.ITaskFactory;
+import com.android.sdklib.internal.repository.ITaskMonitor;
+import com.android.sdklib.internal.repository.MockDownloadCache;
+import com.android.sdklib.internal.repository.MockEmptySdkManager;
+import com.android.sdklib.internal.repository.NullTaskMonitor;
+import com.android.sdklib.internal.repository.archives.ArchiveInstaller;
+import com.android.sdklib.internal.repository.archives.ArchiveReplacement;
+import com.android.sdklib.internal.repository.sources.SdkSourceCategory;
+import com.android.sdklib.internal.repository.sources.SdkSources;
+import com.android.sdklib.internal.repository.updater.ArchiveInfo;
+import com.android.sdklib.internal.repository.updater.SettingsController;
+import com.android.sdklib.internal.repository.updater.SettingsController.Settings;
+import com.android.sdklib.mock.MockLog;
+import com.android.sdkuilib.internal.repository.icons.ImageFactory;
+import com.android.utils.ILogger;
+import com.android.utils.NullLogger;
+import com.google.common.collect.Lists;
+
+import org.eclipse.swt.graphics.Image;
+
+import java.util.List;
+import java.util.Properties;
+
+/** A mock UpdaterData that simply records what would have been installed. */
+public class MockSwtUpdaterData extends SwtUpdaterData {
+
+    public final static String SDK_PATH = "/tmp/SDK";
+
+    private DownloadCache mMockDownloadCache = new MockDownloadCache();
+    private final List<ArchiveReplacement> mInstalled = Lists.newArrayList();
+    private final SdkSources mMockSdkSources = new SdkSources() {
+        @Override
+        public void loadUserAddons(ILogger log) {
+            // This source does not load user addons.
+            removeAll(SdkSourceCategory.USER_ADDONS);
+        };
+    };
+
+    /** Creates a {@link MockSwtUpdaterData} using a {@link MockEmptySdkManager}. */
+    public MockSwtUpdaterData() {
+        super(SDK_PATH, new MockLog());
+
+        setTaskFactory(new MockTaskFactory());
+        setImageFactory(new NullImageFactory());
+    }
+
+    /** Creates a {@link MockSwtUpdaterData} using the given {@link SdkManager}. */
+    public MockSwtUpdaterData(SdkManager sdkManager) {
+        super(sdkManager.getLocation(), new MockLog());
+        setSdkManager(sdkManager);
+        setTaskFactory(new MockTaskFactory());
+        setImageFactory(new NullImageFactory());
+    }
+
+    /** Gives access to the internal {@link #installArchives(List, int)}. */
+    public void _installArchives(List<ArchiveInfo> result) {
+        installArchives(result, 0/*flags*/);
+    }
+
+    public ArchiveReplacement[] getInstalled() {
+        return mInstalled.toArray(new ArchiveReplacement[mInstalled.size()]);
+    }
+
+    /** Overrides the sdk manager with our mock instance. */
+    @Override
+    protected void initSdk() {
+        setSdkManager(new MockEmptySdkManager(SDK_PATH));
+    }
+
+    /** Overrides the settings controller with our mock instance. */
+    @Override
+    protected SettingsController initSettingsController() {
+        return createSettingsController(getSdkLog());
+    }
+
+    /** Override original implementation to do nothing. */
+    @Override
+    public void reloadSdk() {
+        // nop
+    }
+
+    /**
+     * Override original implementation to return a mock SdkSources that
+     * does not load user add-ons from the local .android/repository.cfg file.
+     */
+    @Override
+    public SdkSources getSources() {
+        return mMockSdkSources;
+    }
+
+    /** Returns a mock installer that simply records what would have been installed. */
+    @Override
+    protected ArchiveInstaller createArchiveInstaler() {
+        return new ArchiveInstaller() {
+            @Override
+            public boolean install(
+                    ArchiveReplacement archiveInfo,
+                    String osSdkRoot,
+                    boolean forceHttp,
+                    SdkManager sdkManager,
+                    DownloadCache cache,
+                    ITaskMonitor monitor) {
+                mInstalled.add(archiveInfo);
+                return true;
+            }
+        };
+    }
+
+    /** Returns a mock download cache. */
+    @Override
+    public DownloadCache getDownloadCache() {
+        return mMockDownloadCache;
+    }
+
+    /** Overrides the mock download cache. */
+    public void setMockDownloadCache(DownloadCache mockDownloadCache) {
+        mMockDownloadCache = mockDownloadCache;
+    }
+
+    public void overrideSetting(String key, boolean boolValue) {
+        SettingsController sc = getSettingsController();
+        assert sc instanceof MockSettingsController;
+        ((MockSettingsController)sc).overrideSetting(key, boolValue);
+    }
+
+    //------------
+
+    public static SettingsController createSettingsController(ILogger sdkLog) {
+        Properties props = new Properties();
+        Settings settings = new Settings(props) {};   // this constructor is protected
+        MockSettingsController controller = new MockSettingsController(sdkLog, settings);
+        controller.setProperties(props);
+        return controller;
+    }
+
+    static class MockSettingsController extends SettingsController {
+
+        private Properties mProperties;
+
+        MockSettingsController(ILogger sdkLog, Settings settings) {
+            super(sdkLog, settings);
+        }
+
+        void setProperties(Properties properties) {
+            mProperties = properties;
+        }
+
+        public void overrideSetting(String key, boolean boolValue) {
+            mProperties.setProperty(key, Boolean.valueOf(boolValue).toString());
+        }
+
+        @Override
+        public void loadSettings() {
+            // This mock setting controller does not load live file settings.
+        }
+
+        @Override
+        public void saveSettings() {
+            // This mock setting controller does not save live file settings.
+        }
+    }
+
+    //------------
+
+    private class MockTaskFactory implements ITaskFactory {
+        @Override
+        public void start(String title, ITask task) {
+            start(title, null /*parentMonitor*/, task);
+        }
+
+        @SuppressWarnings("unused") // works by side-effect of creating a new MockTask.
+        @Override
+        public void start(String title, ITaskMonitor parentMonitor, ITask task) {
+            new MockTask(task);
+        }
+    }
+
+    //------------
+
+    private static class MockTask extends NullTaskMonitor {
+        public MockTask(ITask task) {
+            super(NullLogger.getLogger());
+            task.run(this);
+        }
+    }
+
+    //------------
+
+    private static class NullImageFactory extends ImageFactory {
+        public NullImageFactory() {
+            // pass
+            super(null /*display*/);
+        }
+
+        @Override
+        public Image getImageByName(String imageName) {
+            return null;
+        }
+
+        @Override
+        public Image getImageForObject(Object object) {
+            return null;
+        }
+
+        @Override
+        public void dispose() {
+            // pass
+        }
+
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/test/java/com/android/sdkuilib/internal/repository/SdkUpdaterLogicTest.java b/sdkmanager/sdkuilib/src/test/java/com/android/sdkuilib/internal/repository/SdkUpdaterLogicTest.java
new file mode 100755
index 0000000..241ae6d
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/test/java/com/android/sdkuilib/internal/repository/SdkUpdaterLogicTest.java
@@ -0,0 +1,486 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository;
+
+import com.android.sdklib.SdkManager;
+import com.android.sdklib.internal.avd.AvdManager;
+import com.android.sdklib.internal.repository.DownloadCache;
+import com.android.sdklib.internal.repository.ITaskFactory;
+import com.android.sdklib.internal.repository.archives.Archive;
+import com.android.sdklib.internal.repository.packages.MockAddonPackage;
+import com.android.sdklib.internal.repository.packages.MockBrokenPackage;
+import com.android.sdklib.internal.repository.packages.MockPlatformPackage;
+import com.android.sdklib.internal.repository.packages.MockPlatformToolPackage;
+import com.android.sdklib.internal.repository.packages.MockToolPackage;
+import com.android.sdklib.internal.repository.packages.Package;
+import com.android.sdklib.internal.repository.sources.SdkSource;
+import com.android.sdklib.internal.repository.sources.SdkSources;
+import com.android.sdklib.internal.repository.updater.ArchiveInfo;
+import com.android.sdklib.internal.repository.updater.IUpdaterData;
+import com.android.sdklib.internal.repository.updater.SdkUpdaterLogic;
+import com.android.sdklib.internal.repository.updater.SettingsController;
+import com.android.sdklib.repository.FullRevision;
+import com.android.utils.ILogger;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import junit.framework.TestCase;
+
+public class SdkUpdaterLogicTest extends TestCase {
+
+    private static class NullUpdaterData implements IUpdaterData {
+
+        @Override
+        public AvdManager getAvdManager() {
+            return null;
+        }
+
+        @Override
+        public ILogger getSdkLog() {
+            return null;
+        }
+
+        @Override
+        public DownloadCache getDownloadCache() {
+            return null;
+        }
+
+        @Override
+        public SdkManager getSdkManager() {
+            return null;
+        }
+
+        @Override
+        public SettingsController getSettingsController() {
+            return null;
+        }
+
+        @Override
+        public ITaskFactory getTaskFactory() {
+            return null;
+        }
+
+    }
+
+    private static class MockSdkUpdaterLogic extends SdkUpdaterLogic {
+        private final Package[] mRemotePackages;
+
+        public MockSdkUpdaterLogic(IUpdaterData updaterData, Package[] remotePackages) {
+            super(updaterData);
+            mRemotePackages = remotePackages;
+        }
+
+        @Override
+        protected void fetchRemotePackages(Collection<Package> remotePkgs,
+                SdkSource[] remoteSources) {
+            // Ignore remoteSources and instead uses the remotePackages list given to the
+            // constructor.
+            if (mRemotePackages != null) {
+                remotePkgs.addAll(Arrays.asList(mRemotePackages));
+            }
+        }
+    }
+
+    /**
+     * Addon packages depend on a base platform package.
+     * This test checks that UpdaterLogic.findPlatformToolsDependency(...)
+     * can find the base platform for a given addon.
+     */
+    public void testFindAddonDependency() {
+        MockSdkUpdaterLogic mul = new MockSdkUpdaterLogic(new NullUpdaterData(), null);
+
+        MockPlatformPackage p1 = new MockPlatformPackage(1, 1);
+        MockPlatformPackage p2 = new MockPlatformPackage(2, 1);
+
+        MockAddonPackage a1 = new MockAddonPackage(p1, 1);
+        MockAddonPackage a2 = new MockAddonPackage(p2, 2);
+
+        ArrayList<ArchiveInfo> out = new ArrayList<ArchiveInfo>();
+        ArrayList<Archive> selected = new ArrayList<Archive>();
+        ArrayList<Package> remote = new ArrayList<Package>();
+
+        // a2 depends on p2, which is not in the locals
+        Package[] localPkgs = { p1, a1 };
+        ArchiveInfo[] locals = mul.createLocalArchives(localPkgs);
+
+        SdkSource[] sources = null;
+
+        // a2 now depends on a "fake" archive info with no newArchive that wraps the missing
+        // underlying platform.
+        ArchiveInfo fai = mul.findPlatformDependency(a2, out, selected, remote, sources, locals);
+        assertNotNull(fai);
+        assertNull(fai.getNewArchive());
+        assertTrue(fai.isRejected());
+        assertEquals(0, out.size());
+
+        // p2 is now selected, and should be scheduled for install in out
+        Archive p2_archive = p2.getArchives()[0];
+        selected.add(p2_archive);
+        ArchiveInfo ai2 = mul.findPlatformDependency(a2, out, selected, remote, sources, locals);
+        assertNotNull(ai2);
+        assertSame(p2_archive, ai2.getNewArchive());
+        assertEquals(1, out.size());
+        assertSame(p2_archive, out.get(0).getNewArchive());
+    }
+
+    /**
+     * Broken add-on packages require an exact platform package to be present or installed.
+     * This tests checks that findExactApiLevelDependency() can find a base
+     * platform package for a given broken add-on package.
+     */
+    public void testFindExactApiLevelDependency() {
+        MockSdkUpdaterLogic mul = new MockSdkUpdaterLogic(new NullUpdaterData(), null);
+
+        MockPlatformPackage p1 = new MockPlatformPackage(1, 1);
+        MockPlatformPackage p2 = new MockPlatformPackage(2, 1);
+
+        MockBrokenPackage a1 = new MockBrokenPackage(0, 1);
+        MockBrokenPackage a2 = new MockBrokenPackage(0, 2);
+
+        ArrayList<ArchiveInfo> out = new ArrayList<ArchiveInfo>();
+        ArrayList<Archive> selected = new ArrayList<Archive>();
+        ArrayList<Package> remote = new ArrayList<Package>();
+
+        // a2 depends on p2, which is not in the locals
+        Package[] localPkgs = { p1, a1 };
+        ArchiveInfo[] locals = mul.createLocalArchives(localPkgs);
+
+        SdkSource[] sources = null;
+
+        // a1 depends on p1, which can be found in the locals. p1 is already "installed"
+        // so we donn't need to suggest it as a dependency to solve any problem.
+        ArchiveInfo found = mul.findExactApiLevelDependency(
+                a1, out, selected, remote, sources, locals);
+        assertNull(found);
+
+        // a2 now depends on a "fake" archive info with no newArchive that wraps the missing
+        // underlying platform.
+        found = mul.findExactApiLevelDependency(a2, out, selected, remote, sources, locals);
+        assertNotNull(found);
+        assertNull(found.getNewArchive());
+        assertTrue(found.isRejected());
+        assertEquals(0, out.size());
+
+        // p2 is now selected, and should be scheduled for install in out
+        Archive p2_archive = p2.getArchives()[0];
+        selected.add(p2_archive);
+        found = mul.findExactApiLevelDependency(a2, out, selected, remote, sources, locals);
+        assertNotNull(found);
+        assertSame(p2_archive, found.getNewArchive());
+        assertEquals(1, out.size());
+        assertSame(p2_archive, out.get(0).getNewArchive());
+    }
+
+    /**
+     * Platform packages depend on a tool package.
+     * This tests checks that UpdaterLogic.findToolsDependency() can find a base
+     * tool package for a given platform package.
+     */
+    public void testFindPlatformDependency() {
+        MockSdkUpdaterLogic mul = new MockSdkUpdaterLogic(new NullUpdaterData(), null);
+
+        MockPlatformToolPackage pt1 = new MockPlatformToolPackage(1);
+
+        MockToolPackage t1 = new MockToolPackage(1, 1);
+        MockToolPackage t2 = new MockToolPackage(2, 1);
+
+        MockPlatformPackage p2 = new MockPlatformPackage(2, 1, 2);
+
+        ArrayList<ArchiveInfo> out = new ArrayList<ArchiveInfo>();
+        ArrayList<Archive> selected = new ArrayList<Archive>();
+        ArrayList<Package> remote = new ArrayList<Package>();
+
+        // p2 depends on t2, which is not locally installed
+        Package[] localPkgs = { t1, pt1 };
+        ArchiveInfo[] locals = mul.createLocalArchives(localPkgs);
+
+        SdkSource[] sources = null;
+
+        // p2 now depends on a "fake" archive info with no newArchive that wraps the missing
+        // underlying tool
+        ArchiveInfo fai = mul.findToolsDependency(p2, out, selected, remote, sources, locals);
+        assertNotNull(fai);
+        assertNull(fai.getNewArchive());
+        assertTrue(fai.isRejected());
+        assertEquals(0, out.size());
+
+        // t2 is now selected and can be used as a dependency
+        Archive t2_archive = t2.getArchives()[0];
+        selected.add(t2_archive);
+        ArchiveInfo ai2 = mul.findToolsDependency(p2, out, selected, remote, sources, locals);
+        assertNotNull(ai2);
+        assertSame(t2_archive, ai2.getNewArchive());
+        assertEquals(1, out.size());
+        assertSame(t2_archive, out.get(0).getNewArchive());
+    }
+
+    /**
+     * Tool packages require a platform-tool package to be present or installed.
+     * This tests checks that UpdaterLogic.findPlatformToolsDependency() can find a base
+     * platform-tool package for a given tool package.
+     */
+    public void testFindPlatformToolDependency() {
+        MockSdkUpdaterLogic mul = new MockSdkUpdaterLogic(new NullUpdaterData(), null);
+
+        MockPlatformToolPackage t1 = new MockPlatformToolPackage(1);
+        MockPlatformToolPackage t2 = new MockPlatformToolPackage(2);
+
+        MockToolPackage p2 = new MockToolPackage(2, 2);
+
+        ArrayList<ArchiveInfo> out = new ArrayList<ArchiveInfo>();
+        ArrayList<Archive> selected = new ArrayList<Archive>();
+        ArrayList<Package> remote = new ArrayList<Package>();
+
+        // p2 depends on t2, which is not locally installed
+        Package[] localPkgs = { t1 };
+        ArchiveInfo[] locals = mul.createLocalArchives(localPkgs);
+
+        SdkSource[] sources = null;
+
+        // p2 now depends on a "fake" archive info with no newArchive that wraps the missing
+        // underlying tool
+        ArchiveInfo fai = mul.findPlatformToolsDependency(
+                                    p2, out, selected, remote, sources, locals);
+        assertNotNull(fai);
+        assertNull(fai.getNewArchive());
+        assertTrue(fai.isRejected());
+        assertEquals(0, out.size());
+
+        // t2 is now selected and can be used as a dependency
+        Archive t2_archive = t2.getArchives()[0];
+        selected.add(t2_archive);
+        ArchiveInfo ai2 = mul.findPlatformToolsDependency(
+                                    p2, out, selected, remote, sources, locals);
+        assertNotNull(ai2);
+        assertSame(t2_archive, ai2.getNewArchive());
+        assertEquals(1, out.size());
+        assertSame(t2_archive, out.get(0).getNewArchive());
+    }
+
+    public void testComputeRevisionUpdate() {
+        // Scenario:
+        // - user has tools rev 7 installed + plat-tools rev 1 installed
+        // - server has tools rev 8, depending on plat-tools rev 2
+        // - server has tools rev 9, depending on plat-tools rev 3
+        // - server has platform 9 that requires min-tools-rev 9
+        //
+        // If we do an update all, we want to the installer to pick up:
+        // - the new platform 9
+        // - the tools rev 9 (required by platform 9)
+        // - the plat-tools rev 3 (required by tools rev 9)
+
+        final MockPlatformToolPackage pt1 = new MockPlatformToolPackage(1);
+        final MockPlatformToolPackage pt2 = new MockPlatformToolPackage(2);
+        final MockPlatformToolPackage pt3 = new MockPlatformToolPackage(3);
+
+        final MockToolPackage t7 = new MockToolPackage(7, 1 /*min-plat-tools*/);
+        final MockToolPackage t8 = new MockToolPackage(8, 2 /*min-plat-tools*/);
+        final MockToolPackage t9 = new MockToolPackage(9, 3 /*min-plat-tools*/);
+
+        final MockPlatformPackage p9 = new MockPlatformPackage(9, 1, 9 /*min-tools*/);
+
+        // Note: the mock updater logic gets the remotes packages from the array given
+        // here and bypasses the source (to avoid fetching any actual URLs)
+        MockSdkUpdaterLogic mul = new MockSdkUpdaterLogic(new NullUpdaterData(),
+                new Package[] { t8, pt2, t9, pt3, p9 });
+
+        SdkSources sources = new SdkSources();
+        Package[] localPkgs = { t7, pt1 };
+
+        List<ArchiveInfo> selected = mul.computeUpdates(
+                null /*selectedArchives*/,
+                sources,
+                localPkgs,
+                false /*includeObsoletes*/);
+
+        assertEquals(
+                "[Android SDK Platform-tools, revision 3, " +
+                 "Android SDK Tools, revision 9]",
+                Arrays.toString(selected.toArray()));
+
+        mul.addNewPlatforms(
+                selected,
+                sources,
+                localPkgs,
+                false /*includeObsoletes*/);
+
+        assertEquals(
+                "[Android SDK Platform-tools, revision 3, " +
+                 "Android SDK Tools, revision 9, " +
+                 "SDK Platform Android android-9, API 9, revision 1]",
+                Arrays.toString(selected.toArray()));
+
+        // Now try again but reverse the order of the remote package list.
+
+        mul = new MockSdkUpdaterLogic(new NullUpdaterData(),
+                new Package[] { p9, t9, pt3, t8, pt2 });
+
+        selected = mul.computeUpdates(
+                null /*selectedArchives*/,
+                sources,
+                localPkgs,
+                false /*includeObsoletes*/);
+
+        assertEquals(
+                "[Android SDK Platform-tools, revision 3, " +
+                 "Android SDK Tools, revision 9]",
+                Arrays.toString(selected.toArray()));
+
+        mul.addNewPlatforms(
+                selected,
+                sources,
+                localPkgs,
+                false /*includeObsoletes*/);
+
+        assertEquals(
+                "[Android SDK Platform-tools, revision 3, " +
+                 "Android SDK Tools, revision 9, " +
+                 "SDK Platform Android android-9, API 9, revision 1]",
+                Arrays.toString(selected.toArray()));
+    }
+
+    public void testComputeRevisionUpdate2() {
+        // Scenario:
+        // - user has tools rev 2 installed and NO platform-tools
+        // - server has platform tools 1 rc 1 (a preview) and 2.
+        // - server has platform 2 that requires min-tools 2 that requires min-plat-tools 1rc1.
+        //
+        // One issue is that when there was only one instance of platform-tools possible,
+        // the computeUpdates() code would pick the first one. But now there can be 2 of
+        // them (preview, non-preview) and thus we need to pick up the higher one even if
+        // it's not the first choice.
+
+        final MockPlatformToolPackage pt1rc = new MockPlatformToolPackage(
+                                                    null,
+                                                    new FullRevision(1, 0, 0, 1));
+        final MockPlatformToolPackage pt2 = new MockPlatformToolPackage(2);
+
+        // Tools rev 2 requires at least plat-tools 1rc1
+        final MockToolPackage t2 = new MockToolPackage(null,
+                                                       new FullRevision(2),           // tools rev
+                                                       new FullRevision(1, 0, 0, 1)); // min-pt-rev
+
+        final MockPlatformPackage p2 = new MockPlatformPackage(2, 1, 2 /*min-tools*/);
+
+        // Note: the mock updater logic gets the remotes packages from the array given
+        // here and bypasses the source (to avoid fetching any actual URLs)
+        // Remote available packages include both plat-tools 1rc1 and 2.
+        //
+        // Order DOES matter: the issue is that computeUpdates was selecting the first platform
+        // tools (so 1rc1) and ignoring the newer revision 2 because originally there could be
+        // only one platform-tool definition. Now with previews we can have 2 and we need to
+        // select the higher one even if it's not the first choice.
+        MockSdkUpdaterLogic mul = new MockSdkUpdaterLogic(new NullUpdaterData(),
+                new Package[] { t2, pt1rc, pt2, p2 });
+
+        // Local packages only have tools 2.
+        SdkSources sources = new SdkSources();
+        Package[] localPkgs = { t2 };
+        List<Archive> selectedArchives = Arrays.asList( p2.getArchives() );
+
+        List<ArchiveInfo> selected = mul.computeUpdates(
+                selectedArchives,
+                sources,
+                localPkgs,
+                false /*includeObsoletes*/);
+
+        assertEquals(
+                "[SDK Platform Android android-2, API 2, revision 1, " +
+                 "Android SDK Platform-tools, revision 2]",
+                Arrays.toString(selected.toArray()));
+
+        mul.addNewPlatforms(
+                selected,
+                sources,
+                localPkgs,
+                false /*includeObsoletes*/);
+
+        assertEquals(
+                "[SDK Platform Android android-2, API 2, revision 1, " +
+                 "Android SDK Platform-tools, revision 2]",
+                Arrays.toString(selected.toArray()));
+    }
+
+    public void testComputeRevisionUpdate3() {
+        // Scenario:
+        // - user has tools rev 2 installed and NO platform-tools
+        // - server has platform tools 1 rc 1 (a preview) and 2.
+        // - server has platform 2 that requires min-tools 2 that requires min-plat-tools 1rc1.
+        //
+        // One issue is that when there was only one instance of tools possible,
+        // the computeUpdates() code would pick the first one. But now there can be 2 of
+        // them (preview, non-preview) and thus we need to pick up the higher one even if
+        // it's not the first choice.
+
+        final MockPlatformToolPackage pt1rc = new MockPlatformToolPackage(
+                                                    null,
+                                                    new FullRevision(1, 0, 0, 1));
+        final MockPlatformToolPackage pt2 = new MockPlatformToolPackage(2);
+
+        // Tools rev 1rc1 requires plat-tools 1rc1, and tools 2 requires plat-tools 2.
+        final MockToolPackage t1rc = new MockToolPackage(null,
+                                                       new FullRevision(1, 0, 0, 1),  // tools rev
+                                                       new FullRevision(1, 0, 0, 1)); // min-pt-rev
+        final MockToolPackage t2 = new MockToolPackage(null, 2, 2);
+
+        // Platform depends on min-tools 1rc1, so any of tools 1rc1 or 2 would satisfy.
+        final MockPlatformPackage p2 = new MockPlatformPackage(2, 1, new FullRevision(1, 0, 0, 1));
+
+        // Note: the mock updater logic gets the remotes packages from the array given
+        // here and bypasses the source (to avoid fetching any actual URLs)
+        // Remote available packages include both plat-tools 1rc1 and 2.
+        //
+        // Order DOES matter: the issue is that computeUpdates was selecting the first tools (1rc1)
+        // and ignoring the newer revision 2 because originally there could be only one tool
+        // definition. Now with previews we can have 2 and we need to select the higher version
+        // available even if it's not the first choice.
+        MockSdkUpdaterLogic mul = new MockSdkUpdaterLogic(new NullUpdaterData(),
+                new Package[] { t1rc, pt1rc, t2, pt2, p2 });
+
+        // Local packages only have tools 2.
+        SdkSources sources = new SdkSources();
+        Package[] localPkgs = {  };
+        List<Archive> selectedArchives = Arrays.asList( p2.getArchives() );
+
+        List<ArchiveInfo> selected = mul.computeUpdates(
+                selectedArchives,
+                sources,
+                localPkgs,
+                false /*includeObsoletes*/);
+
+        assertEquals(
+                "[Android SDK Platform-tools, revision 2, " +
+                 "Android SDK Tools, revision 2, " +
+                 "SDK Platform Android android-2, API 2, revision 1]",
+                Arrays.toString(selected.toArray()));
+
+        mul.addNewPlatforms(
+                selected,
+                sources,
+                localPkgs,
+                false /*includeObsoletes*/);
+
+        assertEquals(
+                "[Android SDK Platform-tools, revision 2, " +
+                 "Android SDK Tools, revision 2, " +
+                 "SDK Platform Android android-2, API 2, revision 1]",
+                Arrays.toString(selected.toArray()));
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/test/java/com/android/sdkuilib/internal/repository/UpdaterDataTest.java b/sdkmanager/sdkuilib/src/test/java/com/android/sdkuilib/internal/repository/UpdaterDataTest.java
new file mode 100755
index 0000000..1212235
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/test/java/com/android/sdkuilib/internal/repository/UpdaterDataTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository;
+
+import com.android.sdklib.internal.repository.archives.Archive;
+import com.android.sdklib.internal.repository.packages.MockEmptyPackage;
+import com.android.sdklib.internal.repository.updater.ArchiveInfo;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import junit.framework.TestCase;
+
+public class UpdaterDataTest extends TestCase {
+
+    private MockSwtUpdaterData m;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        m = new MockSwtUpdaterData();
+        assertEquals("[]", Arrays.toString(m.getInstalled()));
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    /**
+     * Tests the case where we have nothing to install.
+     */
+    public void testInstallArchives_None() {
+        m._installArchives(new ArrayList<ArchiveInfo>());
+        assertEquals("[]", Arrays.toString(m.getInstalled()));
+    }
+
+
+    /**
+     * Tests the case where there's a simple dependency, in the right order
+     * (e.g. install A1 then A2 that depends on A1).
+     */
+    public void testInstallArchives_SimpleDependency() {
+
+        ArrayList<ArchiveInfo> archives = new ArrayList<ArchiveInfo>();
+
+        Archive a1 = new MockEmptyPackage("a1").getLocalArchive();
+        ArchiveInfo ai1 = new ArchiveInfo(a1, null, null);
+
+        Archive a2 = new MockEmptyPackage("a2").getLocalArchive();
+        ArchiveInfo ai2 = new ArchiveInfo(a2, null, new ArchiveInfo[] { ai1 } );
+
+        archives.add(ai1);
+        archives.add(ai2);
+
+        m._installArchives(archives);
+        assertEquals(
+                "[MockEmptyPackage 'a1', MockEmptyPackage 'a2']",
+                Arrays.toString(m.getInstalled()));
+    }
+
+    /**
+     * Tests the case where there's a simple dependency, in the wrong order
+     * (e.g. install A2 then A1 which A2 depends on)
+     */
+    public void testInstallArchives_ReverseDependency() {
+
+        ArrayList<ArchiveInfo> archives = new ArrayList<ArchiveInfo>();
+
+        Archive a1 = new MockEmptyPackage("a1").getLocalArchive();
+        ArchiveInfo ai1 = new ArchiveInfo(a1, null, null);
+
+        Archive a2 = new MockEmptyPackage("a2").getLocalArchive();
+        ArchiveInfo ai2 = new ArchiveInfo(a2, null, new ArchiveInfo[] { ai1 } );
+
+        archives.add(ai2);
+        archives.add(ai1);
+
+        m._installArchives(archives);
+        assertEquals(
+                "[MockEmptyPackage 'a1', MockEmptyPackage 'a2']",
+                Arrays.toString(m.getInstalled()));
+    }
+
+}
diff --git a/sdkmanager/sdkuilib/src/test/java/com/android/sdkuilib/internal/repository/core/PackagesDiffLogicTest.java b/sdkmanager/sdkuilib/src/test/java/com/android/sdkuilib/internal/repository/core/PackagesDiffLogicTest.java
new file mode 100755
index 0000000..c4e3a81
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/test/java/com/android/sdkuilib/internal/repository/core/PackagesDiffLogicTest.java
@@ -0,0 +1,1948 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository.core;
+
+import com.android.SdkConstants;
+import com.android.sdklib.internal.repository.packages.BrokenPackage;
+import com.android.sdklib.internal.repository.packages.MockAddonPackage;
+import com.android.sdklib.internal.repository.packages.MockBrokenPackage;
+import com.android.sdklib.internal.repository.packages.MockBuildToolPackage;
+import com.android.sdklib.internal.repository.packages.MockEmptyPackage;
+import com.android.sdklib.internal.repository.packages.MockExtraPackage;
+import com.android.sdklib.internal.repository.packages.MockPlatformPackage;
+import com.android.sdklib.internal.repository.packages.MockPlatformToolPackage;
+import com.android.sdklib.internal.repository.packages.MockSystemImagePackage;
+import com.android.sdklib.internal.repository.packages.MockToolPackage;
+import com.android.sdklib.internal.repository.packages.Package;
+import com.android.sdklib.internal.repository.sources.SdkRepoSource;
+import com.android.sdklib.internal.repository.sources.SdkSource;
+import com.android.sdklib.internal.repository.updater.ISettingsPage;
+import com.android.sdklib.internal.repository.updater.PkgItem;
+import com.android.sdklib.repository.FullRevision;
+import com.android.sdklib.repository.PkgProps;
+import com.android.sdkuilib.internal.repository.MockSwtUpdaterData;
+
+import java.util.Properties;
+
+import junit.framework.TestCase;
+
+public class PackagesDiffLogicTest extends TestCase {
+
+    private PackagesDiffLogic m;
+    private MockSwtUpdaterData u;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        u = new MockSwtUpdaterData();
+        m = new PackagesDiffLogic(u);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    // ----
+    //
+    // Test Details Note: the way load is implemented in PackageLoader, the
+    // loader processes each source and then for each source the packages are added
+    // to a list and the sorting algorithm is called with that list. Thus for
+    // one load, many calls to the sortByX/Y happen, with the list progressively
+    // being populated.
+    // However when the user switches sorting algorithm, the package list is not
+    // reloaded and is processed at once.
+
+    public void testSortByApi_Empty() {
+        m.updateStart();
+        assertFalse(m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[0]));
+        assertFalse(m.updateEnd(true /*sortByApi*/));
+
+        // We also keep these 2 categories even if they contain nothing
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=0>\n",
+               getTree(m, true /*displaySortByApi*/));
+    }
+
+    public void testSortByApi_AddSamePackage() {
+        SdkSource src1 = new SdkRepoSource("http://example.com/url", "repo1");
+
+        m.updateStart();
+        // First insert local packages
+        assertTrue(m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "some pkg", 1)
+        }));
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'some pkg' rev=1>\n",
+                getTree(m, true /*displaySortByApi*/));
+
+        // Insert the next source
+        // Same package as the one installed, so we don't display it
+        assertFalse(m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockEmptyPackage(src1, "some pkg", 1)
+        }));
+
+        assertFalse(m.updateEnd(true /*sortByApi*/));
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'some pkg' rev=1>\n",
+                getTree(m, true /*displaySortByApi*/));
+    }
+
+    public void testSortByApi_AddOtherPackage() {
+        SdkSource src1 = new SdkRepoSource("http://example.com/url", "repo1");
+
+        m.updateStart();
+        // First insert local packages
+        assertTrue(m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "some pkg", 1)
+        }));
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'some pkg' rev=1>\n",
+                getTree(m, true /*displaySortByApi*/));
+
+        // Insert the next source
+        // Not the same package as the one installed, so we'll display it
+        assertTrue(m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockEmptyPackage(src1, "other pkg", 1)
+        }));
+
+        assertFalse(m.updateEnd(true /*sortByApi*/));
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=2>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'some pkg' rev=1>\n" +
+                "-- <NEW, pkg:MockEmptyPackage 'other pkg' rev=1>\n",
+                getTree(m, true /*displaySortByApi*/));
+    }
+
+    public void testSortByApi_Update1() {
+        SdkSource src1 = new SdkRepoSource("http://example.com/url", "repo1");
+
+        // Typical case: user has a locally installed package in revision 1
+        // The display list after sort should show that installed package.
+        m.updateStart();
+        // First insert local packages
+        assertTrue(m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1)
+        }));
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1>\n",
+                getTree(m, true /*displaySortByApi*/));
+
+        assertTrue(m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 4),
+                new MockEmptyPackage(src1, "type1", 2)
+        }));
+
+        assertFalse(m.updateEnd(true /*sortByApi*/));
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=4>\n",
+                getTree(m, true /*displaySortByApi*/));
+    }
+
+    public void testSortByApi_Reload() {
+        SdkSource src1 = new SdkRepoSource("http://example.com/url", "repo1");
+
+        // First load reveals a package local package and its update
+        m.updateStart();
+        // First insert local packages
+        assertTrue(m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1)
+        }));
+        assertTrue(m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 2)
+        }));
+
+        assertFalse(m.updateEnd(true /*sortByApi*/));
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n",
+                getTree(m, true /*displaySortByApi*/));
+
+        // Now simulate a reload that clears the package list and creates similar
+        // objects but not the same references. The only difference is that updateXyz
+        // returns false since nothing changes.
+
+        m.updateStart();
+        // First insert local packages
+        assertFalse(m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1)
+        }));
+        assertFalse(m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 2)
+        }));
+
+        assertFalse(m.updateEnd(true /*sortByApi*/));
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n",
+                getTree(m, true /*displaySortByApi*/));
+    }
+
+    public void testSortByApi_InstallPackage() {
+        SdkSource src1 = new SdkRepoSource("http://example.com/url", "repo1");
+
+        // First load reveals a new package
+        m.updateStart();
+        // No local packages at first
+        assertFalse(m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[0]));
+        assertTrue(m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1)
+        }));
+
+        assertFalse(m.updateEnd(true /*sortByApi*/));
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <NEW, pkg:MockEmptyPackage 'type1' rev=1>\n",
+                getTree(m, true /*displaySortByApi*/));
+
+        // Install it.
+        m.updateStart();
+        // local packages
+        assertTrue(m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1)
+        }));
+        assertFalse(m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1)
+        }));
+
+        assertTrue(m.updateEnd(true /*sortByApi*/));
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1>\n",
+                getTree(m, true /*displaySortByApi*/));
+
+        // Load reveals an update
+        m.updateStart();
+        // local packages
+        assertFalse(m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1)
+        }));
+        assertTrue(m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 2)
+        }));
+
+        assertFalse(m.updateEnd(true /*sortByApi*/));
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n",
+                getTree(m, true /*displaySortByApi*/));
+    }
+
+    public void testSortByApi_DeletePackage() {
+        SdkSource src1 = new SdkRepoSource("http://example.com/url", "repo1");
+
+        // We have an installed package
+        m.updateStart();
+        // local packages
+        assertTrue(m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1)
+        }));
+        assertTrue(m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 2)
+        }));
+
+        assertFalse(m.updateEnd(true /*sortByApi*/));
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n",
+                getTree(m, true /*displaySortByApi*/));
+
+        // User now deletes the installed package.
+        m.updateStart();
+        // No local packages
+        assertTrue(m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[0]));
+        assertTrue(m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1)
+        }));
+
+        assertFalse(m.updateEnd(true /*sortByApi*/));
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <NEW, pkg:MockEmptyPackage 'type1' rev=1>\n",
+                getTree(m, true /*displaySortByApi*/));
+    }
+
+    public void testSortByApi_NoRemoteSources() {
+        SdkSource src1 = new SdkRepoSource("http://example.com/url1", "repo1");
+        SdkSource src2 = new SdkRepoSource("http://example.com/url2", "repo2");
+
+        // We have a couple installed packages
+        m.updateStart();
+        // local packages
+        assertTrue(m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockToolPackage(src1, 10, 3),
+                new MockPlatformToolPackage(src1, 3),
+                new MockExtraPackage(src2, "carrier", "custom_rom", 1, 0),
+                new MockExtraPackage(src2, "android", "usb_driver", 5, 3),
+        }));
+        // and no remote sources have been loaded (e.g. because there's no network)
+        assertFalse(m.updateEnd(true /*sortByApi*/));
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=2>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 10>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=2>\n" +
+                "-- <INSTALLED, pkg:Android USB Driver, revision 5>\n" +
+                "-- <INSTALLED, pkg:Carrier Custom Rom, revision 1>\n",
+                getTree(m, true /*displaySortByApi*/));
+
+        assertEquals(
+                "PkgCategorySource <source=repo1 (example.com), #items=2>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 10>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3>\n" +
+                "PkgCategorySource <source=repo2 (example.com), #items=2>\n" +
+                "-- <INSTALLED, pkg:Android USB Driver, revision 5>\n" +
+                "-- <INSTALLED, pkg:Carrier Custom Rom, revision 1>\n",
+                getTree(m, false /*displaySortByApi*/));
+    }
+
+    public void testSortByApi_CompleteUpdate() {
+        SdkSource src1 = new SdkRepoSource("http://1.example.com/url1", "repo1");
+        SdkSource src2 = new SdkRepoSource("http://2.example.com/url2", "repo2");
+
+        // Resulting categories are sorted by Tools, descending platform API and finally Extras.
+        // Addons are sorted by name within their API.
+        // Extras are sorted by vendor name.
+        // The order packages are added to the mAllPkgItems list is purposedly different from
+        // the final order we get.
+
+        // First update has the typical tools and a couple extras
+        m.updateStart();
+
+        assertTrue(m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockToolPackage(src1, 10, 3),
+                new MockPlatformToolPackage(src1, 3),
+                new MockExtraPackage(src1, "android", "usb_driver", 4, 3),
+        }));
+        assertTrue(m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockToolPackage(src1, 10, 3),
+                new MockPlatformToolPackage(src1, 3),
+                new MockExtraPackage(src1, "carrier", "custom_rom", 1, 0),
+                new MockExtraPackage(src1, "android", "usb_driver", 5, 3),
+        }));
+        assertFalse(m.updateEnd(true /*sortByApi*/));
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=2>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 10>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=2>\n" +
+                "-- <INSTALLED, pkg:Android USB Driver, revision 4, updated by:Android USB Driver, revision 5>\n" +
+                "-- <NEW, pkg:Carrier Custom Rom, revision 1>\n",
+                getTree(m, true /*displaySortByApi*/));
+
+        // Next update adds platforms and addon, sorted in a category based on their API level
+        m.updateStart();
+        MockPlatformPackage p1;
+        MockPlatformPackage p2;
+        @SuppressWarnings("unused") // keep p3 for clarity
+        MockPlatformPackage p3;
+
+        assertTrue(m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockToolPackage(src1, 10, 3),
+                new MockPlatformToolPackage(src1, 3),
+                new MockExtraPackage(src1, "android", "usb_driver", 4, 3),
+                // second update
+                p1 = new MockPlatformPackage(src1, 1, 2, 3),  // API 1
+                p3 = new MockPlatformPackage(src1, 3, 6, 3),
+                new MockAddonPackage(src2, "addon A", p1, 5),
+                new MockAddonPackage(src2, "addon D", p1, 10),
+        }));
+        assertTrue(m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockToolPackage(src1, 10, 3),
+                new MockPlatformToolPackage(src1, 3),
+                new MockExtraPackage(src1, "carrier", "custom_rom", 1, 0),
+                new MockExtraPackage(src1, "android", "usb_driver", 5, 3),
+                // second update
+                p2 = new MockPlatformPackage(src1, 2, 4, 3),    // API 2
+        }));
+        assertTrue(m.updateSourcePackages(true /*sortByApi*/, src2, new Package[] {
+                new MockAddonPackage(src2, "addon C", p2, 9),
+                new MockAddonPackage(src2, "addon A", p1, 6),
+                // the rev 7+8 will be ignored since there's a rev 9 coming after
+                new MockAddonPackage(src2, "addon B", p2, 7),
+                new MockAddonPackage(src2, "addon B", p2, 8),
+                new MockAddonPackage(src2, "addon B", p2, 9),
+                // 11+12 should be ignored updates, 13 will update 10
+                new MockAddonPackage(src2, "addon D", p1, 10),
+                new MockAddonPackage(src2, "addon D", p1, 12),  // note: 12 listed before 11
+                new MockAddonPackage(src2, "addon D", p1, 11),
+                new MockAddonPackage(src2, "addon D", p1, 13),
+        }));
+        assertFalse(m.updateEnd(true /*sortByApi*/));
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=2>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 10>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3>\n" +
+                "PkgCategoryApi <API=API 3, label=Android android-3 (API 3), #items=1>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-3, API 3, revision 6>\n" +
+                "PkgCategoryApi <API=API 2, label=Android android-2 (API 2), #items=3>\n" +
+                "-- <NEW, pkg:SDK Platform Android android-2, API 2, revision 4>\n" +
+                "-- <NEW, pkg:The addon B from vendor 2, Android API 2, revision 9>\n" +
+                "-- <NEW, pkg:The addon C from vendor 2, Android API 2, revision 9>\n" +
+                "PkgCategoryApi <API=API 1, label=Android android-1 (API 1), #items=3>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n" +
+                "-- <INSTALLED, pkg:The addon A from vendor 1, Android API 1, revision 5, updated by:The addon A from vendor 1, Android API 1, revision 6>\n" +
+                "-- <INSTALLED, pkg:The addon D from vendor 1, Android API 1, revision 10, updated by:The addon D from vendor 1, Android API 1, revision 13>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=2>\n" +
+                "-- <INSTALLED, pkg:Android USB Driver, revision 4, updated by:Android USB Driver, revision 5>\n" +
+                "-- <NEW, pkg:Carrier Custom Rom, revision 1>\n",
+                getTree(m, true /*displaySortByApi*/));
+
+        // Reloading the same thing should have no impact except for the update methods
+        // returning false when they don't change the current list.
+        m.updateStart();
+
+        assertFalse(m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockToolPackage(src1, 10, 3),
+                new MockPlatformToolPackage(src1, 3),
+                new MockExtraPackage(src1, "android", "usb_driver", 4, 3),
+                // second update
+                p1 = new MockPlatformPackage(src1, 1, 2, 3),
+                p3 = new MockPlatformPackage(src1, 3, 6, 3),
+                new MockAddonPackage(src2, "addon A", p1, 5),
+                new MockAddonPackage(src2, "addon D", p1, 10),
+        }));
+        assertFalse(m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockToolPackage(src1, 10, 3),
+                new MockPlatformToolPackage(src1, 3),
+                new MockExtraPackage(src1, "carrier", "custom_rom", 1, 0),
+                new MockExtraPackage(src1, "android", "usb_driver", 5, 3),
+                // second update
+                p2 = new MockPlatformPackage(src1, 2, 4, 3),
+        }));
+        assertTrue(m.updateSourcePackages(true /*sortByApi*/, src2, new Package[] {
+                new MockAddonPackage(src2, "addon C", p2, 9),
+                new MockAddonPackage(src2, "addon A", p1, 6),
+                // the rev 7+8 will be ignored since there's a rev 9 coming after
+                new MockAddonPackage(src2, "addon B", p2, 7),
+                new MockAddonPackage(src2, "addon B", p2, 8),
+                new MockAddonPackage(src2, "addon B", p2, 9),
+                // 11+12 should be ignored updates, 13 will update 10
+                new MockAddonPackage(src2, "addon D", p1, 10),
+                new MockAddonPackage(src2, "addon D", p1, 12),  // note: 12 listed before 11
+                new MockAddonPackage(src2, "addon D", p1, 11),
+                new MockAddonPackage(src2, "addon D", p1, 13),
+        }));
+        assertFalse(m.updateEnd(true /*sortByApi*/));
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=2>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 10>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3>\n" +
+                "PkgCategoryApi <API=API 3, label=Android android-3 (API 3), #items=1>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-3, API 3, revision 6>\n" +
+                "PkgCategoryApi <API=API 2, label=Android android-2 (API 2), #items=3>\n" +
+                "-- <NEW, pkg:SDK Platform Android android-2, API 2, revision 4>\n" +
+                "-- <NEW, pkg:The addon B from vendor 2, Android API 2, revision 9>\n" +
+                "-- <NEW, pkg:The addon C from vendor 2, Android API 2, revision 9>\n" +
+                "PkgCategoryApi <API=API 1, label=Android android-1 (API 1), #items=3>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n" +
+                "-- <INSTALLED, pkg:The addon A from vendor 1, Android API 1, revision 5, updated by:The addon A from vendor 1, Android API 1, revision 6>\n" +
+                "-- <INSTALLED, pkg:The addon D from vendor 1, Android API 1, revision 10, updated by:The addon D from vendor 1, Android API 1, revision 13>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=2>\n" +
+                "-- <INSTALLED, pkg:Android USB Driver, revision 4, updated by:Android USB Driver, revision 5>\n" +
+                "-- <NEW, pkg:Carrier Custom Rom, revision 1>\n",
+                getTree(m, true /*displaySortByApi*/));
+    }
+
+    // ----
+
+    public void testSortBySource_Empty() {
+        m.updateStart();
+        assertFalse(m.updateSourcePackages(false /*sortByApi*/, null /*locals*/, new Package[0]));
+        // UpdateEnd returns true since it removed the synthetic "unknown source" category
+        assertTrue(m.updateEnd(false /*sortByApi*/));
+
+        assertTrue(m.getCategories(false /*sortByApi*/).isEmpty());
+
+        assertEquals(
+                "",
+                getTree(m, false /*displaySortByApi*/));
+    }
+
+    public void testSortBySource_AddPackages() {
+        // Since we're sorting by source, items are grouped under their source
+        // even if installed. The 'local' source is only for installed items for
+        // which we don't know the source.
+        SdkSource src1 = new SdkRepoSource("http://example.com/url", "repo1");
+
+        m.updateStart();
+        assertTrue(m.updateSourcePackages(false /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "known source", 2),
+                new MockEmptyPackage(null, "unknown source", 3),
+        }));
+
+        assertEquals(
+                "PkgCategorySource <source=Local Packages (no.source), #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'unknown source' rev=3>\n" +
+                "PkgCategorySource <source=repo1 (example.com), #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'known source' rev=2>\n",
+                getTree(m, false /*displaySortByApi*/));
+
+        assertTrue(m.updateSourcePackages(false /*sortByApi*/, src1, new Package[] {
+                new MockEmptyPackage(src1, "new", 1),
+        }));
+
+        assertFalse(m.updateEnd(false /*sortByApi*/));
+
+        assertEquals(
+                "PkgCategorySource <source=Local Packages (no.source), #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'unknown source' rev=3>\n" +
+                "PkgCategorySource <source=repo1 (example.com), #items=2>\n" +
+                "-- <NEW, pkg:MockEmptyPackage 'new' rev=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'known source' rev=2>\n",
+                getTree(m, false /*displaySortByApi*/));
+    }
+
+    public void testSortBySource_Update1() {
+
+        // Typical case: user has a locally installed package in revision 1
+        // The display list after sort should show that instaled package.
+        SdkSource src1 = new SdkRepoSource("http://example.com/url", "repo1");
+        m.updateStart();
+        assertTrue(m.updateSourcePackages(false /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1),
+        }));
+
+        assertEquals(
+                "PkgCategorySource <source=Local Packages (no.source), #items=0>\n" +
+                "PkgCategorySource <source=repo1 (example.com), #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1>\n",
+                getTree(m, false /*displaySortByApi*/));
+
+        // Edge case: the source reveals an update in revision 2. It is ignored since
+        // we already have a package in rev 4.
+
+        assertTrue(m.updateSourcePackages(false /*sortByApi*/, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 4),
+                new MockEmptyPackage(src1, "type1", 2),
+        }));
+
+        assertTrue(m.updateEnd(false /*sortByApi*/));
+
+        assertEquals(
+                "PkgCategorySource <source=repo1 (example.com), #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=4>\n",
+                getTree(m, false /*displaySortByApi*/));
+    }
+
+    public void testSortBySource_Reload() {
+
+        // First load reveals a package local package and its update
+        SdkSource src1 = new SdkRepoSource("http://example.com/url", "repo1");
+        m.updateStart();
+        assertTrue(m.updateSourcePackages(false /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1),
+        }));
+        assertTrue(m.updateSourcePackages(false /*sortByApi*/, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 2),
+        }));
+        assertTrue(m.updateEnd(false /*sortByApi*/));
+
+        assertEquals(
+                "PkgCategorySource <source=repo1 (example.com), #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n",
+                getTree(m, false /*displaySortByApi*/));
+
+        // Now simulate a reload that clears the package list and creates similar
+        // objects but not the same references. Update methods return false since
+        // they don't change anything.
+        m.updateStart();
+        assertFalse(m.updateSourcePackages(false /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1),
+        }));
+        assertFalse(m.updateSourcePackages(false /*sortByApi*/, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 2),
+        }));
+        assertTrue(m.updateEnd(false /*sortByApi*/));
+
+        assertEquals(
+                "PkgCategorySource <source=repo1 (example.com), #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n",
+                getTree(m, false /*displaySortByApi*/));
+    }
+
+    public void testSortBySource_InstallPackage() {
+
+        // First load reveals a new package
+        SdkSource src1 = new SdkRepoSource("http://example.com/url", "repo1");
+        m.updateStart();
+        // no local package
+        assertFalse(m.updateSourcePackages(false /*sortByApi*/, null /*locals*/, new Package[0]));
+        assertTrue(m.updateSourcePackages(false /*sortByApi*/, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1),
+        }));
+        assertTrue(m.updateEnd(false /*sortByApi*/));
+
+        assertEquals(
+                "PkgCategorySource <source=repo1 (example.com), #items=1>\n" +
+                "-- <NEW, pkg:MockEmptyPackage 'type1' rev=1>\n",
+                getTree(m, false /*displaySortByApi*/));
+
+
+        // Install it. The display only shows the installed one, 'hiding' the remote package
+        m.updateStart();
+        assertTrue(m.updateSourcePackages(false /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1),
+        }));
+        assertFalse(m.updateSourcePackages(false /*sortByApi*/, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1),
+        }));
+        assertTrue(m.updateEnd(false /*sortByApi*/));
+
+        assertEquals(
+                "PkgCategorySource <source=repo1 (example.com), #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1>\n",
+                getTree(m, false /*displaySortByApi*/));
+
+        // Now we have an update
+        m.updateStart();
+        assertFalse(m.updateSourcePackages(false /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1),
+        }));
+        assertTrue(m.updateSourcePackages(false /*sortByApi*/, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 2),
+        }));
+        assertTrue(m.updateEnd(false /*sortByApi*/));
+
+        assertEquals(
+                "PkgCategorySource <source=repo1 (example.com), #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n",
+                getTree(m, false /*displaySortByApi*/));
+    }
+
+    public void testSortBySource_DeletePackage() {
+        SdkSource src1 = new SdkRepoSource("http://example.com/url", "repo1");
+
+        // Start with an installed package and its matching remote package
+        m.updateStart();
+        assertTrue(m.updateSourcePackages(false /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1),
+        }));
+        assertFalse(m.updateSourcePackages(false /*sortByApi*/, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1),
+        }));
+        assertTrue(m.updateEnd(false /*sortByApi*/));
+
+        assertEquals(
+                "PkgCategorySource <source=repo1 (example.com), #items=1>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1>\n",
+                getTree(m, false /*displaySortByApi*/));
+
+        // User now deletes the installed package.
+        m.updateStart();
+        // no local package
+        assertTrue(m.updateSourcePackages(false /*sortByApi*/, null /*locals*/, new Package[0]));
+        assertTrue(m.updateSourcePackages(false /*sortByApi*/, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1),
+        }));
+        assertTrue(m.updateEnd(false /*sortByApi*/));
+
+        assertEquals(
+                "PkgCategorySource <source=repo1 (example.com), #items=1>\n" +
+                "-- <NEW, pkg:MockEmptyPackage 'type1' rev=1>\n",
+                getTree(m, false /*displaySortByApi*/));
+    }
+
+    public void testSortBySource_CompleteUpdate() {
+        SdkSource src1 = new SdkRepoSource("http://1.example.com/url1", "repo1");
+        SdkSource src2 = new SdkRepoSource("http://2.example.com/url2", "repo2");
+
+        // First update has the typical tools and a couple extras
+        m.updateStart();
+
+        assertTrue(m.updateSourcePackages(false /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockToolPackage(src1, 10, 3),
+                new MockPlatformToolPackage(src1, 3),
+                new MockExtraPackage(src1, "android", "usb_driver", 4, 3),
+        }));
+        assertTrue(m.updateSourcePackages(false /*sortByApi*/, src1, new Package[] {
+                new MockToolPackage(src1, 10, 3),
+                new MockPlatformToolPackage(src1, 3),
+                new MockExtraPackage(src1, "carrier", "custom_rom", 1, 0),
+                new MockExtraPackage(src1, "android", "usb_driver", 5, 3),
+        }));
+        assertTrue(m.updateEnd(false /*sortByApi*/));
+
+        assertEquals(
+                "PkgCategorySource <source=repo1 (1.example.com), #items=4>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 10>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3>\n" +
+                "-- <INSTALLED, pkg:Android USB Driver, revision 4, updated by:Android USB Driver, revision 5>\n" +
+                "-- <NEW, pkg:Carrier Custom Rom, revision 1>\n",
+                getTree(m, false /*displaySortByApi*/));
+
+        // Next update adds platforms and addon, sorted in a category based on their API level
+        m.updateStart();
+        MockPlatformPackage p1;
+        MockPlatformPackage p2;
+        @SuppressWarnings("unused") // keep p3 for clarity
+        MockPlatformPackage p3;
+
+        assertTrue(m.updateSourcePackages(false /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockToolPackage(src1, 10, 3),
+                new MockPlatformToolPackage(src1, 3),
+                new MockExtraPackage(src1, "android", "usb_driver", 4, 3),
+                // second update
+                p1 = new MockPlatformPackage(src1, 1, 2, 3),  // API 1
+                p3 = new MockPlatformPackage(src1, 3, 6, 3),
+                new MockPlatformPackage(src1, 3, 6, 3),       // API 3
+                new MockAddonPackage(src2, "addon A", p1, 5),
+                new MockAddonPackage(src2, "addon D", p1, 10),
+        }));
+        assertTrue(m.updateSourcePackages(false /*sortByApi*/, src1, new Package[] {
+                new MockToolPackage(src1, 10, 3),
+                new MockPlatformToolPackage(src1, 3),
+                new MockExtraPackage(src1, "carrier", "custom_rom", 1, 0),
+                new MockExtraPackage(src1, "android", "usb_driver", 5, 3),
+                // second update
+                p2 = new MockPlatformPackage(src1, 2, 4, 3),    // API 2
+        }));
+        assertTrue(m.updateSourcePackages(false /*sortByApi*/, src2, new Package[] {
+                new MockAddonPackage(src2, "addon C", p2, 9),
+                new MockAddonPackage(src2, "addon A", p1, 6),
+                // the rev 7+8 will be ignored since there's a rev 9 coming after
+                new MockAddonPackage(src2, "addon B", p2, 7),
+                new MockAddonPackage(src2, "addon B", p2, 8),
+                new MockAddonPackage(src2, "addon B", p2, 9),
+                // 11+12 should be ignored updates, 13 will update 10
+                new MockAddonPackage(src2, "addon D", p1, 10),
+                new MockAddonPackage(src2, "addon D", p1, 12),  // note: 12 listed before 11
+                new MockAddonPackage(src2, "addon D", p1, 11),
+                new MockAddonPackage(src2, "addon D", p1, 13),
+        }));
+        assertTrue(m.updateEnd(false /*sortByApi*/));
+
+        assertEquals(
+                "PkgCategorySource <source=repo1 (1.example.com), #items=7>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 10>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-3, API 3, revision 6>\n" +
+                "-- <NEW, pkg:SDK Platform Android android-2, API 2, revision 4>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n" +
+                "-- <INSTALLED, pkg:Android USB Driver, revision 4, updated by:Android USB Driver, revision 5>\n" +
+                "-- <NEW, pkg:Carrier Custom Rom, revision 1>\n" +
+                "PkgCategorySource <source=repo2 (2.example.com), #items=4>\n" +
+                "-- <NEW, pkg:The addon B from vendor 2, Android API 2, revision 9>\n" +
+                "-- <NEW, pkg:The addon C from vendor 2, Android API 2, revision 9>\n" +
+                "-- <INSTALLED, pkg:The addon A from vendor 1, Android API 1, revision 5, updated by:The addon A from vendor 1, Android API 1, revision 6>\n" +
+                "-- <INSTALLED, pkg:The addon D from vendor 1, Android API 1, revision 10, updated by:The addon D from vendor 1, Android API 1, revision 13>\n",
+                getTree(m, false /*displaySortByApi*/));
+
+        // Reloading the same thing should have no impact except for the update methods
+        // returning false when they don't change the current list.
+        m.updateStart();
+
+        assertFalse(m.updateSourcePackages(false /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockToolPackage(src1, 10, 3),
+                new MockPlatformToolPackage(src1, 3),
+                new MockExtraPackage(src1, "android", "usb_driver", 4, 3),
+                // second update
+                p1 = new MockPlatformPackage(src1, 1, 2, 3),  // API 1
+                p3 = new MockPlatformPackage(src1, 3, 6, 3),
+                new MockPlatformPackage(src1, 3, 6, 3),       // API 3
+                new MockAddonPackage(src2, "addon A", p1, 5),
+                new MockAddonPackage(src2, "addon D", p1, 10),
+        }));
+        assertFalse(m.updateSourcePackages(false /*sortByApi*/, src1, new Package[] {
+                new MockToolPackage(src1, 10, 3),
+                new MockPlatformToolPackage(src1, 3),
+                new MockExtraPackage(src1, "carrier", "custom_rom", 1, 0),
+                new MockExtraPackage(src1, "android", "usb_driver", 5, 3),
+                // second update
+                p2 = new MockPlatformPackage(src1, 2, 4, 3),
+        }));
+        assertTrue(m.updateSourcePackages(false /*sortByApi*/, src2, new Package[] {
+                new MockAddonPackage(src2, "addon C", p2, 9),
+                new MockAddonPackage(src2, "addon A", p1, 6),
+                // the rev 7+8 will be ignored since there's a rev 9 coming after
+                new MockAddonPackage(src2, "addon B", p2, 7),
+                new MockAddonPackage(src2, "addon B", p2, 8),
+                new MockAddonPackage(src2, "addon B", p2, 9),
+                // 11+12 should be ignored updates, 13 will update 10
+                new MockAddonPackage(src2, "addon D", p1, 10),
+                new MockAddonPackage(src2, "addon D", p1, 12),  // note: 12 listed before 11
+                new MockAddonPackage(src2, "addon D", p1, 11),
+                new MockAddonPackage(src2, "addon D", p1, 13),
+        }));
+        assertTrue(m.updateEnd(false /*sortByApi*/));
+
+        assertEquals(
+                "PkgCategorySource <source=repo1 (1.example.com), #items=7>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 10>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-3, API 3, revision 6>\n" +
+                "-- <NEW, pkg:SDK Platform Android android-2, API 2, revision 4>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n" +
+                "-- <INSTALLED, pkg:Android USB Driver, revision 4, updated by:Android USB Driver, revision 5>\n" +
+                "-- <NEW, pkg:Carrier Custom Rom, revision 1>\n" +
+                "PkgCategorySource <source=repo2 (2.example.com), #items=4>\n" +
+                "-- <NEW, pkg:The addon B from vendor 2, Android API 2, revision 9>\n" +
+                "-- <NEW, pkg:The addon C from vendor 2, Android API 2, revision 9>\n" +
+                "-- <INSTALLED, pkg:The addon A from vendor 1, Android API 1, revision 5, updated by:The addon A from vendor 1, Android API 1, revision 6>\n" +
+                "-- <INSTALLED, pkg:The addon D from vendor 1, Android API 1, revision 10, updated by:The addon D from vendor 1, Android API 1, revision 13>\n",
+                getTree(m, false /*displaySortByApi*/));
+    }
+
+    // ----
+
+    public void testIsFirstLoadComplete() {
+        // isFirstLoadComplete is a simple toggle that goes from true to false when read once
+        assertTrue(m.isFirstLoadComplete());
+        assertFalse(m.isFirstLoadComplete());
+        assertFalse(m.isFirstLoadComplete());
+    }
+
+    public void testCheckNewUpdateItems_NewOnly() {
+        // Populate the list with a few items and an update
+        SdkSource src1 = new SdkRepoSource("http://example.com/url", "repo1");
+        m.updateStart();
+        m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "has update", 1),
+                new MockEmptyPackage(src1, "no update", 4)
+        });
+        m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockEmptyPackage(src1, "has update", 2),
+                new MockEmptyPackage(src1, "new stuff", 3),
+        });
+        m.updateEnd(true /*sortByApi*/);
+        // Nothing is checked at first
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=3>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'has update' rev=1, updated by:MockEmptyPackage 'has update' rev=2>\n" +
+                "-- <NEW, pkg:MockEmptyPackage 'new stuff' rev=3>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'no update' rev=4>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=repo1 (example.com), #items=3>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'has update' rev=1, updated by:MockEmptyPackage 'has update' rev=2>\n" +
+                "-- <NEW, pkg:MockEmptyPackage 'new stuff' rev=3>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'no update' rev=4>\n",
+                getTree(m, false /*displaySortByApi*/));
+
+        // Now request to check new items only
+        m.checkNewUpdateItems(true, false, false, SdkConstants.PLATFORM_LINUX);
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=3>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'has update' rev=1, updated by:MockEmptyPackage 'has update' rev=2>\n" +
+                "-- < * NEW, pkg:MockEmptyPackage 'new stuff' rev=3>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'no update' rev=4>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=repo1 (example.com), #items=3>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'has update' rev=1, updated by:MockEmptyPackage 'has update' rev=2>\n" +
+                "-- < * NEW, pkg:MockEmptyPackage 'new stuff' rev=3>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'no update' rev=4>\n",
+                getTree(m, false /*displaySortByApi*/));
+    }
+
+    public void testCheckNewUpdateItems_UpdateOnly() {
+        // Populate the list with a few items and an update
+        SdkSource src1 = new SdkRepoSource("http://example.com/url", "repo1");
+        m.updateStart();
+        m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "has update", 1),
+                new MockEmptyPackage(src1, "no update", 4)
+        });
+        m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockEmptyPackage(src1, "has update", 2),
+                new MockEmptyPackage(src1, "new stuff", 3),
+        });
+        m.updateEnd(true /*sortByApi*/);
+        // Nothing is checked at first
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=3>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'has update' rev=1, updated by:MockEmptyPackage 'has update' rev=2>\n" +
+                "-- <NEW, pkg:MockEmptyPackage 'new stuff' rev=3>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'no update' rev=4>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=repo1 (example.com), #items=3>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'has update' rev=1, updated by:MockEmptyPackage 'has update' rev=2>\n" +
+                "-- <NEW, pkg:MockEmptyPackage 'new stuff' rev=3>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'no update' rev=4>\n",
+                getTree(m, false /*displaySortByApi*/));
+
+        // Now request to check update items only
+        m.checkNewUpdateItems(false, true, false, SdkConstants.PLATFORM_LINUX);
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=3>\n" +
+                "-- < * INSTALLED, pkg:MockEmptyPackage 'has update' rev=1, updated by:MockEmptyPackage 'has update' rev=2>\n" +
+                "-- <NEW, pkg:MockEmptyPackage 'new stuff' rev=3>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'no update' rev=4>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=repo1 (example.com), #items=3>\n" +
+                "-- < * INSTALLED, pkg:MockEmptyPackage 'has update' rev=1, updated by:MockEmptyPackage 'has update' rev=2>\n" +
+                "-- <NEW, pkg:MockEmptyPackage 'new stuff' rev=3>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'no update' rev=4>\n",
+                getTree(m, false /*displaySortByApi*/));
+    }
+
+    public void testCheckNewUpdateItems_SelectInitial() {
+        // Populate the list with typical items: tools, platforms tools, extras, 2 platforms.
+        // With nothing installed, this should pick the top platform and its system images
+        // (the mock platform claims to not have any included abi)
+        // It's ok not to select the tools, since they are a dependency of all platforms.
+
+        SdkSource src1 = new SdkRepoSource("http://1.example.com/url1", "repo1");
+        SdkSource src2 = new SdkRepoSource("http://2.example.com/url2", "repo2");
+
+        m.updateStart();
+        MockPlatformPackage p1;
+        MockPlatformPackage p2;
+
+        m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockToolPackage(src1, 10, 3),
+                new MockPlatformToolPackage(src1, 3),
+                new MockExtraPackage(src1, "google", "usb_driver", 5, 3),
+                p1 = new MockPlatformPackage(src1, 1, 2, 3),    // API 1
+                p2 = new MockPlatformPackage(src1, 2, 4, 3),    // API 2
+                new MockSystemImagePackage(src1, p2, 1, "armeabi"),
+                new MockSystemImagePackage(src1, p2, 1, "x86"),
+        });
+        m.updateSourcePackages(true /*sortByApi*/, src2, new Package[] {
+                new MockAddonPackage(src2, "addon A", p1, 5),
+                new MockAddonPackage(src2, "addon B", p2, 7),
+                new MockExtraPackage(src2, "carrier", "custom_rom", 1, 0),
+        });
+        m.updateEnd(true /*sortByApi*/);
+
+        m.checkNewUpdateItems(false, true, true, SdkConstants.PLATFORM_LINUX);
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=2>\n" +
+                "-- <NEW, pkg:Android SDK Tools, revision 10>\n" +
+                "-- <NEW, pkg:Android SDK Platform-tools, revision 3>\n" +
+                "PkgCategoryApi <API=API 2, label=Android android-2 (API 2), #items=4>\n" +
+                "-- < * NEW, pkg:SDK Platform Android android-2, API 2, revision 4>\n" +
+                "-- < * NEW, pkg:ARM EABI System Image, Android API 2, revision 1>\n" +
+                "-- < * NEW, pkg:Intel x86 Atom System Image, Android API 2, revision 1>\n" +
+                "-- < * NEW, pkg:The addon B from vendor 2, Android API 2, revision 7>\n" +
+                "PkgCategoryApi <API=API 1, label=Android android-1 (API 1), #items=2>\n" +
+                "-- <NEW, pkg:SDK Platform Android android-1, API 1, revision 2>\n" +
+                "-- <NEW, pkg:The addon A from vendor 1, Android API 1, revision 5>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=2>\n" +
+                "-- <NEW, pkg:Carrier Custom Rom, revision 1>\n" +
+                "-- <NEW, pkg:Google USB Driver, revision 5>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=repo1 (1.example.com), #items=7>\n" +
+                "-- <NEW, pkg:Android SDK Tools, revision 10>\n" +
+                "-- <NEW, pkg:Android SDK Platform-tools, revision 3>\n" +
+                "-- < * NEW, pkg:SDK Platform Android android-2, API 2, revision 4>\n" +
+                "-- <NEW, pkg:SDK Platform Android android-1, API 1, revision 2>\n" +
+                "-- < * NEW, pkg:ARM EABI System Image, Android API 2, revision 1>\n" +
+                "-- < * NEW, pkg:Intel x86 Atom System Image, Android API 2, revision 1>\n" +
+                "-- <NEW, pkg:Google USB Driver, revision 5>\n" +
+                "PkgCategorySource <source=repo2 (2.example.com), #items=3>\n" +
+                "-- < * NEW, pkg:The addon B from vendor 2, Android API 2, revision 7>\n" +
+                "-- <NEW, pkg:The addon A from vendor 1, Android API 1, revision 5>\n" +
+                "-- <NEW, pkg:Carrier Custom Rom, revision 1>\n",
+                getTree(m, false /*displaySortByApi*/));
+
+        // We don't install the USB driver by default on Mac or Linux, only on Windows
+        m.clear();
+        m.updateStart();
+        m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockExtraPackage(src1, "google", "usb_driver", 5, 3),
+        });
+        m.updateEnd(true /*sortByApi*/);
+        m.checkNewUpdateItems(false, true, true, SdkConstants.PLATFORM_LINUX);
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <NEW, pkg:Google USB Driver, revision 5>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=repo1 (1.example.com), #items=1>\n" +
+                "-- <NEW, pkg:Google USB Driver, revision 5>\n",
+                getTree(m, false /*displaySortByApi*/));
+
+        m.clear();
+        m.updateStart();
+        m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockExtraPackage(src1, "google", "usb_driver", 5, 3),
+        });
+        m.updateEnd(true /*sortByApi*/);
+        m.checkNewUpdateItems(false, true, true, SdkConstants.PLATFORM_DARWIN);
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <NEW, pkg:Google USB Driver, revision 5>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=repo1 (1.example.com), #items=1>\n" +
+                "-- <NEW, pkg:Google USB Driver, revision 5>\n",
+                getTree(m, false /*displaySortByApi*/));
+
+        m.clear();
+        m.updateStart();
+        m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockExtraPackage(src1, "google", "usb_driver", 5, 3),
+        });
+        m.updateEnd(true /*sortByApi*/);
+        m.checkNewUpdateItems(false, true, true, SdkConstants.PLATFORM_WINDOWS);
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- < * NEW, pkg:Google USB Driver, revision 5>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=repo1 (1.example.com), #items=1>\n" +
+                "-- < * NEW, pkg:Google USB Driver, revision 5>\n",
+                getTree(m, false /*displaySortByApi*/));
+
+    }
+
+    public void testCheckUncheckAllItems() {
+        // Populate the list with a couple items and an update
+        SdkSource src1 = new SdkRepoSource("http://example.com/url", "repo1");
+        m.updateStart();
+        m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockEmptyPackage(src1, "type1", 1)
+        });
+        m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockEmptyPackage(src1, "type1", 2),
+                new MockEmptyPackage(src1, "type3", 3),
+        });
+        m.updateEnd(true /*sortByApi*/);
+        // Nothing is checked at first
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=2>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n" +
+                "-- <NEW, pkg:MockEmptyPackage 'type3' rev=3>\n",
+                getTree(m, true /*displaySortByApi*/));
+
+        // Manually check the items in the sort-by-API case, but not the source
+        for (PkgItem item : m.getAllPkgItems(true /*byApi*/, false /*bySource*/)) {
+            item.setChecked(true);
+        }
+
+        // by-api sort should be checked but not by source
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=2>\n" +
+                "-- < * INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n" +
+                "-- < * NEW, pkg:MockEmptyPackage 'type3' rev=3>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=repo1 (example.com), #items=2>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n" +
+                "-- <NEW, pkg:MockEmptyPackage 'type3' rev=3>\n",
+                getTree(m, false /*displaySortByApi*/));
+
+        // now uncheck them all
+        m.uncheckAllItems();
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=2>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n" +
+                "-- <NEW, pkg:MockEmptyPackage 'type3' rev=3>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=repo1 (example.com), #items=2>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n" +
+                "-- <NEW, pkg:MockEmptyPackage 'type3' rev=3>\n",
+                getTree(m, false /*displaySortByApi*/));
+
+        // Manually check the items in both by-api and by-source
+        for (PkgItem item : m.getAllPkgItems(true /*byApi*/, true /*bySource*/)) {
+            item.setChecked(true);
+        }
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=2>\n" +
+                "-- < * INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n" +
+                "-- < * NEW, pkg:MockEmptyPackage 'type3' rev=3>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=repo1 (example.com), #items=2>\n" +
+                "-- < * INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n" +
+                "-- < * NEW, pkg:MockEmptyPackage 'type3' rev=3>\n",
+                getTree(m, false /*displaySortByApi*/));
+
+        // now uncheck them all
+        m.uncheckAllItems();
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=2>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n" +
+                "-- <NEW, pkg:MockEmptyPackage 'type3' rev=3>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=repo1 (example.com), #items=2>\n" +
+                "-- <INSTALLED, pkg:MockEmptyPackage 'type1' rev=1, updated by:MockEmptyPackage 'type1' rev=2>\n" +
+                "-- <NEW, pkg:MockEmptyPackage 'type3' rev=3>\n",
+                getTree(m, false /*displaySortByApi*/));
+    }
+
+    // ----
+
+    public void testLocalIsNewer() {
+        // This tests an edge case that typically happens only during development where
+        // one would have a local package which revision number is larger than what the
+        // remove repositories can offer. In this case we don't want to offer the remote
+        // package as an "upgrade" nor as a downgrade.
+
+        // Populate the list with local revisions 5 and lower remote revisions 3
+        SdkSource src1 = new SdkRepoSource("http://example.com/url", "repo1");
+        m.updateStart();
+        m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockToolPackage(        src1, 5, 5),
+                new MockPlatformToolPackage(src1, 5),
+        });
+        m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockToolPackage(        src1, 3, 3),
+                new MockPlatformToolPackage(src1, 3),
+        });
+        m.updateEnd(true /*sortByApi*/);
+
+        // The remote packages in rev 3 are hidden by the local packages in rev 5
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=2>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 5>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 5>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=0>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=repo1 (example.com), #items=2>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 5>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 5>\n",
+                getTree(m, false /*displaySortByApi*/));
+    }
+
+    public void testSourceDups() {
+        // This tests an edge case were 2 remote repositories are giving the
+        // same kind of packages. In rev 14, we didn't want to merge them together
+        // unless they had the same hostname. In rev 15, we now treat them the same.
+
+        // repo1, 2 and 3 have the same hostname so redundancy is ok
+        SdkSource src1 = new SdkRepoSource("http://example.com/url1", "repo1");
+        SdkSource src2 = new SdkRepoSource("http://example.com/url2", "repo2");
+        SdkSource src3 = new SdkRepoSource("http://example.com/url3", "repo3");
+        // repo4 has a different hostname but as of rev 15, the packages will be merged together.
+        SdkSource src4 = new SdkRepoSource("http://4.example.com/url4", "repo4");
+        MockPlatformPackage p1 = null;
+
+        m.updateStart();
+        m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockToolPackage(        src1, 3, 3),
+                new MockPlatformToolPackage(src1, 3),
+                p1 = new MockPlatformPackage(src1, 1, 2, 3),    // API 1
+        });
+        m.updateSourcePackages(true /*sortByApi*/, src2, new Package[] {
+                new MockAddonPackage(src2, "addon A", p1, 5),
+                new MockAddonPackage(src2, "addon B", p1, 6),
+        });
+        m.updateSourcePackages(true /*sortByApi*/, src3, new Package[] {
+                new MockAddonPackage(src3, "addon A", p1, 5), // same as  addon A rev 5 from src2
+                new MockAddonPackage(src3, "addon B", p1, 7), // upgrades addon B rev 6 from src2
+        });
+        m.updateSourcePackages(true /*sortByApi*/, src4, new Package[] {
+                new MockAddonPackage(src4, "addon A", p1, 5), // same as  addon A rev 5 from src2
+                new MockAddonPackage(src4, "addon B", p1, 7), // upgrades addon B rev 6 from src2
+        });
+        m.updateEnd(true /*sortByApi*/);
+
+        // The remote packages in rev 3 are hidden by the local packages in rev 5.
+        // When sorting by API, the user can tell where the packages come from by looking
+        // at the UI tooltip on the packages.
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=2>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 3>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3>\n" +
+                "PkgCategoryApi <API=API 1, label=Android android-1 (API 1), #items=3>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n" +
+                "-- <NEW, pkg:The addon A from vendor 1, Android API 1, revision 5>\n" + // from src2+3+4
+                "-- <NEW, pkg:The addon B from vendor 1, Android API 1, revision 7>\n" + // from src3+4
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=0>\n",
+                getTree(m, true /*displaySortByApi*/));
+        // When sorting by source, the src4 source is listed, however since its
+        // packages are the same as the ones from src2 or src3 the packages themselves
+        // are not shown.
+        assertEquals(
+                "PkgCategorySource <source=repo1 (example.com), #items=3>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 3>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n" +
+                "PkgCategorySource <source=repo2 (example.com), #items=1>\n" +
+                "-- <NEW, pkg:The addon A from vendor 1, Android API 1, revision 5>\n" + // from src2+3+4
+                "PkgCategorySource <source=repo3 (example.com), #items=1>\n" +
+                "-- <NEW, pkg:The addon B from vendor 1, Android API 1, revision 7>\n" + // from src3+4
+                "PkgCategorySource <source=repo4 (4.example.com), #items=0>\n",
+                getTree(m, false /*displaySortByApi*/));
+    }
+
+    public void testRenamedExtraPackage() {
+        // Starting with schemas repo v5 and addon v3, an extra package can be renamed
+        // using the "old-paths" attribute. This test checks that the diff logic will
+        // match an old extra and its new name together.
+
+        // First scenario: local pkg "old_path1" and remote pkg "new_path2".
+        // Since the new package does not provide an old_paths attribute, the
+        // new package is not treated as an update.
+
+        SdkSource src1 = new SdkRepoSource("http://example.com/url1", "repo1");
+        m.updateStart();
+        m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockExtraPackage(src1, "vendor1", "old_path1", 1, 1),
+        });
+        m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockExtraPackage(src1, "vendor1", "new_path2", 2, 1),
+        });
+        m.updateEnd(true /*sortByApi*/);
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=2>\n" +
+                "-- <NEW, pkg:Vendor1 New Path2, revision 2>\n" +
+                "-- <INSTALLED, pkg:Vendor1 Old Path1, revision 1>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=repo1 (example.com), #items=2>\n" +
+                "-- <NEW, pkg:Vendor1 New Path2, revision 2>\n" +
+                "-- <INSTALLED, pkg:Vendor1 Old Path1, revision 1>\n",
+                getTree(m, false /*displaySortByApi*/));
+
+        // Now, start again, but this time the new package uses the old-path attribute
+        Properties props = new Properties();
+        props.setProperty(PkgProps.EXTRA_OLD_PATHS, "old_path1;oldpath2");
+        m.clear();
+
+        m.updateStart();
+        m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockExtraPackage(src1, "vendor1", "old_path1", 1, 1),
+        });
+        m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockExtraPackage(src1, props, "vendor1", "new_path2", 2),
+        });
+        m.updateEnd(true /*sortByApi*/);
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <INSTALLED, pkg:Vendor1 Old Path1, revision 1, updated by:Vendor1 New Path2, revision 2>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=repo1 (example.com), #items=1>\n" +
+                "-- <INSTALLED, pkg:Vendor1 Old Path1, revision 1, updated by:Vendor1 New Path2, revision 2>\n",
+                getTree(m, false /*displaySortByApi*/));
+    }
+
+    public void testBrokenAddon() {
+        SdkSource src1 = new SdkRepoSource("http://1.example.com/url1", "repo1");
+        SdkSource src2 = new SdkRepoSource("http://2.example.com/url2", "repo2");
+
+        MockPlatformPackage p1 = null;
+        MockAddonPackage a1 = null;
+
+        // User has a platform + addon locally installed
+        m.updateStart();
+        m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                p1 = new MockPlatformPackage(src1, 1, 2, 3),    // API 1
+                a1 = new MockAddonPackage(src2, "addon A", p1, 4),
+        });
+        m.updateSourcePackages(true /*sortByApi*/, src1 /*locals*/, new Package[] {
+                p1
+        });
+        m.updateSourcePackages(true /*sortByApi*/, src2 /*locals*/, new Package[] {
+                a1
+        });
+        m.updateEnd(true /*sortByApi*/);
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=API 1, label=Android android-1 (API 1), #items=2>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n" +
+                "-- <INSTALLED, pkg:The addon A from vendor 1, Android API 1, revision 4>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=0>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=repo1 (1.example.com), #items=1>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n" +
+                "PkgCategorySource <source=repo2 (2.example.com), #items=1>\n" +
+                "-- <INSTALLED, pkg:The addon A from vendor 1, Android API 1, revision 4>\n",
+                getTree(m, false /*displaySortByApi*/));
+
+        // Now user deletes the platform on disk and reload.
+        // The local package parser will only find a broken addon.
+        m.updateStart();
+        m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockBrokenPackage(BrokenPackage.MIN_API_LEVEL_NOT_SPECIFIED, 1),
+        });
+        m.updateSourcePackages(true /*sortByApi*/, src1 /*locals*/, new Package[] {
+                new MockPlatformPackage(src1, 1, 2, 3)
+        });
+        m.updateSourcePackages(true /*sortByApi*/, src2 /*locals*/, new Package[] {
+                new MockAddonPackage(src2, "addon A", p1, 4)
+        });
+        m.updateEnd(true /*sortByApi*/);
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=API 1, label=Android android-1 (API 1), #items=2>\n" +
+                "-- <NEW, pkg:SDK Platform Android android-1, API 1, revision 2>\n" +
+                "-- <NEW, pkg:The addon A from vendor 1, Android API 1, revision 4>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=1>\n" +
+                "-- <INSTALLED, pkg:Broken package for API 1>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=repo1 (1.example.com), #items=1>\n" +
+                "-- <NEW, pkg:SDK Platform Android android-1, API 1, revision 2>\n" +
+                "PkgCategorySource <source=repo2 (2.example.com), #items=1>\n" +
+                "-- <NEW, pkg:The addon A from vendor 1, Android API 1, revision 4>\n" +
+                "PkgCategorySource <source=Local Packages (no.source), #items=1>\n" +
+                "-- <INSTALLED, pkg:Broken package for API 1>\n",
+                getTree(m, false /*displaySortByApi*/));
+
+        // Now user restores the missing platform on disk.
+        m.updateStart();
+        m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                p1 = new MockPlatformPackage(src1, 1, 2, 3),    // API 1
+                a1 = new MockAddonPackage(src2, "addon A", p1, 4),
+        });
+        m.updateSourcePackages(true /*sortByApi*/, src1 /*locals*/, new Package[] {
+                p1
+        });
+        m.updateSourcePackages(true /*sortByApi*/, src2 /*locals*/, new Package[] {
+                a1
+        });
+        m.updateEnd(true /*sortByApi*/);
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=0>\n" +
+                "PkgCategoryApi <API=API 1, label=Android android-1 (API 1), #items=2>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n" +
+                "-- <INSTALLED, pkg:The addon A from vendor 1, Android API 1, revision 4>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=0>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=repo1 (1.example.com), #items=1>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n" +
+                "PkgCategorySource <source=repo2 (2.example.com), #items=1>\n" +
+                "-- <INSTALLED, pkg:The addon A from vendor 1, Android API 1, revision 4>\n",
+                getTree(m, false /*displaySortByApi*/));
+    }
+
+    public void testToolsUpdate() {
+        SdkSource src1 = new SdkRepoSource("http://1.example.com/url1", "repo1");
+        SdkSource src2 = new SdkRepoSource("http://2.example.com/url2", "repo2");
+        MockPlatformPackage p1;
+
+        m.updateStart();
+        m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockToolPackage(3, 3),    // tool package has no source defined
+                new MockPlatformToolPackage(src1, 3),
+                p1 = new MockPlatformPackage(src1, 1, 2, 3),    // API 1
+        });
+        m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockToolPackage(src1, 4, 4),
+                new MockPlatformToolPackage(src1, 4),
+        });
+        m.updateSourcePackages(true /*sortByApi*/, src2, new Package[] {
+                new MockAddonPackage(src2, "addon A", p1, 5),
+                new MockAddonPackage(src2, "addon B", p1, 6),
+        });
+        m.updateEnd(true /*sortByApi*/);
+
+        // The remote packages in rev 3 are hidden by the local packages in rev 5
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=2>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 3, updated by:Android SDK Tools, revision 4>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3, updated by:Android SDK Platform-tools, revision 4>\n" +
+                "PkgCategoryApi <API=API 1, label=Android android-1 (API 1), #items=3>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n" +
+                "-- <NEW, pkg:The addon A from vendor 1, Android API 1, revision 5>\n" +
+                "-- <NEW, pkg:The addon B from vendor 1, Android API 1, revision 6>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=0>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=Local Packages (no.source), #items=1>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 3, updated by:Android SDK Tools, revision 4>\n" +
+                "PkgCategorySource <source=repo1 (1.example.com), #items=2>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3, updated by:Android SDK Platform-tools, revision 4>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n" +
+                "PkgCategorySource <source=repo2 (2.example.com), #items=2>\n" +
+                "-- <NEW, pkg:The addon A from vendor 1, Android API 1, revision 5>\n" +
+                "-- <NEW, pkg:The addon B from vendor 1, Android API 1, revision 6>\n",
+                getTree(m, false /*displaySortByApi*/));
+    }
+
+    public void testToolsMinorUpdate() {
+        // Test: Check a minor revision updates an installed major revision.
+
+        SdkSource src1 = new SdkRepoSource("http://1.example.com/url1", "repo1");
+
+        m.updateStart();
+        m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockToolPackage(3, 3),                                          // Tools 3.0.0
+                new MockPlatformToolPackage(src1, 3),
+        });
+        m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockToolPackage(src1, new FullRevision(3, 0, 1), 3),          // Tools 3.0.1
+        });
+        m.updateEnd(true /*sortByApi*/);
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=2>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 3, updated by:Android SDK Tools, revision 3.0.1>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=0>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=Local Packages (no.source), #items=1>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 3, updated by:Android SDK Tools, revision 3.0.1>\n" +
+                "PkgCategorySource <source=repo1 (1.example.com), #items=1>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3>\n",
+                getTree(m, false /*displaySortByApi*/));
+    }
+
+    public void testToolsPreviewsDisabled() {
+        // Test: No local tools installed. The remote server has both tools and platforms
+        // in release and RC versions. However the settings "enable previews" is disabled
+        // (which is the default) so the previews are not actually loaded from the server.
+
+        SdkSource src1 = new SdkRepoSource("http://1.example.com/url1", "repo1");
+
+        m.updateStart();
+        m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockToolPackage(src1, new FullRevision(2, 0, 0), 3),          // Tools 2
+                new MockToolPackage(src1, new FullRevision(4, 0, 0, 1), 3),       // Tools 4 rc1
+                new MockPlatformToolPackage(src1, new FullRevision(3, 0, 0)),     // Plat-T 3
+                new MockPlatformToolPackage(src1, new FullRevision(5, 0, 0, 1)),  // Plat-T 5 rc1
+        });
+        m.updateEnd(true /*sortByApi*/);
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=2>\n" +
+                "-- <NEW, pkg:Android SDK Tools, revision 2>\n" +
+                "-- <NEW, pkg:Android SDK Platform-tools, revision 3>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=0>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=repo1 (1.example.com), #items=2>\n" +
+                "-- <NEW, pkg:Android SDK Tools, revision 2>\n" +
+                "-- <NEW, pkg:Android SDK Platform-tools, revision 3>\n",
+                getTree(m, false /*displaySortByApi*/));
+    }
+
+    public void testToolsPreviews() {
+        // Test: No local tools installed. The remote server has both tools and platforms
+        // in release and RC versions.
+
+        // Enable previews in the settings
+        u.overrideSetting(ISettingsPage.KEY_ENABLE_PREVIEWS, true);
+
+        SdkSource src1 = new SdkRepoSource("http://1.example.com/url1", "repo1");
+
+        m.updateStart();
+        m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockToolPackage(src1, new FullRevision(2, 0, 0), 3),          // Tools 2
+                new MockToolPackage(src1, new FullRevision(4, 0, 0, 1), 3),       // Tools 4 rc1
+                new MockPlatformToolPackage(src1, new FullRevision(3, 0, 0)),     // Plat-T 3
+                new MockPlatformToolPackage(src1, new FullRevision(5, 0, 0, 1)),  // Plat-T 5 rc1
+        });
+        m.updateEnd(true /*sortByApi*/);
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=2>\n" +
+                "-- <NEW, pkg:Android SDK Tools, revision 2>\n" +
+                "-- <NEW, pkg:Android SDK Platform-tools, revision 3>\n" +
+                "PkgCategoryApi <API=TOOLS-PREVIEW, label=Tools (Preview Channel), #items=2>\n" +
+                "-- <NEW, pkg:Android SDK Tools, revision 4 rc1>\n" +
+                "-- <NEW, pkg:Android SDK Platform-tools, revision 5 rc1>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=0>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=repo1 (1.example.com), #items=4>\n" +
+                "-- <NEW, pkg:Android SDK Tools, revision 2>\n" +
+                "-- <NEW, pkg:Android SDK Tools, revision 4 rc1>\n" +
+                "-- <NEW, pkg:Android SDK Platform-tools, revision 3>\n" +
+                "-- <NEW, pkg:Android SDK Platform-tools, revision 5 rc1>\n",
+                getTree(m, false /*displaySortByApi*/));
+    }
+
+    public void testPreviewUpdateInstalledRelease() {
+        // Test: Local release Tools 3.0.0 installed, server has both a release 3.0.1 available
+        // and a Tools Preview 4.0.0 rc1 available.
+        // => v3 is updated by 3.0.1
+        // => v4.0.0rc1 does not update 3.0.0, instead it's a separate download.
+
+        // Enable previews in the settings
+        u.overrideSetting(ISettingsPage.KEY_ENABLE_PREVIEWS, true);
+
+        SdkSource src1 = new SdkRepoSource("http://1.example.com/url1", "repo1");
+
+        m.updateStart();
+        m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockToolPackage(3, 3),    // tool package has no source defined
+                new MockPlatformToolPackage(src1, 3),
+                new MockPlatformPackage(src1, 1, 2, 3),    // API 1
+        });
+        m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockToolPackage(src1, 3, 3),                                  // Tools 3
+                new MockToolPackage(src1, new FullRevision(3, 0, 1), 3),          // Tools 3.0.1
+                new MockToolPackage(src1, new FullRevision(4, 0, 0, 1), 3),       // Tools 4 rc1
+                new MockPlatformToolPackage(src1, new FullRevision(3, 0, 1)),     // PT    3.0.1
+                new MockPlatformToolPackage(src1, new FullRevision(4, 0, 0, 1)),  // PT    4 rc1
+        });
+        m.updateEnd(true /*sortByApi*/);
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=2>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 3, updated by:Android SDK Tools, revision 3.0.1>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3, updated by:Android SDK Platform-tools, revision 3.0.1>\n" +
+                "PkgCategoryApi <API=TOOLS-PREVIEW, label=Tools (Preview Channel), #items=2>\n" +
+                "-- <NEW, pkg:Android SDK Tools, revision 4 rc1>\n" +
+                "-- <NEW, pkg:Android SDK Platform-tools, revision 4 rc1>\n" +
+                "PkgCategoryApi <API=API 1, label=Android android-1 (API 1), #items=1>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=0>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=Local Packages (no.source), #items=1>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 3, updated by:Android SDK Tools, revision 3.0.1>\n" +
+                "PkgCategorySource <source=repo1 (1.example.com), #items=4>\n" +
+                "-- <NEW, pkg:Android SDK Tools, revision 4 rc1>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3, updated by:Android SDK Platform-tools, revision 3.0.1>\n" +
+                "-- <NEW, pkg:Android SDK Platform-tools, revision 4 rc1>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n",
+                getTree(m, false /*displaySortByApi*/));
+
+        // Now request to check new items and updates:
+        // Tools 4 rc1 is greater than the installed Tools 3, but it's a preview so we will NOT
+        //   auto-select it by default even though we requested to select "NEW" packages. We
+        //   want the user to manually opt-in into the rc/preview package.
+        // However Tools 3 has a 3.0.1 update that we'll auto-select.
+        m.checkNewUpdateItems(true, true, false, SdkConstants.PLATFORM_LINUX);
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=2>\n" +
+                "-- < * INSTALLED, pkg:Android SDK Tools, revision 3, updated by:Android SDK Tools, revision 3.0.1>\n" +
+                "-- < * INSTALLED, pkg:Android SDK Platform-tools, revision 3, updated by:Android SDK Platform-tools, revision 3.0.1>\n" +
+                "PkgCategoryApi <API=TOOLS-PREVIEW, label=Tools (Preview Channel), #items=2>\n" +
+                "-- <NEW, pkg:Android SDK Tools, revision 4 rc1>\n" +
+                "-- <NEW, pkg:Android SDK Platform-tools, revision 4 rc1>\n" +
+                "PkgCategoryApi <API=API 1, label=Android android-1 (API 1), #items=1>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=0>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=Local Packages (no.source), #items=1>\n" +
+                "-- < * INSTALLED, pkg:Android SDK Tools, revision 3, updated by:Android SDK Tools, revision 3.0.1>\n" +
+                "PkgCategorySource <source=repo1 (1.example.com), #items=4>\n" +
+                "-- <NEW, pkg:Android SDK Tools, revision 4 rc1>\n" +
+                "-- < * INSTALLED, pkg:Android SDK Platform-tools, revision 3, updated by:Android SDK Platform-tools, revision 3.0.1>\n" +
+                "-- <NEW, pkg:Android SDK Platform-tools, revision 4 rc1>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n",
+                getTree(m, false /*displaySortByApi*/));
+
+    }
+
+    public void testPreviewUpdateInstalledPreview() {
+        // Test: Local preview Tools 3.0.1rc1 installed, server has both a release 3.0.0 available
+        // and a Tools Preview 3.0.1 rc2 available.
+        // => Installed 3.0.1rc1 can be updated by 3.0.1rc2
+        // => There's a separate "new" download for 3.0.0, not installed and NOT updating 3.0.1rc1.
+
+        // Enable previews in the settings
+        u.overrideSetting(ISettingsPage.KEY_ENABLE_PREVIEWS, true);
+
+        SdkSource src1 = new SdkRepoSource("http://1.example.com/url1", "repo1");
+
+        m.updateStart();
+        m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockToolPackage(src1, new FullRevision(3, 0, 1, 1), 4),       //  T 3.0.1rc1
+                new MockPlatformToolPackage(src1, new FullRevision(4, 0, 1, 1)),  // PT 4.0.1rc1
+                new MockPlatformPackage(src1, 1, 2, 3),    // API 1
+        });
+        m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockToolPackage(src1, new FullRevision(3, 0, 0), 4),          //  T 3.0.0
+                new MockToolPackage(src1, new FullRevision(3, 0, 1, 2), 4),       //  T 3.0.1rc2
+                new MockPlatformToolPackage(src1, new FullRevision(4, 0, 0)),     // PT 4.0.0
+                new MockPlatformToolPackage(src1, new FullRevision(4, 0, 1, 2)),  // PT 4.0.1 rc2
+        });
+        m.updateEnd(true /*sortByApi*/);
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=2>\n" +
+                "-- <NEW, pkg:Android SDK Tools, revision 3>\n" +
+                "-- <NEW, pkg:Android SDK Platform-tools, revision 4>\n" +
+                "PkgCategoryApi <API=TOOLS-PREVIEW, label=Tools (Preview Channel), #items=2>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 3.0.1 rc1, updated by:Android SDK Tools, revision 3.0.1 rc2>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 4.0.1 rc1, updated by:Android SDK Platform-tools, revision 4.0.1 rc2>\n" +
+                "PkgCategoryApi <API=API 1, label=Android android-1 (API 1), #items=1>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=0>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=repo1 (1.example.com), #items=5>\n" +
+                "-- <NEW, pkg:Android SDK Tools, revision 3>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 3.0.1 rc1, updated by:Android SDK Tools, revision 3.0.1 rc2>\n" +
+                "-- <NEW, pkg:Android SDK Platform-tools, revision 4>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 4.0.1 rc1, updated by:Android SDK Platform-tools, revision 4.0.1 rc2>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n",
+                getTree(m, false /*displaySortByApi*/));
+
+        // Auto select new and update items. In this case:
+        // - the previews have updates available.
+        // - we're not selecting the non-installed "3.0" version that is older than the
+        //   currently installed "3.0.1rc1" version since that would be a downgrade.
+        m.checkNewUpdateItems(true, true, false, SdkConstants.PLATFORM_LINUX);
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=2>\n" +
+                "-- <NEW, pkg:Android SDK Tools, revision 3>\n" +
+                "-- <NEW, pkg:Android SDK Platform-tools, revision 4>\n" +
+                "PkgCategoryApi <API=TOOLS-PREVIEW, label=Tools (Preview Channel), #items=2>\n" +
+                "-- < * INSTALLED, pkg:Android SDK Tools, revision 3.0.1 rc1, updated by:Android SDK Tools, revision 3.0.1 rc2>\n" +
+                "-- < * INSTALLED, pkg:Android SDK Platform-tools, revision 4.0.1 rc1, updated by:Android SDK Platform-tools, revision 4.0.1 rc2>\n" +
+                "PkgCategoryApi <API=API 1, label=Android android-1 (API 1), #items=1>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=0>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=repo1 (1.example.com), #items=5>\n" +
+                "-- <NEW, pkg:Android SDK Tools, revision 3>\n" +
+                "-- < * INSTALLED, pkg:Android SDK Tools, revision 3.0.1 rc1, updated by:Android SDK Tools, revision 3.0.1 rc2>\n" +
+                "-- <NEW, pkg:Android SDK Platform-tools, revision 4>\n" +
+                "-- < * INSTALLED, pkg:Android SDK Platform-tools, revision 4.0.1 rc1, updated by:Android SDK Platform-tools, revision 4.0.1 rc2>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n",
+                getTree(m, false /*displaySortByApi*/));
+
+        // -----
+
+        // Now simulate that the server has a final package (3.0.1) to replace the
+        // installed 3.0.1rc1 package. It's not installed yet, just available.
+        // - A new 3.0.1 will be available.
+        // - The server no longer lists the RC since there's a final package, yet it is
+        //   still locally installed.
+        // - The 3.0.1 rc1 is not listed as having an update, since we treat the previews
+        //   separately. TODO: consider having the 3.0.1 show up as both a new item /and/
+        //   as an update to the 3.0.1rc1. That may have some other side effects.
+
+        m.uncheckAllItems();
+        m.updateStart();
+        m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockToolPackage(src1, new FullRevision(3, 0, 1, 1), 4),       //  T 3.0.1rc1
+                new MockPlatformToolPackage(src1, new FullRevision(4, 0, 1, 1)),  // PT 4.0.1rc1
+                new MockPlatformPackage(src1, 1, 2, 3),    // API 1
+        });
+        m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockToolPackage(src1, new FullRevision(3, 0, 1), 4),          //  T 3.0.1
+                new MockPlatformToolPackage(src1, new FullRevision(4, 0, 1)),     // PT 4.0.1
+        });
+        m.updateEnd(true /*sortByApi*/);
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=2>\n" +
+                "-- <NEW, pkg:Android SDK Tools, revision 3.0.1>\n" +
+                "-- <NEW, pkg:Android SDK Platform-tools, revision 4.0.1>\n" +
+                "PkgCategoryApi <API=TOOLS-PREVIEW, label=Tools (Preview Channel), #items=2>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 3.0.1 rc1>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 4.0.1 rc1>\n" +
+                "PkgCategoryApi <API=API 1, label=Android android-1 (API 1), #items=1>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=0>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=repo1 (1.example.com), #items=5>\n" +
+                "-- <NEW, pkg:Android SDK Tools, revision 3.0.1>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 3.0.1 rc1>\n" +
+                "-- <NEW, pkg:Android SDK Platform-tools, revision 4.0.1>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 4.0.1 rc1>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n",
+                getTree(m, false /*displaySortByApi*/));
+
+        // Auto select new and update items. In this case the new items are considered
+        // updates and yet new at the same time.
+        // Test by selecting new items only:
+        m.checkNewUpdateItems(true, false, false, SdkConstants.PLATFORM_LINUX);
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=2>\n" +
+                "-- < * NEW, pkg:Android SDK Tools, revision 3.0.1>\n" +
+                "-- < * NEW, pkg:Android SDK Platform-tools, revision 4.0.1>\n" +
+                "PkgCategoryApi <API=TOOLS-PREVIEW, label=Tools (Preview Channel), #items=2>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 3.0.1 rc1>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 4.0.1 rc1>\n" +
+                "PkgCategoryApi <API=API 1, label=Android android-1 (API 1), #items=1>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=0>\n",
+                getTree(m, true /*displaySortByApi*/));
+
+        // Test by selecting update items only:
+        m.uncheckAllItems();
+        m.checkNewUpdateItems(false, true, false, SdkConstants.PLATFORM_LINUX);
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=2>\n" +
+                "-- < * NEW, pkg:Android SDK Tools, revision 3.0.1>\n" +
+                "-- < * NEW, pkg:Android SDK Platform-tools, revision 4.0.1>\n" +
+                "PkgCategoryApi <API=TOOLS-PREVIEW, label=Tools (Preview Channel), #items=2>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 3.0.1 rc1>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 4.0.1 rc1>\n" +
+                "PkgCategoryApi <API=API 1, label=Android android-1 (API 1), #items=1>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=0>\n",
+                getTree(m, true /*displaySortByApi*/));
+
+
+        // -----
+
+        // Now simulate that the user has installed the final package (3.0.1) to replace the
+        // installed 3.0.1rc1 package.
+        // - The 3.0.1 is installed.
+        // - The 3.0.1 rc1 isn't listed anymore by the server.
+
+        m.uncheckAllItems();
+        m.updateStart();
+        m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockToolPackage(src1, new FullRevision(3, 0, 1), 4),          //  T 3.0.1
+                new MockPlatformToolPackage(src1, new FullRevision(4, 0, 1)),     // PT 4.0.1
+                new MockPlatformPackage(src1, 1, 2, 3),    // API 1
+        });
+        m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockToolPackage(src1, new FullRevision(3, 0, 1), 4),          //  T 3.0.1
+                new MockPlatformToolPackage(src1, new FullRevision(4, 0, 1)),     // PT 4.0.1
+        });
+        m.updateEnd(true /*sortByApi*/);
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=2>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 3.0.1>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 4.0.1>\n" +
+                "PkgCategoryApi <API=API 1, label=Android android-1 (API 1), #items=1>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=0>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=repo1 (1.example.com), #items=3>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 3.0.1>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 4.0.1>\n" +
+                "-- <INSTALLED, pkg:SDK Platform Android android-1, API 1, revision 2>\n",
+                getTree(m, false /*displaySortByApi*/));
+    }
+
+    public void testBuildTool_New() {
+        // Test: No local packages installed. Remote server has tools, platform-tools and
+        // build-tools. Even though build-tools isn't a dependency we want to auto-select
+        // the latest one as an install candidate.
+
+        // Enable previews in the settings
+        u.overrideSetting(ISettingsPage.KEY_ENABLE_PREVIEWS, true);
+
+        SdkSource src1 = new SdkRepoSource("http://1.example.com/url1", "repo1");
+
+        m.updateStart();
+        m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockToolPackage        (src1, new FullRevision(2, 0, 0), 3),  // Tools 2
+                new MockPlatformToolPackage(src1, new FullRevision(3, 0, 0)),     // Plat-T 3
+                new MockBuildToolPackage   (src1, new FullRevision(4, 0, 0)),     // Build-T 3
+        });
+        m.updateEnd(true /*sortByApi*/);
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=3>\n" +
+                "-- <NEW, pkg:Android SDK Tools, revision 2>\n" +
+                "-- <NEW, pkg:Android SDK Platform-tools, revision 3>\n" +
+                "-- <NEW, pkg:Android SDK Build-tools, revision 4>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=0>\n",
+                getTree(m, true /*displaySortByApi*/));
+        assertEquals(
+                "PkgCategorySource <source=repo1 (1.example.com), #items=3>\n" +
+                "-- <NEW, pkg:Android SDK Tools, revision 2>\n" +
+                "-- <NEW, pkg:Android SDK Platform-tools, revision 3>\n" +
+                "-- <NEW, pkg:Android SDK Build-tools, revision 4>\n",
+                getTree(m, false /*displaySortByApi*/));
+
+        // Auto select top items. This doesn't selected build-tools since no tools are installed.
+        m.checkNewUpdateItems(false, false, true, SdkConstants.PLATFORM_LINUX);
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=3>\n" +
+                "-- <NEW, pkg:Android SDK Tools, revision 2>\n" +
+                "-- <NEW, pkg:Android SDK Platform-tools, revision 3>\n" +
+                "-- <NEW, pkg:Android SDK Build-tools, revision 4>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=0>\n",
+                getTree(m, true /*displaySortByApi*/));
+
+        // Auto select new items. This obviously selects the build-tools since its new.
+        m.checkNewUpdateItems(true, false, false, SdkConstants.PLATFORM_LINUX);
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=3>\n" +
+                "-- < * NEW, pkg:Android SDK Tools, revision 2>\n" +
+                "-- < * NEW, pkg:Android SDK Platform-tools, revision 3>\n" +
+                "-- < * NEW, pkg:Android SDK Build-tools, revision 4>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=0>\n",
+                getTree(m, true /*displaySortByApi*/));
+    }
+
+    public void testBuildTool_InitialTop() {
+        // Test Build tools auto-selected as an initial top package.
+        // This time we have the tool package installed.
+        // When we first start and select the top packages, we should also auto-select
+        // the latest platform-tools and build-tools if none are installed.
+
+        // Enable previews in the settings
+        u.overrideSetting(ISettingsPage.KEY_ENABLE_PREVIEWS, true);
+
+        SdkSource src1 = new SdkRepoSource("http://1.example.com/url1", "repo1");
+
+        // First the local install only has tools, no plat-tools or build-tools.
+
+        m.uncheckAllItems();
+        m.updateStart();
+        m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockToolPackage        (null, new FullRevision(2, 0, 0), 3),  // Tools 2
+        });
+        m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockToolPackage        (src1, new FullRevision(2, 1, 0), 3),  // Tools 2.1
+                new MockPlatformToolPackage(src1, new FullRevision(3, 0, 0)),     // Plat-T 3.1
+                new MockBuildToolPackage   (src1, new FullRevision(4, 0, 0)),     // Build-T 4.1
+        });
+        m.updateEnd(true /*sortByApi*/);
+
+        // Auto select top items.
+        m.checkNewUpdateItems(false, false, true, SdkConstants.PLATFORM_LINUX);
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=3>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 2, updated by:Android SDK Tools, revision 2.1>\n" +
+                "-- < * NEW, pkg:Android SDK Platform-tools, revision 3>\n" +
+                "-- < * NEW, pkg:Android SDK Build-tools, revision 4>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=0>\n",
+                getTree(m, true /*displaySortByApi*/));
+
+        // Next we start again but this time the local install as all 3 tools.
+        // Auto-selecting the top shouldn't select the updated packages available.
+
+        m.uncheckAllItems();
+        m.updateStart();
+        m.updateSourcePackages(true /*sortByApi*/, null /*locals*/, new Package[] {
+                new MockToolPackage        (null, new FullRevision(2, 0, 0), 3),  // Tools 2
+                new MockPlatformToolPackage(null, new FullRevision(3, 0, 0)),     // Plat-T 3
+                new MockBuildToolPackage   (null, new FullRevision(4, 0, 0)),     // Build-T 4
+        });
+        m.updateSourcePackages(true /*sortByApi*/, src1, new Package[] {
+                new MockToolPackage        (src1, new FullRevision(2, 1, 0), 3),  // Tools 2.1
+                new MockPlatformToolPackage(src1, new FullRevision(3, 1, 0)),     // Plat-T 3.1
+                new MockBuildToolPackage   (src1, new FullRevision(4, 1, 0)),     // Build-T 4.1
+        });
+        m.updateEnd(true /*sortByApi*/);
+
+        // Auto select top items.
+        m.checkNewUpdateItems(false, false, true, SdkConstants.PLATFORM_LINUX);
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=4>\n" +
+                "-- <INSTALLED, pkg:Android SDK Tools, revision 2, updated by:Android SDK Tools, revision 2.1>\n" +
+                "-- <INSTALLED, pkg:Android SDK Platform-tools, revision 3, updated by:Android SDK Platform-tools, revision 3.1>\n" +
+                "-- <NEW, pkg:Android SDK Build-tools, revision 4.1>\n" +
+                "-- <INSTALLED, pkg:Android SDK Build-tools, revision 4>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=0>\n",
+                getTree(m, true /*displaySortByApi*/));
+
+        // If we do request updates + top, they are selected however except for build-tools
+        // since new versions are not considered as updates.
+        m.uncheckAllItems();
+        m.checkNewUpdateItems(false, true, true, SdkConstants.PLATFORM_LINUX);
+
+        assertEquals(
+                "PkgCategoryApi <API=TOOLS, label=Tools, #items=4>\n" +
+                "-- < * INSTALLED, pkg:Android SDK Tools, revision 2, updated by:Android SDK Tools, revision 2.1>\n" +
+                "-- < * INSTALLED, pkg:Android SDK Platform-tools, revision 3, updated by:Android SDK Platform-tools, revision 3.1>\n" +
+                "-- <NEW, pkg:Android SDK Build-tools, revision 4.1>\n" +
+                "-- <INSTALLED, pkg:Android SDK Build-tools, revision 4>\n" +
+                "PkgCategoryApi <API=EXTRAS, label=Extras, #items=0>\n",
+                getTree(m, true /*displaySortByApi*/));
+    }
+
+
+
+    // ----
+
+    /**
+     * Simulates the display we would have in the Packages Tree.
+     * This always depends on mCurrentCategories like the tree does.
+     * The display format is something like:
+     * <pre>
+     *   PkgCategory <description>
+     *   -- <PkgItem description>
+     * </pre>
+     */
+    public String getTree(PackagesDiffLogic l, boolean displaySortByApi) {
+        StringBuilder sb = new StringBuilder();
+
+        for (PkgCategory cat : m.getCategories(displaySortByApi)) {
+            sb.append(cat.toString()).append('\n');
+            for (PkgItem item : cat.getItems()) {
+                sb.append("-- ").append(item.toString()).append('\n');
+            }
+        }
+
+        return sb.toString();
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/test/java/com/android/sdkuilib/internal/repository/ui/MockPackagesPageImpl.java b/sdkmanager/sdkuilib/src/test/java/com/android/sdkuilib/internal/repository/ui/MockPackagesPageImpl.java
new file mode 100755
index 0000000..fe854d8
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/test/java/com/android/sdkuilib/internal/repository/ui/MockPackagesPageImpl.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository.ui;
+
+import com.android.sdklib.internal.repository.DownloadCache;
+import com.android.sdklib.internal.repository.MockDownloadCache;
+import com.android.sdklib.internal.repository.DownloadCache.Strategy;
+import com.android.sdklib.internal.repository.updater.PackageLoader;
+import com.android.sdklib.util.SparseIntArray;
+import com.android.sdkuilib.internal.repository.SwtUpdaterData;
+import com.android.sdkuilib.internal.repository.core.PkgCategory;
+import com.android.sdkuilib.internal.repository.core.PkgContentProvider;
+
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.swt.graphics.Font;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MockPackagesPageImpl extends PackagesPageImpl {
+
+    public MockPackagesPageImpl(SwtUpdaterData swtUpdaterData) {
+        super(swtUpdaterData);
+    }
+
+    /** UI is never disposed in the unit test. */
+    @Override
+    protected boolean isUiDisposed() {
+        return false;
+    }
+
+    /** Sync exec always executes immediately in the unit test, no threading is used. */
+    @Override
+    protected void syncExec(Runnable runnable) {
+        runnable.run();
+    }
+
+    @Override
+    protected void syncViewerSelection() {
+        // No-op. There is no real tree viewer to synchronize.
+    }
+
+    private MockTreeViewer mTreeViewer;
+
+    @Override
+    void postCreate() {
+        mTreeViewer = new MockTreeViewer();
+        setITreeViewer(mTreeViewer);
+
+        setIColumns(new MockTreeColumn(mTreeViewer),  // columnName
+                    new MockTreeColumn(mTreeViewer),  // columnApi
+                    new MockTreeColumn(mTreeViewer),  // columnRevision
+                    new MockTreeColumn(mTreeViewer)); // columnStatus
+
+        super.postCreate();
+    }
+
+    @Override
+    protected void refreshViewerInput() {
+        super.setViewerInput();
+    }
+
+    @Override
+    protected boolean isSortByApi() {
+        return true;
+    }
+
+    @Override
+    protected Font getTreeFontItalic() {
+        return null;
+    }
+
+    @Override
+    protected void loadPackages(boolean useLocalCache, boolean overrideExisting) {
+        super.loadPackagesImpl(useLocalCache, overrideExisting);
+    }
+
+    /**
+     * In this mock version, we use the default {@link PackageLoader} which will
+     * use the {@link DownloadCache} from the {@link SwtUpdaterData}. This should be
+     * the mock download cache, in which case we change the strategy at run-time
+     * to set it to only-cache on the first manager update.
+     */
+    @Override
+    protected PackageLoader getPackageLoader(boolean useLocalCache) {
+        DownloadCache dc = mSwtUpdaterData.getDownloadCache();
+        assert dc instanceof MockDownloadCache;
+        if (dc instanceof MockDownloadCache) {
+            ((MockDownloadCache) dc).overrideStrategy(useLocalCache ? Strategy.ONLY_CACHE : null);
+        }
+        return mSwtUpdaterData.getPackageLoader();
+    }
+
+    /**
+     * Get a dump-out of the tree in a format suitable for unit testing.
+     */
+    public String getMockTreeDisplay() throws Exception {
+        return mTreeViewer.getTreeDisplay();
+    }
+
+    private static class MockTreeViewer implements PackagesPageImpl.ICheckboxTreeViewer {
+        private final SparseIntArray mWidths = new SparseIntArray();
+        private final List<MockTreeColumn> mColumns = new ArrayList<MockTreeColumn>();
+        private List<PkgCategory> mInput;
+        private PkgContentProvider mPkgContentProvider;
+        private String mLastRefresh;
+        private static final String SPACE = "                                                 ";
+
+        @Override
+        public void setInput(List<PkgCategory> input) {
+            mInput = input;
+            refresh();
+        }
+
+        @Override
+        public Object getInput() {
+            return mInput;
+        }
+
+        @Override
+        public void setContentProvider(PkgContentProvider pkgContentProvider) {
+            mPkgContentProvider = pkgContentProvider;
+        }
+
+        @Override
+        public void refresh() {
+            // Recompute the display of the tree
+            StringBuilder sb = new StringBuilder();
+            boolean widthChanged = false;
+
+            for (int render = 0; render < (widthChanged ? 2 : 1); render++) {
+                widthChanged = false;
+                sb.setLength(0);
+                for (Object cat : mPkgContentProvider.getElements(mInput)) {
+                    if (cat == null) {
+                        continue;
+                    }
+
+                    if (sb.length() > 0) {
+                        sb.append('\n');
+                    }
+
+                    widthChanged |= rowAsString(cat, sb, 3);
+
+                    Object[] children = mPkgContentProvider.getElements(cat);
+                    if (children == null) {
+                        continue;
+                    }
+                    for (Object child : children) {
+                        sb.append("\n L_");
+                        widthChanged |= rowAsString(child, sb, 0);
+                    }
+                }
+            }
+
+            mLastRefresh = sb.toString();
+        }
+
+        boolean rowAsString(Object element, StringBuilder sb, int space) {
+            boolean widthChanged = false;
+            sb.append("[] ");
+            for (int col = 0; col < mColumns.size(); col++) {
+                if (col > 0) {
+                    sb.append(" | ");
+                }
+                String t = mColumns.get(col).getLabelProvider().getText(element);
+                if (t == null) {
+                    t = "(null)";
+                }
+                int len = t.length();
+                int w = mWidths.get(col);
+                if (len > w) {
+                    widthChanged = true;
+                    mWidths.put(col, len);
+                    w = len;
+                }
+                String pad = len >= w ? "" : SPACE.substring(SPACE.length() - w + len);
+                if (col == 0 && space > 0) {
+                    sb.append(SPACE.substring(SPACE.length() - space));
+                }
+                if (col >= 1 && col <= 2) {
+                    sb.append(pad);
+                }
+                sb.append(t);
+                if (col == 0 || col > 2) {
+                    sb.append(pad);
+                }
+            }
+            return widthChanged;
+        }
+
+        @Override
+        public Object[] getCheckedElements() {
+            return null;
+        }
+
+        public void addColumn(MockTreeColumn mockTreeColumn) {
+            mColumns.add(mockTreeColumn);
+        }
+
+        public String getTreeDisplay() {
+            return mLastRefresh;
+        }
+    }
+
+    private static class MockTreeColumn implements PackagesPageImpl.ITreeViewerColumn {
+        private ColumnLabelProvider mLabelProvider;
+
+        public MockTreeColumn(MockTreeViewer treeViewer) {
+            treeViewer.addColumn(this);
+        }
+
+        @Override
+        public void setLabelProvider(ColumnLabelProvider labelProvider) {
+            mLabelProvider = labelProvider;
+        }
+
+        public ColumnLabelProvider getLabelProvider() {
+            return mLabelProvider;
+        }
+    }
+}
diff --git a/sdkmanager/sdkuilib/src/test/java/com/android/sdkuilib/internal/repository/ui/SdkManagerUpgradeTest.java b/sdkmanager/sdkuilib/src/test/java/com/android/sdkuilib/internal/repository/ui/SdkManagerUpgradeTest.java
new file mode 100755
index 0000000..a31cbac
--- /dev/null
+++ b/sdkmanager/sdkuilib/src/test/java/com/android/sdkuilib/internal/repository/ui/SdkManagerUpgradeTest.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository.ui;
+
+import com.android.sdklib.SdkManager;
+import com.android.sdklib.SdkManagerTestCase;
+import com.android.sdklib.internal.repository.MockDownloadCache;
+import com.android.sdklib.internal.repository.updater.ISettingsPage;
+import com.android.sdklib.repository.SdkRepoConstants;
+import com.android.sdkuilib.internal.repository.MockSwtUpdaterData;
+
+import java.util.Arrays;
+
+public class SdkManagerUpgradeTest extends SdkManagerTestCase {
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    /**
+     * Create a mock page and list the current SDK state
+     */
+    public void testPackagesPage1() throws Exception {
+        SdkManager sdkman = getSdkManager();
+
+        MockSwtUpdaterData updaterData = new MockSwtUpdaterData(sdkman);
+        MockDownloadCache cache = (MockDownloadCache) updaterData.getDownloadCache();
+        updaterData.setupDefaultSources();
+
+        MockPackagesPageImpl pageImpl = new MockPackagesPageImpl(updaterData);
+        pageImpl.postCreate();
+        pageImpl.performFirstLoad();
+
+        // We have no network access possible and no mock download cache items.
+        // The only thing visible in the display are the local packages as set by
+        // the fake locally-installed SDK.
+        String actual = pageImpl.getMockTreeDisplay();
+        assertEquals(
+                "[]    Tools                      |  |            |          \n" +
+                " L_[] Android SDK Tools          |  |      1.0.1 | Installed\n" +
+                " L_[] Android SDK Platform-tools |  |     17.1.2 | Installed\n" +
+                " L_[] Android SDK Build-tools    |  |      3.0.1 | Installed\n" +
+                " L_[] Android SDK Build-tools    |  |          3 | Installed\n" +
+                "[]    Tools (Preview Channel)    |  |            |          \n" +
+                " L_[] Android SDK Build-tools    |  | 12.3.4 rc5 | Installed\n" +
+                "[]    Android 0.0 (API 0)        |  |            |          \n" +
+                " L_[] SDK Platform               |  |          1 | Installed\n" +
+                " L_[] Sources for Android SDK    |  |          0 | Installed\n" +
+                "[]    Extras                     |  |            |          ",
+                actual);
+
+        assertEquals(
+                "[]",  // there are no direct downloads till we try to install.
+                Arrays.toString(cache.getDirectHits()));
+        assertEquals(
+                "[<https://dl-ssl.google.com/android/repository/addons_list-1.xml : 1>, " +
+                 "<https://dl-ssl.google.com/android/repository/addons_list-2.xml : 1>, " +
+                 "<https://dl-ssl.google.com/android/repository/repository-5.xml : 2>, " +
+                 "<https://dl-ssl.google.com/android/repository/repository-6.xml : 2>, " +
+                 "<https://dl-ssl.google.com/android/repository/repository-7.xml : 2>, " +
+                 "<https://dl-ssl.google.com/android/repository/repository-8.xml : 2>, " +
+                 "<https://dl-ssl.google.com/android/repository/repository.xml : 2>]",
+                Arrays.toString(cache.getCachedHits()));
+
+
+        // Now prepare a tools update on the server and reload, with previews disabled.
+        setupToolsXml1(cache);
+        cache.clearDirectHits();
+        cache.clearCachedHits();
+        updaterData.overrideSetting(ISettingsPage.KEY_ENABLE_PREVIEWS, false);
+        pageImpl.fullReload();
+
+        actual = pageImpl.getMockTreeDisplay();
+        assertEquals(
+                "[]    Tools                      |  |            |                              \n" +
+                " L_[] Android SDK Tools          |  |      1.0.1 | Update available: rev. 20.0.3\n" +
+                " L_[] Android SDK Platform-tools |  |     17.1.2 | Update available: rev. 18    \n" +
+                " L_[] Android SDK Build-tools    |  |         18 | Not installed                \n" +
+                " L_[] Android SDK Build-tools    |  |      3.0.1 | Installed                    \n" +
+                " L_[] Android SDK Build-tools    |  |          3 | Installed                    \n" +
+                "[]    Tools (Preview Channel)    |  |            |                              \n" +
+                // Note: locally installed previews are always shown, even when enable previews is false.
+                " L_[] Android SDK Build-tools    |  | 12.3.4 rc5 | Installed                    \n" +
+                "[]    Android 0.0 (API 0)        |  |            |                              \n" +
+                " L_[] SDK Platform               |  |          1 | Installed                    \n" +
+                " L_[] Sources for Android SDK    |  |          0 | Installed                    \n" +
+                "[]    Extras                     |  |            |                              ",
+                actual);
+
+        assertEquals(
+                "[]",  // there are no direct downloads till we try to install.
+                Arrays.toString(cache.getDirectHits()));
+        assertEquals(
+                "[<https://dl-ssl.google.com/android/repository/repository-5.xml : 1>, " +
+                 "<https://dl-ssl.google.com/android/repository/repository-6.xml : 1>, " +
+                 "<https://dl-ssl.google.com/android/repository/repository-7.xml : 1>, " +
+                 "<https://dl-ssl.google.com/android/repository/repository-8.xml : 1>, " +
+                 "<https://dl-ssl.google.com/android/repository/repository.xml : 1>]",
+                Arrays.toString(cache.getCachedHits()));
+
+
+        // We should get the same display if we restart the manager page from scratch
+        // (e.g. simulate a first load)
+
+        cache.clearDirectHits();
+        cache.clearCachedHits();
+        pageImpl = new MockPackagesPageImpl(updaterData);
+        pageImpl.postCreate();
+        pageImpl.performFirstLoad();
+
+        actual = pageImpl.getMockTreeDisplay();
+        assertEquals(
+                "[]    Tools                      |  |            |                              \n" +
+                " L_[] Android SDK Tools          |  |      1.0.1 | Update available: rev. 20.0.3\n" +
+                " L_[] Android SDK Platform-tools |  |     17.1.2 | Update available: rev. 18    \n" +
+                " L_[] Android SDK Build-tools    |  |         18 | Not installed                \n" +
+                " L_[] Android SDK Build-tools    |  |      3.0.1 | Installed                    \n" +
+                " L_[] Android SDK Build-tools    |  |          3 | Installed                    \n" +
+                "[]    Tools (Preview Channel)    |  |            |                              \n" +
+                " L_[] Android SDK Build-tools    |  | 12.3.4 rc5 | Installed                    \n" +
+                "[]    Android 0.0 (API 0)        |  |            |                              \n" +
+                " L_[] SDK Platform               |  |          1 | Installed                    \n" +
+                " L_[] Sources for Android SDK    |  |          0 | Installed                    \n" +
+                "[]    Extras                     |  |            |                              ",
+                actual);
+
+        assertEquals(
+                "[]",  // there are no direct downloads till we try to install.
+                Arrays.toString(cache.getDirectHits()));
+        assertEquals(
+                "[<https://dl-ssl.google.com/android/repository/repository-5.xml : 1>, " +
+                "<https://dl-ssl.google.com/android/repository/repository-6.xml : 1>, " +
+                "<https://dl-ssl.google.com/android/repository/repository-7.xml : 1>, " +
+                "<https://dl-ssl.google.com/android/repository/repository-8.xml : 1>, " +
+                "<https://dl-ssl.google.com/android/repository/repository.xml : 1>]",
+                Arrays.toString(cache.getCachedHits()));
+
+
+        // Now simulate a reload but this time enable previews.
+
+        cache.clearDirectHits();
+        cache.clearCachedHits();
+        pageImpl = new MockPackagesPageImpl(updaterData);
+        pageImpl.postCreate();
+        updaterData.overrideSetting(ISettingsPage.KEY_ENABLE_PREVIEWS, true);
+        pageImpl.performFirstLoad();
+
+        actual = pageImpl.getMockTreeDisplay();
+        assertEquals(
+                "[]    Tools                      |  |            |                                   \n" +
+                " L_[] Android SDK Tools          |  |      1.0.1 | Update available: rev. 20.0.3     \n" +
+                " L_[] Android SDK Platform-tools |  |     17.1.2 | Update available: rev. 18         \n" +
+                " L_[] Android SDK Build-tools    |  |         18 | Not installed                     \n" +
+                " L_[] Android SDK Build-tools    |  |      3.0.1 | Installed                         \n" +
+                " L_[] Android SDK Build-tools    |  |          3 | Installed                         \n" +
+                "[]    Tools (Preview Channel)    |  |            |                                   \n" +
+                " L_[] Android SDK Build-tools    |  | 12.3.4 rc5 | Update available: rev. 12.3.4 rc15\n" +
+                "[]    Android 0.0 (API 0)        |  |            |                                   \n" +
+                " L_[] SDK Platform               |  |          1 | Installed                         \n" +
+                " L_[] Sources for Android SDK    |  |          0 | Installed                         \n" +
+                "[]    Extras                     |  |            |                                   ",
+                actual);
+
+        assertEquals(
+                "[]",  // there are no direct downloads till we try to install.
+                Arrays.toString(cache.getDirectHits()));
+        assertEquals(
+                "[<https://dl-ssl.google.com/android/repository/repository-5.xml : 1>, " +
+                "<https://dl-ssl.google.com/android/repository/repository-6.xml : 1>, " +
+                "<https://dl-ssl.google.com/android/repository/repository-7.xml : 1>, " +
+                "<https://dl-ssl.google.com/android/repository/repository-8.xml : 1>, " +
+                "<https://dl-ssl.google.com/android/repository/repository.xml : 1>]",
+                Arrays.toString(cache.getCachedHits()));
+    }
+
+    private void setupToolsXml1(MockDownloadCache cache) throws Exception {
+        String repoXml =
+            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+            "<sdk:sdk-repository xmlns:sdk=\"http://schemas.android.com/sdk/android/repository/8\" " +
+            "                    xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n" +
+            "<sdk:license id=\"android-sdk-license\" type=\"text\">Blah blah blah.</sdk:license>\n" +
+            "\n" +
+            "<sdk:build-tool>\n" +
+            "    <sdk:revision>\n" +
+            "        <sdk:major>18</sdk:major>\n" +
+            "    </sdk:revision>\n" +
+            "    <sdk:archives>\n" +
+            "        <sdk:archive arch=\"any\" os=\"windows\">\n" +
+            "            <sdk:size>11159472</sdk:size>\n" +
+            "            <sdk:checksum type=\"sha1\">6028258d8f2fba14d8b40c3cf507afa0289aaa13</sdk:checksum>\n" +
+            "            <sdk:url>platform-tools_r18-windows.zip</sdk:url>\n" +
+            "        </sdk:archive>\n" +
+            "        <sdk:archive arch=\"any\" os=\"linux\">\n" +
+            "            <sdk:size>10985068</sdk:size>\n" +
+            "            <sdk:checksum type=\"sha1\">6e2bc329c9485eb383172cbc2cde8b0c0cd1843f</sdk:checksum>\n" +
+            "            <sdk:url>platform-tools_r18-linux.zip</sdk:url>\n" +
+            "        </sdk:archive>\n" +
+            "        <sdk:archive arch=\"any\" os=\"macosx\">\n" +
+            "            <sdk:size>11342461</sdk:size>\n" +
+            "            <sdk:checksum type=\"sha1\">4a015090c6a209fc33972acdbc65745e0b3c08b9</sdk:checksum>\n" +
+            "            <sdk:url>platform-tools_r18-macosx.zip</sdk:url>\n" +
+            "        </sdk:archive>\n" +
+            "    </sdk:archives>\n" +
+            "</sdk:build-tool>\n" +
+            "\n" +
+            "<sdk:build-tool>\n" +
+            "    <sdk:revision>\n" +
+            "        <sdk:major>12</sdk:major>\n" +
+            "        <sdk:minor>3</sdk:minor>\n" +
+            "        <sdk:micro>4</sdk:micro>\n" +
+            "        <sdk:preview>15</sdk:preview>\n" +
+            "    </sdk:revision>\n" +
+            "    <sdk:archives>\n" +
+            "        <sdk:archive arch=\"any\" os=\"windows\">\n" +
+            "            <sdk:size>11159472</sdk:size>\n" +
+            "            <sdk:checksum type=\"sha1\">6028258d8f2fba14d8b40c3cf507afa0289aaa13</sdk:checksum>\n" +
+            "            <sdk:url>platform-tools_r18-windows.zip</sdk:url>\n" +
+            "        </sdk:archive>\n" +
+            "        <sdk:archive arch=\"any\" os=\"linux\">\n" +
+            "            <sdk:size>10985068</sdk:size>\n" +
+            "            <sdk:checksum type=\"sha1\">6e2bc329c9485eb383172cbc2cde8b0c0cd1843f</sdk:checksum>\n" +
+            "            <sdk:url>platform-tools_r18-linux.zip</sdk:url>\n" +
+            "        </sdk:archive>\n" +
+            "        <sdk:archive arch=\"any\" os=\"macosx\">\n" +
+            "            <sdk:size>11342461</sdk:size>\n" +
+            "            <sdk:checksum type=\"sha1\">4a015090c6a209fc33972acdbc65745e0b3c08b9</sdk:checksum>\n" +
+            "            <sdk:url>platform-tools_r18-macosx.zip</sdk:url>\n" +
+            "        </sdk:archive>\n" +
+            "    </sdk:archives>\n" +
+            "</sdk:build-tool>\n" +
+            "\n" +
+            "<sdk:platform-tool>\n" +
+            "    <sdk:revision>\n" +
+            "        <sdk:major>18</sdk:major>\n" +
+            "    </sdk:revision>\n" +
+            "    <sdk:archives>\n" +
+            "        <sdk:archive arch=\"any\" os=\"windows\">\n" +
+            "            <sdk:size>11159472</sdk:size>\n" +
+            "            <sdk:checksum type=\"sha1\">6028258d8f2fba14d8b40c3cf507afa0289aaa13</sdk:checksum>\n" +
+            "            <sdk:url>platform-tools_r18-windows.zip</sdk:url>\n" +
+            "        </sdk:archive>\n" +
+            "        <sdk:archive arch=\"any\" os=\"linux\">\n" +
+            "            <sdk:size>10985068</sdk:size>\n" +
+            "            <sdk:checksum type=\"sha1\">6e2bc329c9485eb383172cbc2cde8b0c0cd1843f</sdk:checksum>\n" +
+            "            <sdk:url>platform-tools_r18-linux.zip</sdk:url>\n" +
+            "        </sdk:archive>\n" +
+            "        <sdk:archive arch=\"any\" os=\"macosx\">\n" +
+            "            <sdk:size>11342461</sdk:size>\n" +
+            "            <sdk:checksum type=\"sha1\">4a015090c6a209fc33972acdbc65745e0b3c08b9</sdk:checksum>\n" +
+            "            <sdk:url>platform-tools_r18-macosx.zip</sdk:url>\n" +
+            "        </sdk:archive>\n" +
+            "    </sdk:archives>\n" +
+            "</sdk:platform-tool>\n" +
+            "\n" +
+            "<sdk:tool>\n" +
+            "    <sdk:revision>\n" +
+            "        <sdk:major>20</sdk:major>\n" +
+            "        <sdk:minor>0</sdk:minor>\n" +
+            "        <sdk:micro>3</sdk:micro>\n" +
+            "    </sdk:revision>\n" +
+            "    <sdk:min-platform-tools-rev>\n" +
+            "        <sdk:major>18</sdk:major>\n" +
+            "    </sdk:min-platform-tools-rev>\n" +
+            "    <sdk:archives>\n" +
+            "        <sdk:archive arch=\"any\" os=\"windows\">\n" +
+            "            <sdk:size>90272048</sdk:size>\n" +
+            "            <sdk:checksum type=\"sha1\">54fb94168e631e211910f88aa40c532205730dd4</sdk:checksum>\n" +
+            "            <sdk:url>tools_r20.0.3-windows.zip</sdk:url>\n" +
+            "        </sdk:archive>\n" +
+            "        <sdk:archive arch=\"any\" os=\"linux\">\n" +
+            "            <sdk:size>82723559</sdk:size>\n" +
+            "            <sdk:checksum type=\"sha1\">09bc633b406ae81981e3a0db19426acbb01ef219</sdk:checksum>\n" +
+            "            <sdk:url>tools_r20.0.3-linux.zip</sdk:url>\n" +
+            "        </sdk:archive>\n" +
+            "        <sdk:archive arch=\"any\" os=\"macosx\">\n" +
+            "            <sdk:size>58197071</sdk:size>\n" +
+            "            <sdk:checksum type=\"sha1\">09cee5ff3226277a6f0c07dcd29cba4ffc2e1da4</sdk:checksum>\n" +
+            "            <sdk:url>tools_r20.0.3-macosx.zip</sdk:url>\n" +
+            "        </sdk:archive>\n" +
+            "    </sdk:archives>\n" +
+            "</sdk:tool>\n" +
+            "\n" +
+            "</sdk:sdk-repository>\n";
+
+        String url = SdkRepoConstants.URL_GOOGLE_SDK_SITE +
+           String.format(SdkRepoConstants.URL_DEFAULT_FILENAME, SdkRepoConstants.NS_LATEST_VERSION);
+
+        cache.registerCachedPayload(url, repoXml.getBytes("UTF-8"));
+    }
+
+}
diff --git a/sdkstats/.classpath b/sdkstats/.classpath
new file mode 100644
index 0000000..138980a
--- /dev/null
+++ b/sdkstats/.classpath
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src/main/java"/>
+	<classpathentry kind="src" path="src/test/java"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+	<classpathentry exported="true" kind="var" path="ANDROID_OUT_FRAMEWORK/swt.jar"/>
+	<classpathentry exported="true" kind="var" path="ANDROID_SRC/prebuilts/tools/common/m2/repository/com/android/external/eclipse/org-eclipse-core-commands/3.6.0/org-eclipse-core-commands-3.6.0.jar"/>
+	<classpathentry exported="true" kind="var" path="ANDROID_SRC/prebuilts/tools/common/m2/repository/com/android/external/eclipse/org-eclipse-equinox-common/3.6.0/org-eclipse-equinox-common-3.6.0.jar"/>
+	<classpathentry exported="true" kind="var" path="ANDROID_SRC/prebuilts/tools/common/m2/repository/com/android/external/eclipse/org-eclipse-jface/3.6.2/org-eclipse-jface-3.6.2.jar"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/3"/>
+	<classpathentry combineaccessrules="false" exported="true" kind="src" path="/common"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/sdkstats/.project b/sdkstats/.project
new file mode 100644
index 0000000..fdc8593
--- /dev/null
+++ b/sdkstats/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>sdkstats</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>
diff --git a/sdkstats/.settings/README.txt b/sdkstats/.settings/README.txt
new file mode 100644
index 0000000..9120b20
--- /dev/null
+++ b/sdkstats/.settings/README.txt
@@ -0,0 +1,2 @@
+Copy this in eclipse project as a .settings folder at the root.
+This ensure proper compilation compliance and warning/error levels.
\ No newline at end of file
diff --git a/sdkstats/.settings/org.eclipse.jdt.core.prefs b/sdkstats/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..9dbff07
--- /dev/null
+++ b/sdkstats/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,98 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
+org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault
+org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled
+org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
+org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
+org.eclipse.jdt.core.compiler.problem.deadCode=warning
+org.eclipse.jdt.core.compiler.problem.deprecation=warning
+org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
+org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
+org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
+org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning
+org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled
+org.eclipse.jdt.core.compiler.problem.fieldHiding=warning
+org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
+org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=error
+org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
+org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled
+org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
+org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning
+org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning
+org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
+org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
+org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
+org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
+org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
+org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
+org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
+org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
+org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
+org.eclipse.jdt.core.compiler.problem.nullReference=error
+org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning
+org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore
+org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
+org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning
+org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
+org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
+org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
+org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled
+org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
+org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
+org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
+org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled
+org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning
+org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error
+org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning
+org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.unusedImport=warning
+org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
+org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
+org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning
+org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled
+org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
+org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
+org.eclipse.jdt.core.compiler.source=1.6
diff --git a/sdkstats/NOTICE b/sdkstats/NOTICE
new file mode 100644
index 0000000..c5b1efa
--- /dev/null
+++ b/sdkstats/NOTICE
@@ -0,0 +1,190 @@
+
+   Copyright (c) 2005-2008, 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.
+
+   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.
+
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
diff --git a/sdkstats/README b/sdkstats/README
new file mode 100644
index 0000000..8ed0880
--- /dev/null
+++ b/sdkstats/README
@@ -0,0 +1,11 @@
+How to use the Eclipse projects for SdkStats.
+
+SdkStats requires SWT to compile.
+
+SWT is available in the depot under //device/prebuild/<platform>/swt
+
+Because the build path cannot contain relative path that are not inside the project directory,
+the .classpath file references a user library called ANDROID_SWT.
+
+In order to compile the project, make a user library called ANDROID_SWT containing the jar
+available at //device/prebuild/<platform>/swt.
diff --git a/sdkstats/src/main/java/com/android/sdkstats/DdmsPreferenceStore.java b/sdkstats/src/main/java/com/android/sdkstats/DdmsPreferenceStore.java
new file mode 100755
index 0000000..890eae7
--- /dev/null
+++ b/sdkstats/src/main/java/com/android/sdkstats/DdmsPreferenceStore.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) 2007 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.sdkstats;
+
+import com.android.prefs.AndroidLocation;
+import com.android.prefs.AndroidLocation.AndroidLocationException;
+
+import org.eclipse.jface.preference.PreferenceStore;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Random;
+
+/**
+ * Manages persistence settings for DDMS.
+ *
+ * For convenience, this also stores persistence settings related to the "server stats" ping
+ * as well as some ADT settings that are SDK specific but not workspace specific.
+ */
+public class DdmsPreferenceStore {
+
+    public  final static String PING_OPT_IN = "pingOptIn";          //$NON-NLS-1$
+    private final static String PING_TIME   = "pingTime";           //$NON-NLS-1$
+    private final static String PING_ID     = "pingId";             //$NON-NLS-1$
+
+    private final static String ADT_USED = "adtUsed";               //$NON-NLS-1$
+    private final static String LAST_SDK_PATH = "lastSdkPath";      //$NON-NLS-1$
+
+    /**
+     * PreferenceStore for DDMS.
+     * Creation and usage must be synchronized on {@code DdmsPreferenceStore.class}.
+     * Don't use it directly, instead retrieve it via {@link #getPreferenceStore()}.
+     */
+    private static volatile PreferenceStore sPrefStore;
+
+    public DdmsPreferenceStore() {
+    }
+
+    /**
+     * Returns the DDMS {@link PreferenceStore}.
+     * This keeps a static reference on the store, so consequent calls will
+     * return always the same store.
+     */
+    public PreferenceStore getPreferenceStore() {
+        synchronized (DdmsPreferenceStore.class) {
+            if (sPrefStore == null) {
+                // get the location of the preferences
+                String homeDir = null;
+                try {
+                    homeDir = AndroidLocation.getFolder();
+                } catch (AndroidLocationException e1) {
+                    // pass, we'll do a dummy store since homeDir is null
+                }
+
+                if (homeDir == null) {
+                    sPrefStore = new PreferenceStore();
+                    return sPrefStore;
+                }
+
+                assert homeDir != null;
+
+                String rcFileName = homeDir + "ddms.cfg";                       //$NON-NLS-1$
+
+                // also look for an old pref file in the previous location
+                String oldPrefPath = System.getProperty("user.home")            //$NON-NLS-1$
+                    + File.separator + ".ddmsrc";                               //$NON-NLS-1$
+                File oldPrefFile = new File(oldPrefPath);
+                if (oldPrefFile.isFile()) {
+                    FileOutputStream fileOutputStream = null;
+                    try {
+                        PreferenceStore oldStore = new PreferenceStore(oldPrefPath);
+                        oldStore.load();
+
+                        fileOutputStream = new FileOutputStream(rcFileName);
+                        oldStore.save(fileOutputStream, "");    //$NON-NLS-1$
+                        oldPrefFile.delete();
+
+                        PreferenceStore newStore = new PreferenceStore(rcFileName);
+                        newStore.load();
+                        sPrefStore = newStore;
+                    } catch (IOException e) {
+                        // create a new empty store.
+                        sPrefStore = new PreferenceStore(rcFileName);
+                    } finally {
+                        if (fileOutputStream != null) {
+                            try {
+                                fileOutputStream.close();
+                            } catch (IOException e) {
+                                // pass
+                            }
+                        }
+                    }
+                } else {
+                    sPrefStore = new PreferenceStore(rcFileName);
+
+                    try {
+                        sPrefStore.load();
+                    } catch (IOException e) {
+                        System.err.println("Error Loading DDMS Preferences");
+                    }
+                }
+            }
+
+            assert sPrefStore != null;
+            return sPrefStore;
+        }
+    }
+
+    /**
+     * Save the prefs to the config file.
+     */
+    public void save() {
+        PreferenceStore prefs = getPreferenceStore();
+        synchronized (DdmsPreferenceStore.class) {
+            try {
+                prefs.save();
+            }
+            catch (IOException ioe) {
+                // FIXME com.android.dmmlib.Log.w("ddms", "Failed saving prefs file: " + ioe.getMessage());
+            }
+        }
+    }
+
+    // ---- Utility methods to access some specific prefs ----
+
+    /**
+     * Indicates whether the ping ID is set.
+     * This should be true when {@link #isPingOptIn()} is true.
+     *
+     * @return true if a ping ID is set, which means the user gave permission
+     *              to use the ping service.
+     */
+    public boolean hasPingId() {
+        PreferenceStore prefs = getPreferenceStore();
+        synchronized (DdmsPreferenceStore.class) {
+            return prefs != null && prefs.contains(PING_ID);
+        }
+    }
+
+    /**
+     * Retrieves the current ping ID, if set.
+     * To know if the ping ID is set, use {@link #hasPingId()}.
+     * <p/>
+     * There is no magic value reserved for "missing ping id or invalid store".
+     * The only proper way to know if the ping id is missing is to use {@link #hasPingId()}.
+     */
+    public long getPingId() {
+        PreferenceStore prefs = getPreferenceStore();
+        synchronized (DdmsPreferenceStore.class) {
+            // Note: getLong() returns 0L if the ID is missing so we do that too when
+            // there's no store.
+            return prefs == null ? 0L : prefs.getLong(PING_ID);
+        }
+    }
+
+    /**
+     * Generates a new random ping ID and saves it in the preference store.
+     *
+     * @return The new ping ID.
+     */
+    public long generateNewPingId() {
+        PreferenceStore prefs = getPreferenceStore();
+
+        Random rnd = new Random();
+        long id = rnd.nextLong();
+
+        synchronized (DdmsPreferenceStore.class) {
+            prefs.setValue(PING_ID, id);
+            try {
+                prefs.save();
+            } catch (IOException e) {
+                /* ignore exceptions while saving preferences */
+            }
+        }
+
+        return id;
+    }
+
+    /**
+     * Returns the "ping opt in" value from the preference store.
+     * This would be true if there's a valid preference store and
+     * the user opted for sending ping statistics.
+     */
+    public boolean isPingOptIn() {
+        PreferenceStore prefs = getPreferenceStore();
+        synchronized (DdmsPreferenceStore.class) {
+            return prefs != null && prefs.contains(PING_OPT_IN);
+        }
+    }
+
+    /**
+     * Saves the "ping opt in" value in the preference store.
+     *
+     * @param optIn The new user opt-in value.
+     */
+    public void setPingOptIn(boolean optIn) {
+        PreferenceStore prefs = getPreferenceStore();
+
+        synchronized (DdmsPreferenceStore.class) {
+            prefs.setValue(PING_OPT_IN, optIn);
+            try {
+                prefs.save();
+            } catch (IOException e) {
+                /* ignore exceptions while saving preferences */
+            }
+        }
+    }
+
+    /**
+     * Retrieves the ping time for the given app from the preference store.
+     * Callers should use {@link System#currentTimeMillis()} for time stamps.
+     *
+     * @param app The app name identifier.
+     * @return 0L if we don't have a preference store or there was no time
+     *  recorded in the store for the requested app. Otherwise the time stamp
+     *  from the store.
+     */
+    public long getPingTime(String app) {
+        PreferenceStore prefs = getPreferenceStore();
+        String timePref = PING_TIME + "." + app;  //$NON-NLS-1$
+        synchronized (DdmsPreferenceStore.class) {
+            return prefs == null ? 0 : prefs.getLong(timePref);
+        }
+    }
+
+    /**
+     * Sets the ping time for the given app from the preference store.
+     * Callers should use {@link System#currentTimeMillis()} for time stamps.
+     *
+     * @param app The app name identifier.
+     * @param timeStamp The time stamp from the store.
+     *                   0L is a special value that should not be used.
+     */
+    public void setPingTime(String app, long timeStamp) {
+        PreferenceStore prefs = getPreferenceStore();
+        String timePref = PING_TIME + "." + app;  //$NON-NLS-1$
+        synchronized (DdmsPreferenceStore.class) {
+            prefs.setValue(timePref, timeStamp);
+            try {
+                prefs.save();
+            } catch (IOException ioe) {
+                /* ignore exceptions while saving preferences */
+            }
+        }
+    }
+
+    /**
+     * True if this is the first time the users runs ADT, which is detected by
+     * the lack of the setting set using {@link #setAdtUsed(boolean)}
+     * or this value being set to true.
+     *
+     * @return true if ADT has been used  before
+     *
+     * @see #setAdtUsed(boolean)
+     */
+    public boolean isAdtUsed() {
+        PreferenceStore prefs = getPreferenceStore();
+        synchronized (DdmsPreferenceStore.class) {
+            if (prefs == null || !prefs.contains(ADT_USED)) {
+                return false;
+            }
+            return prefs.getBoolean(ADT_USED);
+        }
+    }
+
+    /**
+     * Sets whether the ADT startup wizard has been shown.
+     * ADT sets first to false once the welcome wizard has been shown once.
+     *
+     * @param used true if ADT has been used
+     */
+    public void setAdtUsed(boolean used) {
+        PreferenceStore prefs = getPreferenceStore();
+        synchronized (DdmsPreferenceStore.class) {
+            prefs.setValue(ADT_USED, used);
+            try {
+                prefs.save();
+            } catch (IOException ioe) {
+                /* ignore exceptions while saving preferences */
+            }
+        }
+    }
+
+    /**
+     * Retrieves the last SDK OS path.
+     * <p/>
+     * This is just an information value, the path may not exist, may not
+     * even be on an existing file system and/or may not point to an SDK
+     * anymore.
+     *
+     * @return The last SDK OS path from the preference store, or null if
+     *  there is no store or an empty string if it is not defined.
+     */
+    public String getLastSdkPath() {
+        PreferenceStore prefs = getPreferenceStore();
+        synchronized (DdmsPreferenceStore.class) {
+            return prefs == null ? null : prefs.getString(LAST_SDK_PATH);
+        }
+    }
+
+    /**
+     * Sets the last SDK OS path.
+     *
+     * @param osSdkPath The SDK OS Path. Can be null or empty.
+     */
+    public void setLastSdkPath(String osSdkPath) {
+        PreferenceStore prefs = getPreferenceStore();
+        synchronized (DdmsPreferenceStore.class) {
+            prefs.setValue(LAST_SDK_PATH, osSdkPath);
+            try {
+                prefs.save();
+            } catch (IOException ioe) {
+                /* ignore exceptions while saving preferences */
+            }
+        }
+    }
+}
diff --git a/sdkstats/src/main/java/com/android/sdkstats/SdkStatsPermissionDialog.java b/sdkstats/src/main/java/com/android/sdkstats/SdkStatsPermissionDialog.java
new file mode 100644
index 0000000..f9856cc
--- /dev/null
+++ b/sdkstats/src/main/java/com/android/sdkstats/SdkStatsPermissionDialog.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.sdkstats;
+
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.program.Program;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Link;
+import org.eclipse.swt.widgets.Shell;
+
+import java.io.IOException;
+
+/**
+ * Dialog to get user permission for ping service.
+ */
+public class SdkStatsPermissionDialog extends Dialog {
+    /* Text strings displayed in the opt-out dialog. */
+    private static final String HEADER_TEXT =
+        "Thanks for using the Android SDK!";
+
+    /** Used in the ADT welcome wizard as well. */
+    public static final String NOTICE_TEXT =
+        "We know you just want to get started but please read this first.";
+
+    /** Used in the preference pane (PrefsDialog) as well. */
+    public static final String BODY_TEXT =
+        "By choosing to send certain usage statistics to Google, you can " +
+        "help us improve the Android SDK. These usage statistics lets us " +
+        "measure things like active usage of the SDK, and let us know things " +
+        "like which versions of the SDK are in use and which tools are the " +
+        "most popular with developers. This limited data is not associated " +
+        "with personal information about you, and is examined on an aggregate " +
+        "basis, and is maintained in accordance with the Google Privacy Policy.";
+
+    /** Used in the ADT welcome wizard as well. */
+    public static final String PRIVACY_POLICY_LINK_TEXT =
+        "<a href=\"http://www.google.com/intl/en/privacy.html\">Google " +
+        "Privacy Policy</a>";
+
+    /** Used in the preference pane (PrefsDialog) as well. */
+    public static final String CHECKBOX_TEXT =
+        "Send usage statistics to Google.";
+
+    /** Used in the ADT welcome wizard as well. */
+    public static final String FOOTER_TEXT =
+        "If you later decide to change this setting, you can do so in the" +
+        "\"ddms\" tool under \"File\" > \"Preferences\" > \"Usage Stats\".";
+
+    private static final String BUTTON_TEXT = "Proceed";
+
+    /** List of Linux browser commands to try, in order (see openUrl). */
+    private static final String[] LINUX_BROWSERS = new String[] {
+        "firefox -remote openurl(%URL%,new-window)",  //$NON-NLS-1$ running FF
+        "mozilla -remote openurl(%URL%,new-window)",  //$NON-NLS-1$ running Moz
+        "firefox %URL%",                              //$NON-NLS-1$ new FF
+        "mozilla %URL%",                              //$NON-NLS-1$ new Moz
+        "kfmclient openURL %URL%",                    //$NON-NLS-1$ Konqueror
+        "opera -newwindow %URL%",                     //$NON-NLS-1$ Opera
+    };
+
+    private static final boolean ALLOW_PING_DEFAULT = true;
+    private boolean mAllowPing = ALLOW_PING_DEFAULT;
+
+    public SdkStatsPermissionDialog(Shell parentShell) {
+        super(parentShell);
+        setBlockOnOpen(true);
+    }
+
+    @Override
+    protected void createButtonsForButtonBar(Composite parent) {
+        createButton(parent, Window.OK, BUTTON_TEXT, true);
+    }
+
+    @Override
+    protected Control createDialogArea(Composite parent) {
+        Composite composite = (Composite) super.createDialogArea(parent);
+        composite.setLayout(new GridLayout(1, false));
+
+        final Label title = new Label(composite, SWT.CENTER | SWT.WRAP);
+        final FontData[] fontdata = title.getFont().getFontData();
+        for (int i = 0; i < fontdata.length; i++) {
+            fontdata[i].setHeight(fontdata[i].getHeight() * 4 / 3);
+        }
+        title.setFont(new Font(getShell().getDisplay(), fontdata));
+        title.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        title.setText(HEADER_TEXT);
+
+        final Label notice = new Label(composite, SWT.WRAP);
+        notice.setFont(title.getFont());
+        notice.setForeground(new Color(getShell().getDisplay(), 255, 0, 0));
+        notice.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        notice.setText(NOTICE_TEXT);
+        notice.pack();
+
+        final Label bodyText = new Label(composite, SWT.WRAP);
+        GridData gd = new GridData();
+        gd.widthHint = notice.getSize().x;  // do not extend beyond the NOTICE text's width
+        gd.grabExcessHorizontalSpace = true;
+        bodyText.setLayoutData(gd);
+        bodyText.setText(BODY_TEXT);
+
+        final Link privacyLink = new Link(composite, SWT.NO_FOCUS);
+        privacyLink.setText(PRIVACY_POLICY_LINK_TEXT);
+        privacyLink.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent event) {
+                openUrl(event.text);
+            }
+        });
+
+        final Button checkbox = new Button(composite, SWT.CHECK);
+        checkbox.setSelection(ALLOW_PING_DEFAULT);
+        checkbox.setText(CHECKBOX_TEXT);
+        checkbox.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent event) {
+                mAllowPing = checkbox.getSelection();
+            }
+        });
+        checkbox.setFocus();
+
+        final Label footer = new Label(composite, SWT.WRAP);
+        gd = new GridData();
+        gd.widthHint = notice.getSize().x;
+        gd.grabExcessHorizontalSpace = true;
+        footer.setLayoutData(gd);
+        footer.setText(FOOTER_TEXT);
+
+        return composite;
+    }
+
+    /**
+     * Open a URL in an external browser.
+     * @param url to open - MUST be sanitized and properly formed!
+     */
+    public static void openUrl(final String url) {
+        // TODO: consider using something like BrowserLauncher2
+        // (http://browserlaunch2.sourceforge.net/) instead of these hacks.
+
+        // SWT's Program.launch() should work on Mac, Windows, and GNOME
+        // (because the OS shell knows how to launch a default browser).
+        if (!Program.launch(url)) {
+            // Must be Linux non-GNOME (or something else broke).
+            // Try a few Linux browser commands in the background.
+            new Thread() {
+                @Override
+                public void run() {
+                    for (String cmd : LINUX_BROWSERS) {
+                        cmd = cmd.replaceAll("%URL%", url);  //$NON-NLS-1$
+                        try {
+                            Process proc = Runtime.getRuntime().exec(cmd);
+                            if (proc.waitFor() == 0) break;  // Success!
+                        } catch (InterruptedException e) {
+                            // Should never happen!
+                            throw new RuntimeException(e);
+                        } catch (IOException e) {
+                            // Swallow the exception and try the next browser.
+                        }
+                    }
+
+                    // TODO: Pop up some sort of error here?
+                    // (We're in a new thread; can't use the existing Display.)
+                }
+            }.start();
+        }
+    }
+
+    public boolean getPingUserPreference() {
+        return mAllowPing;
+    }
+}
diff --git a/sdkstats/src/main/java/com/android/sdkstats/SdkStatsService.java b/sdkstats/src/main/java/com/android/sdkstats/SdkStatsService.java
new file mode 100644
index 0000000..79c2ef5
--- /dev/null
+++ b/sdkstats/src/main/java/com/android/sdkstats/SdkStatsService.java
@@ -0,0 +1,558 @@
+/*
+ * Copyright (C) 2007 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.sdkstats;
+
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Utility class to send "ping" usage reports to the server. */
+public class SdkStatsService {
+
+    protected static final String SYS_PROP_OS_ARCH      = "os.arch";        //$NON-NLS-1$
+    protected static final String SYS_PROP_JAVA_VERSION = "java.version";   //$NON-NLS-1$
+    protected static final String SYS_PROP_OS_VERSION   = "os.version";     //$NON-NLS-1$
+    protected static final String SYS_PROP_OS_NAME      = "os.name";        //$NON-NLS-1$
+
+    /** Minimum interval between ping, in milliseconds. */
+    private static final long PING_INTERVAL_MSEC = 86400 * 1000;  // 1 day
+
+    private static final boolean DEBUG = System.getenv("ANDROID_DEBUG_PING") != null; //$NON-NLS-1$
+
+    private DdmsPreferenceStore mStore = new DdmsPreferenceStore();
+
+    public SdkStatsService() {
+    }
+
+    /**
+     * Send a "ping" to the Google toolbar server, if enough time has
+     * elapsed since the last ping, and if the user has not opted out.
+     * <p/>
+     * This is a simplified version of {@link #ping(String[])} that only
+     * sends an "application" name and a "version" string. See the explanation
+     * there for details.
+     *
+     * @param app The application name that reports the ping (e.g. "emulator" or "ddms".)
+     *          Valid characters are a-zA-Z0-9 only.
+     * @param version The version string (e.g. "12" or "1.2.3.4", 4 groups max.)
+     * @see #ping(String[])
+     */
+    public void ping(String app, String version) {
+        doPing(app, version, null);
+    }
+
+    /**
+     * Send a "ping" to the Google toolbar server, if enough time has
+     * elapsed since the last ping, and if the user has not opted out.
+     * <p/>
+     * The ping will not be sent if the user opt out dialog has not been shown yet.
+     * Use {@link #checkUserPermissionForPing(Shell)} to display the dialog requesting
+     * user permissions.
+     * <p/>
+     * Note: The actual ping (if any) is sent in a <i>non-daemon</i> background thread.
+     * <p/>
+     * The arguments are defined as follow:
+     * <ul>
+     * <li>Argument 0 is the "ping" command and is ignored.</li>
+     * <li>Argument 1 is the application name that reports the ping (e.g. "emulator" or "ddms".)
+     *          Valid characters are a-zA-Z0-9 only.</li>
+     * <li>Argument 2 is the version string (e.g. "12" or "1.2.3.4", 4 groups max.)</li>
+     * <li>Arguments 3+ are optional and depend on the application name.</li>
+     * <li>"emulator" application currently has 3 optional arguments:
+     *      <ul>
+     *      <li>Arugment 3: android_gl_vendor</li>
+     *      <li>Arugment 4: android_gl_renderer</li>
+     *      <li>Arugment 5: android_gl_version</li>
+     *      </ul>
+     * </li>
+     * </ul>
+     *
+     * @param arguments A non-empty non-null array of arguments to the ping as described above.
+     */
+    public void ping(String[] arguments) {
+        if (arguments == null || arguments.length < 3) {
+            throw new IllegalArgumentException(
+                    "Invalid ping arguments: expected ['ping', app, version] but got " +
+                    (arguments == null ? "null" : Arrays.toString(arguments)));
+        }
+        int len = arguments.length;
+        String app = arguments[1];
+        String version = arguments[2];
+
+        Map<String, String> extras = new HashMap<String, String>();
+
+        if ("emulator".equals(app)) {                                   //$NON-NLS-1$
+            if (len > 3) {
+                extras.put("glm", sanitizeGlArg(arguments[3])); //$NON-NLS-1$ vendor
+            }
+            if (len > 4) {
+                extras.put("glr", sanitizeGlArg(arguments[4])); //$NON-NLS-1$ renderer
+            }
+            if (len > 5) {
+                extras.put("glv", sanitizeGlArg(arguments[5])); //$NON-NLS-1$ version
+            }
+        }
+
+        doPing(app, version, extras);
+    }
+
+    private String sanitizeGlArg(String arg) {
+        if (arg == null) {
+        arg = "";                                                   //$NON-NLS-1$
+        } else {
+            try {
+                arg = arg.trim();
+                arg = arg.replaceAll("[^A-Za-z0-9\\s_()./-]", " "); //$NON-NLS-1$ //$NON-NLS-2$
+                arg = arg.replaceAll("\\s\\s+", " ");               //$NON-NLS-1$ //$NON-NLS-2$
+
+                // Guard from arbitrarily long parameters
+                if (arg.length() > 128) {
+                    arg = arg.substring(0, 128);
+                }
+
+                arg = URLEncoder.encode(arg, "UTF-8");              //$NON-NLS-1$
+            } catch (UnsupportedEncodingException e) {
+                arg = "";                                           //$NON-NLS-1$
+            }
+        }
+
+        return arg;
+    }
+
+    /**
+     * Display a dialog to the user providing information about the ping service,
+     * and whether they'd like to opt-out of it.
+     *
+     * Once the dialog has been shown, it sets a preference internally indicating
+     * that the user has viewed this dialog.
+     */
+    public void checkUserPermissionForPing(Shell parent) {
+        if (!mStore.hasPingId()) {
+            askUserPermissionForPing(parent);
+            mStore.generateNewPingId();
+        }
+    }
+
+    /**
+     * Prompt the user for whether they want to opt out of reporting, and save the user
+     * input in preferences.
+     */
+    private void askUserPermissionForPing(final Shell parent) {
+        final Display display = parent.getDisplay();
+        display.syncExec(new Runnable() {
+            @Override
+            public void run() {
+                SdkStatsPermissionDialog dialog = new SdkStatsPermissionDialog(parent);
+                dialog.open();
+                mStore.setPingOptIn(dialog.getPingUserPreference());
+            }
+        });
+    }
+
+    // -------
+
+    /**
+     * Pings the usage stats server, as long as the prefs contain the opt-in boolean
+     *
+     * @param app The application name that reports the ping (e.g. "emulator" or "ddms".)
+     *          Will be normalized.  Valid characters are a-zA-Z0-9 only.
+     * @param version The version string (e.g. "12" or "1.2.3.4", 4 groups max.)
+     * @param extras Extra key/value parameters to send. They are send as-is and must
+     *  already be well suited and escaped using {@link URLEncoder#encode(String, String)}.
+     */
+    protected void doPing(String app, String version, final Map<String, String> extras) {
+        // Note: if you change the implementation here, you also need to change
+        // the overloaded SdkStatsServiceTest.doPing() used for testing.
+
+        // Validate the application and version input.
+        final String nApp = normalizeAppName(app);
+        final String nVersion = normalizeVersion(version);
+
+        // If the user has not opted in, do nothing and quietly return.
+        if (!mStore.isPingOptIn()) {
+            // user opted out.
+            return;
+        }
+
+        // If the last ping *for this app* was too recent, do nothing.
+        long now = System.currentTimeMillis();
+        long then = mStore.getPingTime(app);
+        if (now - then < PING_INTERVAL_MSEC) {
+            // too soon after a ping.
+            return;
+        }
+
+        // Record the time of the attempt, whether or not it succeeds.
+        mStore.setPingTime(app, now);
+
+        // Send the ping itself in the background (don't block if the
+        // network is down or slow or confused).
+        final long id = mStore.getPingId();
+        new Thread() {
+            @Override
+            public void run() {
+                try {
+                    URL url = createPingUrl(nApp, nVersion, id, extras);
+                    actuallySendPing(url);
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }.start();
+    }
+
+
+    /**
+     * Unconditionally send a "ping" request to the server.
+     *
+     * @param url The URL to send to the server.
+     * * @throws IOException if the ping failed
+     */
+    private void actuallySendPing(URL url) throws IOException {
+        assert url != null;
+
+        if (DEBUG) {
+            System.err.println("Ping: " + url.toString());          //$NON-NLS-1$
+        }
+
+        // Discard the actual response, but make sure it reads OK
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+
+        // Believe it or not, a 404 response indicates success:
+        // the ping was logged, but no update is configured.
+        if (conn.getResponseCode() != HttpURLConnection.HTTP_OK &&
+            conn.getResponseCode() != HttpURLConnection.HTTP_NOT_FOUND) {
+            throw new IOException(
+                conn.getResponseMessage() + ": " + url);            //$NON-NLS-1$
+        }
+    }
+
+    /**
+     * Compute the ping URL to send the data to the server.
+     *
+     * @param app The application name that reports the ping (e.g. "emulator" or "ddms".)
+     *          Valid characters are a-zA-Z0-9 only.
+     * @param version The version string already formatted as a 4 dotted group (e.g. "1.2.3.4".)
+     * @param id of the local installation
+     * @param extras Extra key/value parameters to send. They are send as-is and must
+     *  already be well suited and escaped using {@link URLEncoder#encode(String, String)}.
+     */
+    protected URL createPingUrl(String app, String version, long id, Map<String, String> extras)
+            throws UnsupportedEncodingException, MalformedURLException {
+
+        String osName  = URLEncoder.encode(getOsName(),  "UTF-8");  //$NON-NLS-1$
+        String osArch  = URLEncoder.encode(getOsArch(),  "UTF-8");  //$NON-NLS-1$
+        String jvmArch = URLEncoder.encode(getJvmInfo(), "UTF-8");  //$NON-NLS-1$
+
+        // Include the application's name as part of the as= value.
+        // Share the user ID for all apps, to allow unified activity reports.
+
+        String extraStr = "";                                       //$NON-NLS-1$
+        if (extras != null && !extras.isEmpty()) {
+            StringBuilder sb = new StringBuilder();
+            for (Map.Entry<String, String> entry : extras.entrySet()) {
+                sb.append('&').append(entry.getKey()).append('=').append(entry.getValue());
+            }
+            extraStr = sb.toString();
+        }
+
+        URL url = new URL(
+            "http",                                                 //$NON-NLS-1$
+            "tools.google.com",                                     //$NON-NLS-1$
+            "/service/update?as=androidsdk_" + app +                //$NON-NLS-1$
+                "&id=" + Long.toHexString(id) +                     //$NON-NLS-1$
+                "&version=" + version +                             //$NON-NLS-1$
+                "&os=" + osName +                                   //$NON-NLS-1$
+                "&osa=" + osArch +                                  //$NON-NLS-1$
+                "&vma=" + jvmArch +                                 //$NON-NLS-1$
+                extraStr);
+        return url;
+    }
+
+    /**
+     * Detects and reports the host OS: "linux", "win" or "mac".
+     * For Windows and Mac also append the version, so for example
+     * Win XP will return win-5.1.
+     */
+    protected String getOsName() {                   // made protected for testing
+        String os = getSystemProperty(SYS_PROP_OS_NAME);
+
+        if (os == null || os.length() == 0) {
+            return "unknown";                               //$NON-NLS-1$
+        }
+
+        String os2 = os.toLowerCase(Locale.US);
+
+        if (os2.startsWith("mac")) {                        //$NON-NLS-1$
+            os = "mac";                                     //$NON-NLS-1$
+            String osVers = getOsVersion();
+            if (osVers != null) {
+                os = os + '-' + osVers;
+            }
+        } else if (os2.startsWith("win")) {                 //$NON-NLS-1$
+            os = "win";                                     //$NON-NLS-1$
+            String osVers = getOsVersion();
+            if (osVers != null) {
+                os = os + '-' + osVers;
+            }
+        } else if (os2.startsWith("linux")) {               //$NON-NLS-1$
+            os = "linux";                                   //$NON-NLS-1$
+
+        } else if (os.length() > 32) {
+            // Unknown -- send it verbatim so we can see it
+            // but protect against arbitrarily long values
+            os = os.substring(0, 32);
+        }
+        return os;
+    }
+
+    /**
+     * Detects and returns the OS architecture: x86, x86_64, ppc.
+     * This may differ or be equal to the JVM architecture in the sense that
+     * a 64-bit OS can run a 32-bit JVM.
+     */
+    protected String getOsArch() {                   // made protected for testing
+        String arch = getJvmArch();
+
+        if ("x86_64".equals(arch)) {                                    //$NON-NLS-1$
+            // This is a simple case: the JVM runs in 64-bit so the
+            // OS must be a 64-bit one.
+            return arch;
+
+        } else if ("x86".equals(arch)) {                                //$NON-NLS-1$
+            // This is the misleading case: the JVM is 32-bit but the OS
+            // might be either 32 or 64. We can't tell just from this
+            // property.
+            // Macs are always on 64-bit, so we just need to figure it
+            // out for Windows and Linux.
+
+            String os = getOsName();
+            if (os.startsWith("win")) {                                 //$NON-NLS-1$
+                // When WOW64 emulates a 32-bit environment under a 64-bit OS,
+                // it sets PROCESSOR_ARCHITEW6432 to AMD64 or IA64 accordingly.
+                // Ref: http://msdn.microsoft.com/en-us/library/aa384274(v=vs.85).aspx
+
+                String w6432 = getSystemEnv("PROCESSOR_ARCHITEW6432");  //$NON-NLS-1$
+                if (w6432 != null && w6432.indexOf("64") != -1) {       //$NON-NLS-1$
+                    return "x86_64";                                    //$NON-NLS-1$
+                }
+            } else if (os.startsWith("linux")) {                        //$NON-NLS-1$
+                // Let's try the obvious. This works in Ubuntu and Debian
+                String s = getSystemEnv("HOSTTYPE");                    //$NON-NLS-1$
+
+                s = sanitizeOsArch(s);
+                if (s.indexOf("86") != -1) {                            //$NON-NLS-1$
+                    arch = s;
+                }
+            }
+        }
+
+        return arch;
+    }
+
+    /**
+     * Returns the version of the OS version if it is defined as X.Y, or null otherwise.
+     * <p/>
+     * Example of returned versions can be found at http://lopica.sourceforge.net/os.html
+     * <p/>
+     * This method removes any exiting micro versions.
+     * Returns null if the version doesn't match X.Y.Z.
+     */
+    protected String getOsVersion() {                           // made protected for testing
+        Pattern p = Pattern.compile("(\\d+)\\.(\\d+).*");       //$NON-NLS-1$
+        String osVers = getSystemProperty(SYS_PROP_OS_VERSION);
+        if (osVers != null && osVers.length() > 0) {
+            Matcher m = p.matcher(osVers);
+            if (m.matches()) {
+                return m.group(1) + '.' + m.group(2);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Detects and returns the JVM info: version + architecture.
+     * Examples: 1.4-ppc, 1.6-x86, 1.7-x86_64
+     */
+    protected String getJvmInfo() {                      // made protected for testing
+        return getJvmVersion() + '-' + getJvmArch();
+    }
+
+    /**
+     * Returns the major.minor Java version.
+     * <p/>
+     * The "java.version" property returns something like "1.6.0_20"
+     * of which we want to return "1.6".
+     */
+    protected String getJvmVersion() {                   // made protected for testing
+        String version = getSystemProperty(SYS_PROP_JAVA_VERSION);
+
+        if (version == null || version.length() == 0) {
+            return "unknown";                                   //$NON-NLS-1$
+        }
+
+        Pattern p = Pattern.compile("(\\d+)\\.(\\d+).*");       //$NON-NLS-1$
+        Matcher m = p.matcher(version);
+        if (m.matches()) {
+            return m.group(1) + '.' + m.group(2);
+        }
+
+        // Unknown version. Send it as-is within a reasonable size limit.
+        if (version.length() > 8) {
+            version = version.substring(0, 8);
+        }
+        return version;
+    }
+
+    /**
+     * Detects and returns the JVM architecture.
+     * <p/>
+     * The HotSpot JVM has a private property for this, "sun.arch.data.model",
+     * which returns either "32" or "64". However it's not in any kind of spec.
+     * <p/>
+     * What we want is to know whether the JVM is running in 32-bit or 64-bit and
+     * the best indicator is to use the "os.arch" property.
+     * - On a 32-bit system, only a 32-bit JVM can run so it will be x86 or ppc.<br/>
+     * - On a 64-bit system, a 32-bit JVM will also return x86 since the OS needs
+     *   to masquerade as a 32-bit OS for backward compatibility.<br/>
+     * - On a 64-bit system, a 64-bit JVM will properly return x86_64.
+     * <pre>
+     * JVM:       Java 32-bit   Java 64-bit
+     * Windows:   x86           x86_64
+     * Linux:     x86           x86_64
+     * Mac        untested      x86_64
+     * </pre>
+     */
+    protected String getJvmArch() {                  // made protected for testing
+        String arch = getSystemProperty(SYS_PROP_OS_ARCH);
+        return sanitizeOsArch(arch);
+    }
+
+    private String sanitizeOsArch(String arch) {
+        if (arch == null || arch.length() == 0) {
+            return "unknown";                               //$NON-NLS-1$
+        }
+
+        if (arch.equalsIgnoreCase("x86_64") ||              //$NON-NLS-1$
+                arch.equalsIgnoreCase("ia64") ||            //$NON-NLS-1$
+                arch.equalsIgnoreCase("amd64")) {           //$NON-NLS-1$
+            return "x86_64";                                //$NON-NLS-1$
+        }
+
+        if (arch.length() >= 4 && arch.charAt(0) == 'i' && arch.indexOf("86") == 2) { //$NON-NLS-1$
+            // Any variation of iX86 counts as x86 (i386, i486, i686).
+            return "x86";                                   //$NON-NLS-1$
+        }
+
+        if (arch.equalsIgnoreCase("PowerPC")) {             //$NON-NLS-1$
+            return "ppc";                                   //$NON-NLS-1$
+        }
+
+        // Unknown arch. Send it as-is but protect against arbitrarily long values.
+        if (arch.length() > 32) {
+            arch = arch.substring(0, 32);
+        }
+        return arch;
+    }
+
+    /**
+     * Normalize the supplied application name.
+     *
+     * @param app to report
+     */
+    protected String normalizeAppName(String app) {
+        // Filter out \W , non-word character: [^a-zA-Z_0-9]
+        String app2 = app.replaceAll("\\W", "");                  //$NON-NLS-1$ //$NON-NLS-2$
+
+        if (app.length() == 0) {
+            throw new IllegalArgumentException("Bad app name: " + app);         //$NON-NLS-1$
+        }
+
+        return app2;
+    }
+
+    /**
+     * Validate the supplied application version, and normalize the version.
+     *
+     * @param version supplied by caller
+     * @return normalized dotted quad version
+     */
+    protected String normalizeVersion(String version) {
+
+        Pattern regex = Pattern.compile(
+                //1=major     2=minor       3=micro       4=build |  5=rc
+                "^(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?(?:\\.(\\d+)| +rc(\\d+))?"); //$NON-NLS-1$
+
+        Matcher m = regex.matcher(version);
+        if (m != null && m.lookingAt()) {
+            StringBuilder normal = new StringBuilder();
+            for (int i = 1; i <= 4; i++) {
+                int v = 0;
+                // If build is null but we have an rc, take that number instead as the 4th part.
+                if (i == 4 &&
+                        i < m.groupCount() &&
+                        m.group(i) == null &&
+                        m.group(i+1) != null) {
+                    i++;
+                }
+                if (m.group(i) != null) {
+                    try {
+                        v = Integer.parseInt(m.group(i));
+                    } catch (Exception ignore) {
+                    }
+                }
+                if (i > 1) {
+                    normal.append('.');
+                }
+                normal.append(v);
+            }
+            return normal.toString();
+        }
+
+        throw new IllegalArgumentException("Bad version: " + version);          //$NON-NLS-1$
+    }
+
+    /**
+     * Calls {@link System#getProperty(String)}.
+     * Allows unit-test to override the return value.
+     * @see System#getProperty(String)
+     */
+    protected String getSystemProperty(String name) {
+        return System.getProperty(name);
+    }
+
+    /**
+     * Calls {@link System#getenv(String)}.
+     * Allows unit-test to override the return value.
+     * @see System#getenv(String)
+     */
+    protected String getSystemEnv(String name) {
+        return System.getenv(name);
+    }
+}
diff --git a/swtmenubar/.classpath b/swtmenubar/.classpath
new file mode 100644
index 0000000..25adf96
--- /dev/null
+++ b/swtmenubar/.classpath
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src/main/java"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+	<classpathentry exported="true" kind="var" path="ANDROID_OUT_FRAMEWORK/swt.jar"/>
+	<classpathentry exported="true" kind="var" path="ANDROID_SRC/prebuilts/tools/common/m2/repository/com/android/external/eclipse/org-eclipse-core-commands/3.6.0/org-eclipse-core-commands-3.6.0.jar"/>
+	<classpathentry exported="true" kind="var" path="ANDROID_SRC/prebuilts/tools/common/m2/repository/com/android/external/eclipse/org-eclipse-equinox-common/3.6.0/org-eclipse-equinox-common-3.6.0.jar"/>
+	<classpathentry exported="true" kind="var" path="ANDROID_SRC/prebuilts/tools/common/m2/repository/com/android/external/eclipse/org-eclipse-jface/3.6.2/org-eclipse-jface-3.6.2.jar"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/swtmenubar/.project b/swtmenubar/.project
new file mode 100644
index 0000000..b81e72f
--- /dev/null
+++ b/swtmenubar/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>swtmenubar</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>
diff --git a/swtmenubar/MODULE_LICENSE_EPL b/swtmenubar/MODULE_LICENSE_EPL
new file mode 100644
index 0000000..e69de29
diff --git a/swtmenubar/NOTICE b/swtmenubar/NOTICE
new file mode 100644
index 0000000..49c101d
--- /dev/null
+++ b/swtmenubar/NOTICE
@@ -0,0 +1,224 @@
+*Eclipse Public License - v 1.0*
+
+THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
+PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF
+THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
+
+*1. DEFINITIONS*
+
+"Contribution" means:
+
+a) in the case of the initial Contributor, the initial code and
+documentation distributed under this Agreement, and
+b) in the case of each subsequent Contributor:
+
+i) changes to the Program, and
+
+ii) additions to the Program;
+
+where such changes and/or additions to the Program originate from and
+are distributed by that particular Contributor. A Contribution
+'originates' from a Contributor if it was added to the Program by such
+Contributor itself or anyone acting on such Contributor's behalf.
+Contributions do not include additions to the Program which: (i) are
+separate modules of software distributed in conjunction with the Program
+under their own license agreement, and (ii) are not derivative works of
+the Program.
+
+"Contributor" means any person or entity that distributes the Program.
+
+"Licensed Patents " mean patent claims licensable by a Contributor which
+are necessarily infringed by the use or sale of its Contribution alone
+or when combined with the Program.
+
+"Program" means the Contributions distributed in accordance with this
+Agreement.
+
+"Recipient" means anyone who receives the Program under this Agreement,
+including all Contributors.
+
+*2. GRANT OF RIGHTS*
+
+a) Subject to the terms of this Agreement, each Contributor hereby
+grants Recipient a non-exclusive, worldwide, royalty-free copyright
+license to reproduce, prepare derivative works of, publicly display,
+publicly perform, distribute and sublicense the Contribution of such
+Contributor, if any, and such derivative works, in source code and
+object code form.
+
+b) Subject to the terms of this Agreement, each Contributor hereby
+grants Recipient a non-exclusive, worldwide, royalty-free patent license
+under Licensed Patents to make, use, sell, offer to sell, import and
+otherwise transfer the Contribution of such Contributor, if any, in
+source code and object code form. This patent license shall apply to the
+combination of the Contribution and the Program if, at the time the
+Contribution is added by the Contributor, such addition of the
+Contribution causes such combination to be covered by the Licensed
+Patents. The patent license shall not apply to any other combinations
+which include the Contribution. No hardware per se is licensed hereunder.
+
+c) Recipient understands that although each Contributor grants the
+licenses to its Contributions set forth herein, no assurances are
+provided by any Contributor that the Program does not infringe the
+patent or other intellectual property rights of any other entity. Each
+Contributor disclaims any liability to Recipient for claims brought by
+any other entity based on infringement of intellectual property rights
+or otherwise. As a condition to exercising the rights and licenses
+granted hereunder, each Recipient hereby assumes sole responsibility to
+secure any other intellectual property rights needed, if any. For
+example, if a third party patent license is required to allow Recipient
+to distribute the Program, it is Recipient's responsibility to acquire
+that license before distributing the Program.
+
+d) Each Contributor represents that to its knowledge it has sufficient
+copyright rights in its Contribution, if any, to grant the copyright
+license set forth in this Agreement.
+
+*3. REQUIREMENTS*
+
+A Contributor may choose to distribute the Program in object code form
+under its own license agreement, provided that:
+
+a) it complies with the terms and conditions of this Agreement; and
+
+b) its license agreement:
+
+i) effectively disclaims on behalf of all Contributors all warranties
+and conditions, express and implied, including warranties or conditions
+of title and non-infringement, and implied warranties or conditions of
+merchantability and fitness for a particular purpose;
+
+ii) effectively excludes on behalf of all Contributors all liability for
+damages, including direct, indirect, special, incidental and
+consequential damages, such as lost profits;
+
+iii) states that any provisions which differ from this Agreement are
+offered by that Contributor alone and not by any other party; and
+
+iv) states that source code for the Program is available from such
+Contributor, and informs licensees how to obtain it in a reasonable
+manner on or through a medium customarily used for software exchange.
+
+When the Program is made available in source code form:
+
+a) it must be made available under this Agreement; and
+
+b) a copy of this Agreement must be included with each copy of the Program.
+
+Contributors may not remove or alter any copyright notices contained
+within the Program.
+
+Each Contributor must identify itself as the originator of its
+Contribution, if any, in a manner that reasonably allows subsequent
+Recipients to identify the originator of the Contribution.
+
+*4. COMMERCIAL DISTRIBUTION*
+
+Commercial distributors of software may accept certain responsibilities
+with respect to end users, business partners and the like. While this
+license is intended to facilitate the commercial use of the Program, the
+Contributor who includes the Program in a commercial product offering
+should do so in a manner which does not create potential liability for
+other Contributors. Therefore, if a Contributor includes the Program in
+a commercial product offering, such Contributor ("Commercial
+Contributor") hereby agrees to defend and indemnify every other
+Contributor ("Indemnified Contributor") against any losses, damages and
+costs (collectively "Losses") arising from claims, lawsuits and other
+legal actions brought by a third party against the Indemnified
+Contributor to the extent caused by the acts or omissions of such
+Commercial Contributor in connection with its distribution of the
+Program in a commercial product offering. The obligations in this
+section do not apply to any claims or Losses relating to any actual or
+alleged intellectual property infringement. In order to qualify, an
+Indemnified Contributor must: a) promptly notify the Commercial
+Contributor in writing of such claim, and b) allow the Commercial
+Contributor to control, and cooperate with the Commercial Contributor
+in, the defense and any related settlement negotiations. The Indemnified
+Contributor may participate in any such claim at its own expense.
+
+For example, a Contributor might include the Program in a commercial
+product offering, Product X. That Contributor is then a Commercial
+Contributor. If that Commercial Contributor then makes performance
+claims, or offers warranties related to Product X, those performance
+claims and warranties are such Commercial Contributor's responsibility
+alone. Under this section, the Commercial Contributor would have to
+defend claims against the other Contributors related to those
+performance claims and warranties, and if a court requires any other
+Contributor to pay any damages as a result, the Commercial Contributor
+must pay those damages.
+
+*5. NO WARRANTY*
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED
+ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES
+OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR
+A PARTICULAR PURPOSE. Each Recipient is solely responsible for
+determining the appropriateness of using and distributing the Program
+and assumes all risks associated with its exercise of rights under this
+Agreement , including but not limited to the risks and costs of program
+errors, compliance with applicable laws, damage to or loss of data,
+programs or equipment, and unavailability or interruption of operations.
+
+*6. DISCLAIMER OF LIABILITY*
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR
+ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING
+WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR
+DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED
+HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+*7. GENERAL*
+
+If any provision of this Agreement is invalid or unenforceable under
+applicable law, it shall not affect the validity or enforceability of
+the remainder of the terms of this Agreement, and without further action
+by the parties hereto, such provision shall be reformed to the minimum
+extent necessary to make such provision valid and enforceable.
+
+If Recipient institutes patent litigation against any entity (including
+a cross-claim or counterclaim in a lawsuit) alleging that the Program
+itself (excluding combinations of the Program with other software or
+hardware) infringes such Recipient's patent(s), then such Recipient's
+rights granted under Section 2(b) shall terminate as of the date such
+litigation is filed.
+
+All Recipient's rights under this Agreement shall terminate if it fails
+to comply with any of the material terms or conditions of this Agreement
+and does not cure such failure in a reasonable period of time after
+becoming aware of such noncompliance. If all Recipient's rights under
+this Agreement terminate, Recipient agrees to cease use and distribution
+of the Program as soon as reasonably practicable. However, Recipient's
+obligations under this Agreement and any licenses granted by Recipient
+relating to the Program shall continue and survive.
+
+Everyone is permitted to copy and distribute copies of this Agreement,
+but in order to avoid inconsistency the Agreement is copyrighted and may
+only be modified in the following manner. The Agreement Steward reserves
+the right to publish new versions (including revisions) of this
+Agreement from time to time. No one other than the Agreement Steward has
+the right to modify this Agreement. The Eclipse Foundation is the
+initial Agreement Steward. The Eclipse Foundation may assign the
+responsibility to serve as the Agreement Steward to a suitable separate
+entity. Each new version of the Agreement will be given a distinguishing
+version number. The Program (including Contributions) may always be
+distributed subject to the version of the Agreement under which it was
+received. In addition, after a new version of the Agreement is
+published, Contributor may elect to distribute the Program (including
+its Contributions) under the new version. Except as expressly stated in
+Sections 2(a) and 2(b) above, Recipient receives no rights or licenses
+to the intellectual property of any Contributor under this Agreement,
+whether expressly, by implication, estoppel or otherwise. All rights in
+the Program not expressly granted under this Agreement are reserved.
+
+This Agreement is governed by the laws of the State of New York and the
+intellectual property laws of the United States of America. No party to
+this Agreement will bring a legal action under this Agreement more than
+one year after the cause of action arose. Each party waives its rights
+to a jury trial in any resulting litigation.
+
+ 
+
diff --git a/swtmenubar/README b/swtmenubar/README
new file mode 100755
index 0000000..ba7c25a
--- /dev/null
+++ b/swtmenubar/README
@@ -0,0 +1,80 @@
+Using the Eclipse project SwtMenuBar
+------------------------------------
+
+This project provides a platform-specific way to hook into
+the default OS menu bar.
+
+On MacOS, it allows an SWT app to have an About menu item
+and to hook into the default Preferences menu item.
+
+On Windows and Linux, an SWT Menu should be provided (typically
+named "Tools") into which the About and Options menu items
+will be added.
+
+
+Consequently the implementation contains platform-specific source
+folders for the Java files that rely on a platform-specific version
+of SWT.jar.
+
+Right now we have the following source folders:
+- src/        - Generic implementation for all platforms.
+- src-darwin/ - Implementation for MacOS Carbon.
+
+*Only* the default "src/" folder is declared in the project .classpath
+so that the project can be opened in Eclipse on any platform and still
+work. However that means that on MacOS the custom src-darwin folder is
+not used by default.
+
+
+
+1- To build the library:
+
+Do not use Eclipse to build the library. Instead use the makefile:
+
+$ cd $TOP_OF_ANDROID_TREE
+$ . build/envsetup.sh && lunch sdk-eng
+$ make swtmenubar
+
+This will create a Jar in <Android tree>/out/host/<platform>/framework/
+that can then be included in the target application.
+
+
+2- To use the library in a target application:
+
+Build the swtmenubar library as explained in step 1.
+
+In the target application, define a classpath variable in Eclipse:
+- Open Preferences > Java > Build Path > Classpath Variables
+- Create a new classpath variable named ANDROID_OUT_FRAMEWORK
+- Set its folder value to <Android tree>/out/host/<platform>/framework
+
+Then add a variable to the Build Path of the target project:
+- Open Project > Properties > Java Build Path
+- Select the "Libraries" tab
+- Use "Add Variable"
+- Select ANDROID_OUT_FRAMEWORK
+- Select "Extend..."
+- Select swtmenubar.jar (which you previously built at step 1)
+
+
+3- Tip for developing this library:
+
+Keep in mind that src-darwin folder must not be added to the
+source folder list, otherwise the library would not compile
+on Windows or Linux.
+
+If you change anything to IMenuBarCallback, make sure to test
+on a Mac to be sure you're not breaking the API.
+
+To work on this on a Mac, you can either:
+a- simply temporarily add src-darwin as a source folder to the
+   build path and remove it before submitting.
+b- or directly edit the java files and rebuild the library using
+   'make swtmenubar' from a shell.
+
+To test the library, use 'make swtmenubar'. This will build the
+library in out/... and the sdkmanager project is already setup
+to find it there.
+
+--
+EOF
diff --git a/swtmenubar/src/main-darwin/java/com/android/menubar/internal/MenuBarEnhancerCocoa.java b/swtmenubar/src/main-darwin/java/com/android/menubar/internal/MenuBarEnhancerCocoa.java
new file mode 100644
index 0000000..88d230f
--- /dev/null
+++ b/swtmenubar/src/main-darwin/java/com/android/menubar/internal/MenuBarEnhancerCocoa.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.
+ *
+ * History:
+ * Original code by the <a href="http://www.simidude.com/blog/2008/macify-a-swt-application-in-a-cross-platform-way/">CarbonUIEnhancer from Agynami</a>
+ * with the implementation being modified from the <a href="http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.ui.cocoa/src/org/eclipse/ui/internal/cocoa/CocoaUIEnhancer.java">org.eclipse.ui.internal.cocoa.CocoaUIEnhancer</a>,
+ * then modified by http://www.transparentech.com/opensource/cocoauienhancer to use reflection
+ * rather than 'link' to SWT cocoa, and finally modified to be usable by the SwtMenuBar project.
+ */
+
+package com.android.menubar.internal;
+
+import com.android.menubar.IMenuBarCallback;
+import com.android.menubar.IMenuBarEnhancer;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.internal.C;
+import org.eclipse.swt.internal.Callback;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Menu;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public class MenuBarEnhancerCocoa implements IMenuBarEnhancer {
+
+    private static final long kAboutMenuItem = 0;
+    private static final long kPreferencesMenuItem = 2;
+    // private static final long kServicesMenuItem = 4;
+    // private static final long kHideApplicationMenuItem = 6;
+    private static final long kQuitMenuItem = 10;
+
+    static long mSelPreferencesMenuItemSelected;
+    static long mSelAboutMenuItemSelected;
+    static Callback mProc3Args;
+
+    private String mAppName;
+
+    /**
+     * Class invoked via the Callback object to run the about and preferences
+     * actions.
+     * <p>
+     * If you don't use JFace in your application (SWT only), change the
+     * {@link org.eclipse.jface.action.IAction}s to
+     * {@link org.eclipse.swt.widgets.Listener}s.
+     * </p>
+     */
+    private static class ActionProctarget {
+        private final IMenuBarCallback mCallbacks;
+
+        public ActionProctarget(IMenuBarCallback callbacks) {
+            mCallbacks = callbacks;
+        }
+
+        /**
+         * Will be called on 32bit SWT.
+         */
+        @SuppressWarnings("unused")
+        public int actionProc(int id, int sel, int arg0) {
+            return (int) actionProc((long) id, (long) sel, (long) arg0);
+        }
+
+        /**
+         * Will be called on 64bit SWT.
+         */
+        public long actionProc(long id, long sel, long arg0) {
+            if (sel == mSelAboutMenuItemSelected) {
+                mCallbacks.onAboutMenuSelected();
+            } else if (sel == mSelPreferencesMenuItemSelected) {
+                mCallbacks.onPreferencesMenuSelected();
+            } else {
+                // Unknown selection!
+            }
+            // Return value is not used.
+            return 0;
+        }
+    }
+
+    /**
+     * Construct a new CocoaUIEnhancer.
+     *
+     * @param mAppName The name of the application. It will be used to customize
+     *            the About and Quit menu items. If you do not wish to customize
+     *            the About and Quit menu items, just pass <tt>null</tt> here.
+     */
+    public MenuBarEnhancerCocoa() {
+    }
+
+    public MenuBarMode getMenuBarMode() {
+        return MenuBarMode.MAC_OS;
+    }
+
+    /**
+     * Setup the About and Preferences native menut items with the
+     * given application name and links them to the callback.
+     *
+     * @param appName The application name.
+     * @param display The SWT display. Must not be null.
+     * @param callbacks The callbacks invoked by the menus.
+     */
+    public void setupMenu(
+            String appName,
+            Display display,
+            IMenuBarCallback callbacks) {
+
+        mAppName = appName;
+
+        // This is our callback object whose 'actionProc' method will be called
+        // when the About or Preferences menuItem is invoked.
+        ActionProctarget target = new ActionProctarget(callbacks);
+
+        try {
+            // Initialize the menuItems.
+            initialize(target);
+        } catch (Exception e) {
+            throw new IllegalStateException(e);
+        }
+
+        // Schedule disposal of callback object
+        display.disposeExec(new Runnable() {
+            public void run() {
+                invoke(mProc3Args, "dispose");
+            }
+        });
+    }
+
+    private void initialize(Object callbackObject)
+            throws Exception {
+
+        Class<?> osCls = classForName("org.eclipse.swt.internal.cocoa.OS");
+
+        // Register names in objective-c.
+        if (mSelAboutMenuItemSelected == 0) {
+            mSelPreferencesMenuItemSelected = registerName(osCls, "preferencesMenuItemSelected:"); //$NON-NLS-1$
+            mSelAboutMenuItemSelected = registerName(osCls, "aboutMenuItemSelected:");             //$NON-NLS-1$
+        }
+
+        // Create an SWT Callback object that will invoke the actionProc method
+        // of our internal callback Object.
+        mProc3Args = new Callback(callbackObject, "actionProc", 3); //$NON-NLS-1$
+        Method getAddress = Callback.class.getMethod("getAddress", new Class[0]);
+        Object object = getAddress.invoke(mProc3Args, (Object[]) null);
+        long proc3 = convertToLong(object);
+        if (proc3 == 0) {
+            SWT.error(SWT.ERROR_NO_MORE_CALLBACKS);
+        }
+
+        Class<?> nsMenuCls        = classForName("org.eclipse.swt.internal.cocoa.NSMenu");
+        Class<?> nsMenuitemCls    = classForName("org.eclipse.swt.internal.cocoa.NSMenuItem");
+        Class<?> nsStringCls      = classForName("org.eclipse.swt.internal.cocoa.NSString");
+        Class<?> nsApplicationCls = classForName("org.eclipse.swt.internal.cocoa.NSApplication");
+
+        // Instead of creating a new delegate class in objective-c,
+        // just use the current SWTApplicationDelegate. An instance of this
+        // is a field of the Cocoa Display object and is already the target
+        // for the menuItems. So just get this class and add the new methods
+        // to it.
+        object = invoke(osCls, "objc_lookUpClass", new Object[] {
+            "SWTApplicationDelegate"
+        });
+        long cls = convertToLong(object);
+
+        // Add the action callbacks for Preferences and About menu items.
+        invoke(osCls, "class_addMethod",
+                new Object[] {
+                    wrapPointer(cls),
+                    wrapPointer(mSelPreferencesMenuItemSelected),
+                    wrapPointer(proc3), "@:@"}); //$NON-NLS-1$
+        invoke(osCls,  "class_addMethod",
+                new Object[] {
+                    wrapPointer(cls),
+                    wrapPointer(mSelAboutMenuItemSelected),
+                    wrapPointer(proc3), "@:@"}); //$NON-NLS-1$
+
+        // Get the Mac OS X Application menu.
+        Object sharedApplication = invoke(nsApplicationCls, "sharedApplication");
+        Object mainMenu = invoke(sharedApplication, "mainMenu");
+        Object mainMenuItem = invoke(nsMenuCls, mainMenu, "itemAtIndex", new Object[] {
+            wrapPointer(0)
+        });
+        Object appMenu = invoke(mainMenuItem, "submenu");
+
+        // Create the About <application-name> menu command
+        Object aboutMenuItem =
+                invoke(nsMenuCls, appMenu, "itemAtIndex", new Object[] {
+                    wrapPointer(kAboutMenuItem)
+                });
+        if (mAppName != null) {
+            Object nsStr = invoke(nsStringCls, "stringWith", new Object[] {
+                "About " + mAppName
+            });
+            invoke(nsMenuitemCls, aboutMenuItem, "setTitle", new Object[] {
+                nsStr
+            });
+        }
+        // Rename the quit action.
+        if (mAppName != null) {
+            Object quitMenuItem =
+                    invoke(nsMenuCls, appMenu, "itemAtIndex", new Object[] {
+                        wrapPointer(kQuitMenuItem)
+                    });
+            Object nsStr = invoke(nsStringCls, "stringWith", new Object[] {
+                "Quit " + mAppName
+            });
+            invoke(nsMenuitemCls, quitMenuItem, "setTitle", new Object[] {
+                nsStr
+            });
+        }
+
+        // Enable the Preferences menuItem.
+        Object prefMenuItem =
+                invoke(nsMenuCls, appMenu, "itemAtIndex", new Object[] {
+                    wrapPointer(kPreferencesMenuItem)
+                });
+        invoke(nsMenuitemCls, prefMenuItem, "setEnabled", new Object[] {
+            true
+        });
+
+        // Set the action to execute when the About or Preferences menuItem is
+        // invoked.
+        //
+        // We don't need to set the target here as the current target is the
+        // SWTApplicationDelegate and we have registered the new selectors on
+        // it. So just set the new action to invoke the selector.
+        invoke(nsMenuitemCls, prefMenuItem, "setAction",
+                new Object[] {
+                    wrapPointer(mSelPreferencesMenuItemSelected)
+                });
+        invoke(nsMenuitemCls, aboutMenuItem, "setAction",
+                new Object[] {
+                    wrapPointer(mSelAboutMenuItemSelected)
+                });
+    }
+
+    private long registerName(Class<?> osCls, String name)
+            throws IllegalArgumentException, SecurityException, IllegalAccessException,
+            InvocationTargetException, NoSuchMethodException {
+        Object object = invoke(osCls, "sel_registerName", new Object[] {
+            name
+        });
+        return convertToLong(object);
+    }
+
+    private long convertToLong(Object object) {
+        if (object instanceof Integer) {
+            Integer i = (Integer) object;
+            return i.longValue();
+        }
+        if (object instanceof Long) {
+            Long l = (Long) object;
+            return l.longValue();
+        }
+        return 0;
+    }
+
+    private static Object wrapPointer(long value) {
+        Class<?> PTR_CLASS = C.PTR_SIZEOF == 8 ? long.class : int.class;
+        if (PTR_CLASS == long.class) {
+            return new Long(value);
+        } else {
+            return new Integer((int) value);
+        }
+    }
+
+    private static Object invoke(Class<?> clazz, String methodName, Object[] args) {
+        return invoke(clazz, null, methodName, args);
+    }
+
+    private static Object invoke(Class<?> clazz, Object target, String methodName, Object[] args) {
+        try {
+            Class<?>[] signature = new Class<?>[args.length];
+            for (int i = 0; i < args.length; i++) {
+                Class<?> thisClass = args[i].getClass();
+                if (thisClass == Integer.class)
+                    signature[i] = int.class;
+                else if (thisClass == Long.class)
+                    signature[i] = long.class;
+                else if (thisClass == Byte.class)
+                    signature[i] = byte.class;
+                else if (thisClass == Boolean.class)
+                    signature[i] = boolean.class;
+                else
+                    signature[i] = thisClass;
+            }
+            Method method = clazz.getMethod(methodName, signature);
+            return method.invoke(target, args);
+        } catch (Exception e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    private Class<?> classForName(String classname) {
+        try {
+            Class<?> cls = Class.forName(classname);
+            return cls;
+        } catch (ClassNotFoundException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    private Object invoke(Class<?> cls, String methodName) {
+        return invoke(cls, methodName, (Class<?>[]) null, (Object[]) null);
+    }
+
+    private Object invoke(Class<?> cls, String methodName, Class<?>[] paramTypes,
+            Object... arguments) {
+        try {
+            Method m = cls.getDeclaredMethod(methodName, paramTypes);
+            return m.invoke(null, arguments);
+        } catch (Exception e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    private Object invoke(Object obj, String methodName) {
+        return invoke(obj, methodName, (Class<?>[]) null, (Object[]) null);
+    }
+
+    private Object invoke(Object obj, String methodName, Class<?>[] paramTypes, Object... arguments) {
+        try {
+            Method m = obj.getClass().getDeclaredMethod(methodName, paramTypes);
+            return m.invoke(obj, arguments);
+        } catch (Exception e) {
+            throw new IllegalStateException(e);
+        }
+    }
+}
diff --git a/swtmenubar/src/main/java/com/android/menubar/IMenuBarCallback.java b/swtmenubar/src/main/java/com/android/menubar/IMenuBarCallback.java
new file mode 100644
index 0000000..b0d6568
--- /dev/null
+++ b/swtmenubar/src/main/java/com/android/menubar/IMenuBarCallback.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.menubar;
+
+
+
+/**
+ * Callbacks used by {@link IMenuBarEnhancer}.
+ */
+public interface IMenuBarCallback {
+    /**
+     * Invoked when the About menu item is selected by the user.
+     */
+    abstract public void onAboutMenuSelected();
+
+    /**
+     * Invoked when the Preferences or Options menu item is selected by the user.
+     */
+    abstract public void onPreferencesMenuSelected();
+
+    /**
+     * Used by the enhancer implementations to report errors.
+     *
+     * @param format A printf-like format string.
+     * @param args The parameters for the printf-like format string.
+     */
+    abstract public void printError(String format, Object...args);
+}
diff --git a/swtmenubar/src/main/java/com/android/menubar/IMenuBarEnhancer.java b/swtmenubar/src/main/java/com/android/menubar/IMenuBarEnhancer.java
new file mode 100644
index 0000000..d835bd6
--- /dev/null
+++ b/swtmenubar/src/main/java/com/android/menubar/IMenuBarEnhancer.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.menubar;
+
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Menu;
+
+
+/**
+ * Interface to the platform-specific MenuBarEnhancer implementation returned by
+ * {@link MenuBarEnhancer#setupMenu}.
+ */
+public interface IMenuBarEnhancer {
+
+    /** Values that indicate how the menu bar is being handlded. */
+    public enum MenuBarMode {
+        /**
+         * The Mac-specific About and Preferences are being used.
+         * No File > Exit menu should be provided by the application.
+         */
+        MAC_OS,
+        /**
+         * The provided SWT {@link Menu} is being used for About and Options.
+         * The application should provide a File > Exit menu.
+         */
+        GENERIC
+    }
+
+    /**
+     * Returns a {@link MenuBarMode} enum that indicates how the menu bar is going to
+     * or has been modified. This is implementation specific and can be called before or
+     * after {@link #setupMenu}.
+     * <p/>
+     * Callers would typically call that to know if they need to hide or display
+     * menu items. For example when {@link MenuBarMode#MAC_OS} is used, an app
+     * would typically not need to provide any "File > Exit" menu item.
+     *
+     * @return One of the {@link MenuBarMode} values.
+     */
+    public MenuBarMode getMenuBarMode();
+
+    /**
+     * Updates the menu bar to provide an About menu item and a Preferences menu item.
+     * Depending on the platform, the menu items might be decorated with the
+     * given {@code appName}.
+     * <p/>
+     * Users should not call this directly.
+     * {@link MenuBarEnhancer#setupMenu} should be used instead.
+     *
+     * @param appName Name used for the About menu item and similar. Must not be null.
+     * @param display The SWT display. Must not be null.
+     * @param callbacks Callbacks called when "About" and "Preferences" menu items are invoked.
+     *          Must not be null.
+     */
+    public void setupMenu(
+            String appName,
+            Display display,
+            IMenuBarCallback callbacks);
+}
diff --git a/swtmenubar/src/main/java/com/android/menubar/MenuBarEnhancer.java b/swtmenubar/src/main/java/com/android/menubar/MenuBarEnhancer.java
new file mode 100644
index 0000000..8fc8213
--- /dev/null
+++ b/swtmenubar/src/main/java/com/android/menubar/MenuBarEnhancer.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.menubar;
+
+import com.android.menubar.IMenuBarEnhancer.MenuBarMode;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.IMenuManager;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
+
+
+/**
+ * On Mac, {@link MenuBarEnhancer#setupMenu} plugs a listener on the About and the
+ * Preferences menu items of the standard "application" menu in the menu bar.
+ * On Windows or Linux, it adds relevant items to a given {@link Menu} linked to
+ * the same listeners.
+ */
+public final class MenuBarEnhancer {
+
+    private MenuBarEnhancer() {
+    }
+
+    /**
+     * Creates an instance of {@link IMenuBarEnhancer} specific to the current platform
+     * and invoke its {@link IMenuBarEnhancer#setupMenu} to updates the menu bar.
+     * <p/>
+     * Depending on the platform, this will either hook into the existing About menu item
+     * and a Preferences or Options menu item or add new ones to the given {@code swtMenu}.
+     * Depending on the platform, the menu items might be decorated with the
+     * given {@code appName}.
+     * <p/>
+     * Potential errors are reported through {@link IMenuBarCallback}.
+     *
+     * @param appName Name used for the About menu item and similar. Must not be null.
+     * @param swtMenu For non-mac platform this is the menu where the "About" and
+     *          the "Options" menu items are created. Typically the menu might be
+     *          called "Tools". Must not be null.
+     * @param callbacks Callbacks called when "About" and "Preferences" menu items are invoked.
+     *          Must not be null.
+     * @return An actual {@link IMenuBarEnhancer} implementation. Can be null on failure.
+     *          This is currently not of any use for the caller but is left in case
+     *          we want to expand the functionality later.
+     */
+    public static IMenuBarEnhancer setupMenu(
+            String appName,
+            final Menu swtMenu,
+            IMenuBarCallback callbacks) {
+
+        IMenuBarEnhancer enhancer = getEnhancer(callbacks, swtMenu.getDisplay());
+
+        // Default implementation for generic platforms
+        if (enhancer == null) {
+            enhancer = getGenericEnhancer(swtMenu);
+        }
+
+        try {
+            enhancer.setupMenu(appName, swtMenu.getDisplay(), callbacks);
+        } catch (Exception e) {
+            // If the enhancer failed, try to fall back on the generic one
+            if (enhancer.getMenuBarMode() != MenuBarMode.GENERIC) {
+                enhancer = getGenericEnhancer(swtMenu);
+                try {
+                    enhancer.setupMenu(appName, swtMenu.getDisplay(), callbacks);
+                } catch (Exception e2) {
+                    callbacks.printError("SWTMenuBar failed: %s", e2.toString());
+                    return null;
+                }
+            }
+        }
+        return enhancer;
+    }
+
+    private static IMenuBarEnhancer getGenericEnhancer(final Menu swtMenu) {
+        IMenuBarEnhancer enhancer;
+        enhancer = new IMenuBarEnhancer() {
+
+            @Override
+            public MenuBarMode getMenuBarMode() {
+                return MenuBarMode.GENERIC;
+            }
+
+            @Override
+            public void setupMenu(
+                    String appName,
+                    Display display,
+                    final IMenuBarCallback callbacks) {
+                if (swtMenu.getItemCount() > 0) {
+                    new MenuItem(swtMenu, SWT.SEPARATOR);
+                }
+
+                // Note: we use "Preferences" on Mac and "Options" on Windows/Linux.
+                final MenuItem pref = new MenuItem(swtMenu, SWT.NONE);
+                pref.setText("&Options...");
+
+                final MenuItem about = new MenuItem(swtMenu, SWT.NONE);
+                about.setText("&About...");
+
+                pref.addSelectionListener(new SelectionAdapter() {
+                    @Override
+                    public void widgetSelected(SelectionEvent e) {
+                        try {
+                            pref.setEnabled(false);
+                            callbacks.onPreferencesMenuSelected();
+                            super.widgetSelected(e);
+                        } finally {
+                            pref.setEnabled(true);
+                        }
+                    }
+                });
+
+                about.addSelectionListener(new SelectionAdapter() {
+                    @Override
+                    public void widgetSelected(SelectionEvent e) {
+                        try {
+                            about.setEnabled(false);
+                            callbacks.onAboutMenuSelected();
+                            super.widgetSelected(e);
+                        } finally {
+                            about.setEnabled(true);
+                        }
+                    }
+                });
+            }
+        };
+        return enhancer;
+    }
+
+
+    public static IMenuBarEnhancer setupMenuManager(
+            String appName,
+            Display display,
+            final IMenuManager menuManager,
+            final IAction aboutAction,
+            final IAction preferencesAction,
+            final IAction quitAction) {
+
+        IMenuBarCallback callbacks = new IMenuBarCallback() {
+            @Override
+            public void printError(String format, Object... args) {
+                System.err.println(String.format(format, args));
+            }
+
+            @Override
+            public void onPreferencesMenuSelected() {
+                if (preferencesAction != null) {
+                    preferencesAction.run();
+                }
+            }
+
+            @Override
+            public void onAboutMenuSelected() {
+                if (aboutAction != null) {
+                    aboutAction.run();
+                }
+            }
+        };
+
+        IMenuBarEnhancer enhancer = getEnhancer(callbacks, display);
+
+        // Default implementation for generic platforms
+        if (enhancer == null) {
+            enhancer = new IMenuBarEnhancer() {
+
+                @Override
+                public MenuBarMode getMenuBarMode() {
+                    return MenuBarMode.GENERIC;
+                }
+
+                @Override
+                public void setupMenu(
+                        String appName,
+                        Display display,
+                        final IMenuBarCallback callbacks) {
+                    if (!menuManager.isEmpty()) {
+                        menuManager.add(new Separator());
+                    }
+
+                    if (aboutAction != null) {
+                        menuManager.add(aboutAction);
+                    }
+                    if (preferencesAction != null) {
+                        menuManager.add(preferencesAction);
+                    }
+                    if (quitAction != null) {
+                        if (aboutAction != null || preferencesAction != null) {
+                            menuManager.add(new Separator());
+                        }
+                        menuManager.add(quitAction);
+                    }
+                }
+            };
+        }
+
+        enhancer.setupMenu(appName, display, callbacks);
+        return enhancer;
+    }
+
+    private static IMenuBarEnhancer getEnhancer(IMenuBarCallback callbacks, Display display) {
+        IMenuBarEnhancer enhancer = null;
+        String p = SWT.getPlatform();
+        String className = null;
+        if ("cocoa".equals(p)) {                                                  //$NON-NLS-1$
+            className = "com.android.menubar.internal.MenuBarEnhancerCocoa";      //$NON-NLS-1$
+
+            if (SWT.getVersion() >= 3700 && MenuBarEnhancer37.isSupported(display)) {
+                className = MenuBarEnhancer37.class.getName();
+            }
+        }
+
+        if (System.getenv("DEBUG_SWTMENUBAR") != null) {
+            callbacks.printError("DEBUG SwtMenuBar: SWT=%1$s, class=%2$s", p, className);
+        }
+
+        if (className != null) {
+            try {
+                Class<?> clazz = Class.forName(className);
+                enhancer = (IMenuBarEnhancer) clazz.newInstance();
+            } catch (Exception e) {
+                // Log an error and fallback on the default implementation.
+                callbacks.printError(
+                        "Failed to instantiate %1$s: %2$s",                       //$NON-NLS-1$
+                        className,
+                        e.toString());
+            }
+        }
+        return enhancer;
+    }
+}
diff --git a/swtmenubar/src/main/java/com/android/menubar/MenuBarEnhancer37.java b/swtmenubar/src/main/java/com/android/menubar/MenuBarEnhancer37.java
new file mode 100644
index 0000000..0e8df09
--- /dev/null
+++ b/swtmenubar/src/main/java/com/android/menubar/MenuBarEnhancer37.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.
+ *
+ * References:
+ * Based on the SWT snippet example at
+ * http://dev.eclipse.org/viewcvs/viewvc.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet354.java?view=co
+ */
+
+package com.android.menubar;
+
+
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
+
+import java.lang.reflect.Method;
+
+public class MenuBarEnhancer37 implements IMenuBarEnhancer {
+
+    private static final int kAboutMenuItem = -1;           // SWT.ID_ABOUT       in SWT 3.7
+    private static final int kPreferencesMenuItem = -2;     // SWT.ID_PREFERENCES in SWT 3.7
+    private static final int kQuitMenuItem = -6;            // SWT.ID_QUIT        in SWT 3.7
+
+    public MenuBarEnhancer37() {
+    }
+
+    @Override
+    public MenuBarMode getMenuBarMode() {
+        return MenuBarMode.MAC_OS;
+    }
+
+    /**
+     * Setup the About and Preferences native menut items with the
+     * given application name and links them to the callback.
+     *
+     * @param appName The application name.
+     * @param display The SWT display. Must not be null.
+     * @param callbacks The callbacks invoked by the menus.
+     */
+    @Override
+    public void setupMenu(
+            String appName,
+            Display display,
+            IMenuBarCallback callbacks) {
+
+        try {
+            // Initialize the menuItems.
+            initialize(display, appName, callbacks);
+        } catch (Exception e) {
+            throw new IllegalStateException(e);
+        }
+
+        // Schedule disposal of callback object
+        display.disposeExec(new Runnable() {
+            @Override
+            public void run() {
+            }
+        });
+    }
+
+    /**
+     * Checks whether the required SWT 3.7 APIs are available.
+     * <br/>
+     * Calling this will load the class, which is OK since this class doesn't
+     * directly use any SWT 3.7 API -- instead it uses reflection so that the
+     * code can be loaded under SWT 3.6.
+     *
+     * @param display The current SWT display.
+     * @return True if the SWT 3.7 API are available and this enhancer can be used.
+     */
+    public static boolean isSupported(Display display) {
+        try {
+            Object sysMenu = call0(display, "getSystemMenu");
+            if (sysMenu instanceof Menu) {
+                return findMenuById((Menu)sysMenu, kPreferencesMenuItem) != null &&
+                       findMenuById((Menu)sysMenu, kAboutMenuItem) != null;
+            }
+        } catch (Exception ignore) {}
+        return false;
+    }
+
+    private void initialize(
+            Display display,
+            String appName,
+            final IMenuBarCallback callbacks)
+                    throws Exception {
+        Object sysMenu = call0(display, "getSystemMenu");
+        if (sysMenu instanceof Menu) {
+            MenuItem menu = findMenuById((Menu)sysMenu, kPreferencesMenuItem);
+            if (menu != null) {
+                menu.addSelectionListener(new SelectionAdapter() {
+                    @Override
+                    public void widgetSelected(SelectionEvent event) {
+                        callbacks.onPreferencesMenuSelected();
+                    }
+                });
+            }
+
+            menu = findMenuById((Menu)sysMenu, kAboutMenuItem);
+            if (menu != null) {
+                menu.addSelectionListener(new SelectionAdapter() {
+                    @Override
+                    public void widgetSelected(SelectionEvent event) {
+                        callbacks.onAboutMenuSelected();
+                    }
+                });
+                menu.setText("About " + appName);
+            }
+
+            menu = findMenuById((Menu)sysMenu, kQuitMenuItem);
+            if (menu != null) {
+                // We already support the "quit" operation, no need for an extra handler here.
+                menu.setText("Quit " + appName);
+            }
+
+        }
+    }
+
+    private static Object call0(Object obj, String method) {
+        try {
+            Method m = obj.getClass().getMethod(method, (Class<?>[])null);
+            if (m != null) {
+                return m.invoke(obj, (Object[])null);
+            }
+        } catch (Exception ignore) {}
+        return null;
+    }
+
+    private static MenuItem findMenuById(Menu menu, int id) {
+        MenuItem[] items = menu.getItems();
+        for (int i = items.length - 1; i >= 0; i--) {
+            MenuItem item = items[i];
+            Object menuId = call0(item, "getID");
+            if (menuId instanceof Integer) {
+                if (((Integer) menuId).intValue() == id) {
+                    return item;
+                }
+            }
+        }
+        return null;
+    }
+}
diff --git a/traceview/.classpath b/traceview/.classpath
new file mode 100644
index 0000000..b6b7f30
--- /dev/null
+++ b/traceview/.classpath
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry excluding="resources/" kind="src" path="src/main/java"/>
+	<classpathentry kind="src" path="src/main/java/resources"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+	<classpathentry combineaccessrules="false" exported="true" kind="src" path="/common"/>
+	<classpathentry exported="true" kind="var" path="ANDROID_OUT_FRAMEWORK/swt.jar"/>
+	<classpathentry combineaccessrules="false" exported="true" kind="src" path="/sdkstats"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/traceview/.project b/traceview/.project
new file mode 100644
index 0000000..692297f
--- /dev/null
+++ b/traceview/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>traceview</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>
diff --git a/traceview/.settings/README.txt b/traceview/.settings/README.txt
new file mode 100644
index 0000000..9120b20
--- /dev/null
+++ b/traceview/.settings/README.txt
@@ -0,0 +1,2 @@
+Copy this in eclipse project as a .settings folder at the root.
+This ensure proper compilation compliance and warning/error levels.
\ No newline at end of file
diff --git a/traceview/.settings/org.eclipse.jdt.core.prefs b/traceview/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..9dbff07
--- /dev/null
+++ b/traceview/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,98 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
+org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault
+org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled
+org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
+org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
+org.eclipse.jdt.core.compiler.problem.deadCode=warning
+org.eclipse.jdt.core.compiler.problem.deprecation=warning
+org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
+org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
+org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
+org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning
+org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled
+org.eclipse.jdt.core.compiler.problem.fieldHiding=warning
+org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
+org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=error
+org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
+org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled
+org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
+org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning
+org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning
+org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
+org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
+org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
+org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
+org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
+org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
+org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
+org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
+org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
+org.eclipse.jdt.core.compiler.problem.nullReference=error
+org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning
+org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore
+org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
+org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning
+org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
+org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
+org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
+org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled
+org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
+org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
+org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
+org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled
+org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning
+org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error
+org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning
+org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.unusedImport=warning
+org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
+org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
+org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning
+org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled
+org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
+org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
+org.eclipse.jdt.core.compiler.source=1.6
diff --git a/traceview/NOTICE b/traceview/NOTICE
new file mode 100644
index 0000000..c5b1efa
--- /dev/null
+++ b/traceview/NOTICE
@@ -0,0 +1,190 @@
+
+   Copyright (c) 2005-2008, 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.
+
+   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.
+
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
diff --git a/traceview/README b/traceview/README
new file mode 100644
index 0000000..6f4576a
--- /dev/null
+++ b/traceview/README
@@ -0,0 +1,11 @@
+Using the Eclipse projects for traceview.
+
+traceview requires SWT to compile.
+
+SWT is available in the depot under //device/prebuild/<platform>/swt
+
+Because the build path cannot contain relative path that are not inside the project directory,
+the .classpath file references a user library called ANDROID_SWT.
+
+In order to compile the project, make a user library called ANDROID_SWT containing the jar
+available at //device/prebuild/<platform>/swt.
diff --git a/traceview/etc/traceview b/traceview/etc/traceview
new file mode 100755
index 0000000..cd4a25f
--- /dev/null
+++ b/traceview/etc/traceview
@@ -0,0 +1,108 @@
+#!/bin/bash
+#
+# Copyright 2005-2006, 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.
+
+# Set up prog to be the path of this script, including following symlinks,
+# and set up progdir to be the fully-qualified pathname of its directory.
+prog="$0"
+while [ -h "${prog}" ]; do
+    newProg=`/bin/ls -ld "${prog}"`
+    newProg=`expr "${newProg}" : ".* -> \(.*\)$"`
+    if expr "x${newProg}" : 'x/' >/dev/null; then
+        prog="${newProg}"
+    else
+        progdir=`dirname "${prog}"`
+        prog="${progdir}/${newProg}"
+    fi
+done
+oldwd=`pwd`
+progdir=`dirname "${prog}"`
+progname=`basename "${prog}"`
+cd "${progdir}"
+progdir=`pwd`
+prog="${progdir}"/"${progname}"
+cd "${oldwd}"
+
+jarfile=traceview.jar
+frameworkdir="$progdir"
+libdir="$progdir"
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+    frameworkdir=`dirname "$progdir"`/tools/lib
+    libdir=`dirname "$progdir"`/tools/lib
+fi
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+    frameworkdir=`dirname "$progdir"`/framework
+    libdir=`dirname "$progdir"`/lib
+fi
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+    echo "${progname}: can't find $jarfile"
+    exit 1
+fi
+
+javaCmd="java"
+
+os=`uname`
+if [ $os == 'Darwin' ]; then
+  javaOpts="-Xmx1600M -XstartOnFirstThread"
+else
+  javaOpts="-Xmx1600M"
+fi
+
+if [ `uname` = "Linux" ]; then
+    export GDK_NATIVE_WINDOWS=true
+fi
+
+while expr "x$1" : 'x-J' >/dev/null; do
+    opt=`expr "x$1" : 'x-J\(.*\)'`
+    javaOpts="${javaOpts} -${opt}"
+    shift
+done
+
+jarpath="$frameworkdir/$jarfile"
+
+# Figure out the path to the swt.jar for the current architecture.
+# if ANDROID_SWT is defined, then just use this.
+# else, if running in the Android source tree, then look for the correct swt folder in prebuilt
+# else, look for the correct swt folder in the SDK under tools/lib/
+swtpath=""
+if [ -n "$ANDROID_SWT" ]; then
+    swtpath="$ANDROID_SWT"
+else
+    vmarch=`${javaCmd} -jar "${frameworkdir}"/archquery.jar`
+    if [ -n "$ANDROID_BUILD_TOP" ]; then
+        osname=`uname -s | tr A-Z a-z`
+        swtpath="${ANDROID_BUILD_TOP}/prebuilts/tools/${osname}-${vmarch}/swt"
+    else
+        swtpath="${frameworkdir}/${vmarch}"
+    fi
+fi
+
+# Combine the swtpath and the framework dir path.
+if [ -d "$swtpath" ]; then
+    frameworkdir="${swtpath}:${frameworkdir}"
+else
+    echo "SWT folder '${swtpath}' does not exist."
+    echo "Please export ANDROID_SWT to point to the folder containing swt.jar for your platform."
+    exit 1
+fi
+
+if [ -x $progdir/monitor ]; then
+    echo "The standalone version of traceview is deprecated."
+    echo "Please use Android Device Monitor (tools/monitor) instead."
+fi
+exec "${javaCmd}" $javaOpts -Djava.ext.dirs="$frameworkdir" -Dcom.android.traceview.toolsdir="$progdir" -jar "$jarpath" "$@"
diff --git a/traceview/etc/traceview.bat b/traceview/etc/traceview.bat
new file mode 100755
index 0000000..63416dd
--- /dev/null
+++ b/traceview/etc/traceview.bat
@@ -0,0 +1,65 @@
+ at echo off
+rem Copyright (C) 2007 The Android Open Source Project
+rem
+rem Licensed under the Apache License, Version 2.0 (the "License");
+rem you may not use this file except in compliance with the License.
+rem You may obtain a copy of the License at
+rem
+rem      http://www.apache.org/licenses/LICENSE-2.0
+rem
+rem Unless required by applicable law or agreed to in writing, software
+rem distributed under the License is distributed on an "AS IS" BASIS,
+rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+rem See the License for the specific language governing permissions and
+rem limitations under the License.
+
+rem don't modify the caller's environment
+setlocal
+
+rem Set up prog to be the path of this script, including following symlinks,
+rem and set up progdir to be the fully-qualified pathname of its directory.
+set prog=%~f0
+
+rem Change current directory and drive to where the script is, to avoid
+rem issues with directories containing whitespaces.
+cd /d %~dp0
+
+rem Check we have a valid Java.exe in the path.
+set java_exe=
+call lib\find_java.bat
+if not defined java_exe goto :EOF
+
+set jarfile=traceview.jar
+set frameworkdir=
+
+if exist %frameworkdir%%jarfile% goto JarFileOk
+    set frameworkdir=lib\
+
+if exist %frameworkdir%%jarfile% goto JarFileOk
+    set frameworkdir=..\framework\
+
+:JarFileOk
+
+set jarpath=%frameworkdir%%jarfile%
+
+if not defined ANDROID_SWT goto QueryArch
+    set swt_path=%ANDROID_SWT%
+    goto SwtDone
+
+:QueryArch
+
+    for /f %%a in ('%java_exe% -jar %frameworkdir%archquery.jar') do set swt_path=%frameworkdir%%%a
+
+:SwtDone
+
+if exist %swt_path% goto SetPath
+    echo SWT folder '%swt_path%' does not exist.
+    echo Please set ANDROID_SWT to point to the folder containing swt.jar for your platform.
+    exit /B
+
+:SetPath
+set javaextdirs=%swt_path%;%frameworkdir%
+
+echo The standalone version of traceview is deprecated.
+echo Please use Android Device Monitor (tools/monitor) instead.
+call %java_exe% -Djava.ext.dirs=%javaextdirs% -Dcom.android.traceview.toolsdir= -jar %jarpath% %*
diff --git a/traceview/src/main/java/com/android/traceview/Call.java b/traceview/src/main/java/com/android/traceview/Call.java
new file mode 100644
index 0000000..0330b05
--- /dev/null
+++ b/traceview/src/main/java/com/android/traceview/Call.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2006 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.traceview;
+
+import org.eclipse.swt.graphics.Color;
+
+class Call implements TimeLineView.Block {
+    final private ThreadData mThreadData;
+    final private MethodData mMethodData;
+    final Call mCaller; // the caller, or null if this is the root
+
+    private String mName;
+    private boolean mIsRecursive;
+
+    long mGlobalStartTime;
+    long mGlobalEndTime;
+
+    long mThreadStartTime;
+    long mThreadEndTime;
+
+    long mInclusiveRealTime; // real time spent in this call including its children
+    long mExclusiveRealTime; // real time spent in this call including its children
+
+    long mInclusiveCpuTime; // cpu time spent in this call including its children
+    long mExclusiveCpuTime; // cpu time spent in this call excluding its children
+
+    Call(ThreadData threadData, MethodData methodData, Call caller) {
+        mThreadData = threadData;
+        mMethodData = methodData;
+        mName = methodData.getProfileName();
+        mCaller = caller;
+    }
+
+    public void updateName() {
+        mName = mMethodData.getProfileName();
+    }
+
+    @Override
+    public double addWeight(int x, int y, double weight) {
+        return mMethodData.addWeight(x, y, weight);
+    }
+
+    @Override
+    public void clearWeight() {
+        mMethodData.clearWeight();
+    }
+
+    @Override
+    public long getStartTime() {
+        return mGlobalStartTime;
+    }
+
+    @Override
+    public long getEndTime() {
+        return mGlobalEndTime;
+    }
+
+    @Override
+    public long getExclusiveCpuTime() {
+        return mExclusiveCpuTime;
+    }
+
+    @Override
+    public long getInclusiveCpuTime() {
+        return mInclusiveCpuTime;
+    }
+
+    @Override
+    public long getExclusiveRealTime() {
+        return mExclusiveRealTime;
+    }
+
+    @Override
+    public long getInclusiveRealTime() {
+        return mInclusiveRealTime;
+    }
+
+    @Override
+    public Color getColor() {
+        return mMethodData.getColor();
+    }
+
+    @Override
+    public String getName() {
+        return mName;
+    }
+
+    public void setName(String name) {
+        mName = name;
+    }
+
+    public ThreadData getThreadData() {
+        return mThreadData;
+    }
+
+    public int getThreadId() {
+        return mThreadData.getId();
+    }
+
+    @Override
+    public MethodData getMethodData() {
+        return mMethodData;
+    }
+
+    @Override
+    public boolean isContextSwitch() {
+        return mMethodData.getId() == -1;
+    }
+
+    @Override
+    public boolean isIgnoredBlock() {
+        // Ignore the top-level call or context switches within the top-level call.
+        return mCaller == null || isContextSwitch() && mCaller.mCaller == null;
+    }
+
+    @Override
+    public TimeLineView.Block getParentBlock() {
+        return mCaller;
+    }
+
+    public boolean isRecursive() {
+        return mIsRecursive;
+    }
+
+    void setRecursive(boolean isRecursive) {
+        mIsRecursive = isRecursive;
+    }
+
+    void addCpuTime(long elapsedCpuTime) {
+        mExclusiveCpuTime += elapsedCpuTime;
+        mInclusiveCpuTime += elapsedCpuTime;
+    }
+
+    /**
+     * Record time spent in the method call.
+     */
+    void finish() {
+        if (mCaller != null) {
+            mCaller.mInclusiveCpuTime += mInclusiveCpuTime;
+            mCaller.mInclusiveRealTime += mInclusiveRealTime;
+        }
+
+        mMethodData.addElapsedExclusive(mExclusiveCpuTime, mExclusiveRealTime);
+        if (!mIsRecursive) {
+            mMethodData.addTopExclusive(mExclusiveCpuTime, mExclusiveRealTime);
+        }
+        mMethodData.addElapsedInclusive(mInclusiveCpuTime, mInclusiveRealTime,
+                mIsRecursive, mCaller);
+    }
+
+    public static final class TraceAction {
+        public static final int ACTION_ENTER = 0;
+        public static final int ACTION_EXIT = 1;
+
+        public final int mAction;
+        public final Call mCall;
+
+        public TraceAction(int action, Call call) {
+            mAction = action;
+            mCall = call;
+        }
+    }
+}
diff --git a/traceview/src/main/java/com/android/traceview/ColorController.java b/traceview/src/main/java/com/android/traceview/ColorController.java
new file mode 100644
index 0000000..f5e4c0d
--- /dev/null
+++ b/traceview/src/main/java/com/android/traceview/ColorController.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2006 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.traceview;
+
+import java.util.HashMap;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.widgets.Display;
+
+public class ColorController {
+    private static final int[] systemColors = { SWT.COLOR_BLUE, SWT.COLOR_RED,
+        SWT.COLOR_GREEN, SWT.COLOR_CYAN, SWT.COLOR_MAGENTA, SWT.COLOR_DARK_BLUE,
+        SWT.COLOR_DARK_RED, SWT.COLOR_DARK_GREEN, SWT.COLOR_DARK_YELLOW,
+        SWT.COLOR_DARK_CYAN, SWT.COLOR_DARK_MAGENTA, SWT.COLOR_BLACK };
+
+    private static RGB[] rgbColors = { new RGB(90, 90, 255), // blue
+            new RGB(0, 240, 0), // green
+            new RGB(255, 0, 0), // red
+            new RGB(0, 255, 255), // cyan
+            new RGB(255, 80, 255), // magenta
+            new RGB(200, 200, 0), // yellow
+            new RGB(40, 0, 200), // dark blue
+            new RGB(150, 255, 150), // light green
+            new RGB(150, 0, 0), // dark red
+            new RGB(30, 150, 150), // dark cyan
+            new RGB(200, 200, 255), // light blue
+            new RGB(0, 120, 0), // dark green
+            new RGB(255, 150, 150), // light red
+            new RGB(140, 80, 140), // dark magenta
+            new RGB(150, 100, 50), // brown
+            new RGB(70, 70, 70), // dark grey
+    };
+
+    private static HashMap<Integer, Color> colorCache = new HashMap<Integer, Color>();
+    private static HashMap<Integer, Image> imageCache = new HashMap<Integer, Image>();
+
+    public ColorController() {
+    }
+
+    public static Color requestColor(Display display, RGB rgb) {
+        return requestColor(display, rgb.red, rgb.green, rgb.blue);
+    }
+
+    public static Image requestColorSquare(Display display, RGB rgb) {
+        return requestColorSquare(display, rgb.red, rgb.green, rgb.blue);
+    }
+
+    public static Color requestColor(Display display, int red, int green, int blue) {
+        int key = (red << 16) | (green << 8) | blue;
+        Color color = colorCache.get(key);
+        if (color == null) {
+            color = new Color(display, red, green, blue);
+            colorCache.put(key, color);
+        }
+        return color;
+    }
+
+    public static Image requestColorSquare(Display display, int red, int green, int blue) {
+        int key = (red << 16) | (green << 8) | blue;
+        Image image = imageCache.get(key);
+        if (image == null) {
+            image = new Image(display, 8, 14);
+            GC gc = new GC(image);
+            Color color = requestColor(display, red, green, blue);
+            gc.setBackground(color);
+            gc.fillRectangle(image.getBounds());
+            gc.dispose();
+            imageCache.put(key, image);
+        }
+        return image;
+    }
+
+    public static void assignMethodColors(Display display, MethodData[] methods) {
+        int nextColorIndex = 0;
+        for (MethodData md : methods) {
+            RGB rgb = rgbColors[nextColorIndex];
+            if (++nextColorIndex == rgbColors.length)
+                nextColorIndex = 0;
+            Color color = requestColor(display, rgb);
+            Image image = requestColorSquare(display, rgb);
+            md.setColor(color);
+            md.setImage(image);
+
+            // Compute and set a faded color
+            int fadedRed = 150 + rgb.red / 4;
+            int fadedGreen = 150 + rgb.green / 4;
+            int fadedBlue = 150 + rgb.blue / 4;
+            RGB faded = new RGB(fadedRed, fadedGreen, fadedBlue);
+            color = requestColor(display, faded);
+            image = requestColorSquare(display, faded);
+            md.setFadedColor(color);
+            md.setFadedImage(image);
+        }
+    }
+}
diff --git a/traceview/src/main/java/com/android/traceview/DmTraceReader.java b/traceview/src/main/java/com/android/traceview/DmTraceReader.java
new file mode 100644
index 0000000..9bd6882
--- /dev/null
+++ b/traceview/src/main/java/com/android/traceview/DmTraceReader.java
@@ -0,0 +1,754 @@
+/*
+ * Copyright (C) 2006 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.traceview;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteOrder;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class DmTraceReader extends TraceReader {
+    private static final int TRACE_MAGIC = 0x574f4c53;
+
+    private static final int METHOD_TRACE_ENTER = 0x00; // method entry
+    private static final int METHOD_TRACE_EXIT = 0x01; // method exit
+    private static final int METHOD_TRACE_UNROLL = 0x02; // method exited by exception unrolling
+
+    // When in dual clock mode, we report that a context switch has occurred
+    // when skew between the real time and thread cpu clocks is more than this
+    // many microseconds.
+    private static final long MIN_CONTEXT_SWITCH_TIME_USEC = 100;
+
+    private enum ClockSource {
+        THREAD_CPU, WALL, DUAL,
+    };
+
+    private int mVersionNumber;
+    private boolean mRegression;
+    private ProfileProvider mProfileProvider;
+    private String mTraceFileName;
+    private MethodData mTopLevel;
+    private ArrayList<Call> mCallList;
+    private HashMap<String, String> mPropertiesMap;
+    private HashMap<Integer, MethodData> mMethodMap;
+    private HashMap<Integer, ThreadData> mThreadMap;
+    private ThreadData[] mSortedThreads;
+    private MethodData[] mSortedMethods;
+    private long mTotalCpuTime;
+    private long mTotalRealTime;
+    private MethodData mContextSwitch;
+    private int mRecordSize;
+    private ClockSource mClockSource;
+
+    // A regex for matching the thread "id name" lines in the .key file
+    private static final Pattern mIdNamePattern = Pattern.compile("(\\d+)\t(.*)");  //$NON-NLS-1$
+
+    public DmTraceReader(String traceFileName, boolean regression) throws IOException {
+        mTraceFileName = traceFileName;
+        mRegression = regression;
+        mPropertiesMap = new HashMap<String, String>();
+        mMethodMap = new HashMap<Integer, MethodData>();
+        mThreadMap = new HashMap<Integer, ThreadData>();
+        mCallList = new ArrayList<Call>();
+
+        // Create a single top-level MethodData object to hold the profile data
+        // for time spent in the unknown caller.
+        mTopLevel = new MethodData(0, "(toplevel)");
+        mContextSwitch = new MethodData(-1, "(context switch)");
+        mMethodMap.put(0, mTopLevel);
+        mMethodMap.put(-1, mContextSwitch);
+        generateTrees();
+    }
+
+    void generateTrees() throws IOException {
+        long offset = parseKeys();
+        parseData(offset);
+        analyzeData();
+    }
+
+    @Override
+    public ProfileProvider getProfileProvider() {
+        if (mProfileProvider == null)
+            mProfileProvider = new ProfileProvider(this);
+        return mProfileProvider;
+    }
+
+    private MappedByteBuffer mapFile(String filename, long offset) throws IOException {
+        MappedByteBuffer buffer = null;
+        FileInputStream dataFile = new FileInputStream(filename);
+        try {
+            File file = new File(filename);
+            FileChannel fc = dataFile.getChannel();
+            buffer = fc.map(FileChannel.MapMode.READ_ONLY, offset, file.length() - offset);
+            buffer.order(ByteOrder.LITTLE_ENDIAN);
+
+            return buffer;
+        } finally {
+            dataFile.close(); // this *also* closes the associated channel, fc
+        }
+    }
+
+    private void readDataFileHeader(MappedByteBuffer buffer) {
+        int magic = buffer.getInt();
+        if (magic != TRACE_MAGIC) {
+            System.err.printf(
+                    "Error: magic number mismatch; got 0x%x, expected 0x%x\n",
+                    magic, TRACE_MAGIC);
+            throw new RuntimeException();
+        }
+
+        // read version
+        int version = buffer.getShort();
+        if (version != mVersionNumber) {
+            System.err.printf(
+                    "Error: version number mismatch; got %d in data header but %d in options\n",
+                    version, mVersionNumber);
+            throw new RuntimeException();
+        }
+        if (version < 1 || version > 3) {
+            System.err.printf(
+                    "Error: unsupported trace version number %d.  "
+                    + "Please use a newer version of TraceView to read this file.", version);
+            throw new RuntimeException();
+        }
+
+        // read offset
+        int offsetToData = buffer.getShort() - 16;
+
+        // read startWhen
+        buffer.getLong();
+
+        // read record size
+        if (version == 1) {
+            mRecordSize = 9;
+        } else if (version == 2) {
+            mRecordSize = 10;
+        } else {
+            mRecordSize = buffer.getShort();
+            offsetToData -= 2;
+        }
+
+        // Skip over offsetToData bytes
+        while (offsetToData-- > 0) {
+            buffer.get();
+        }
+    }
+
+    private void parseData(long offset) throws IOException {
+        MappedByteBuffer buffer = mapFile(mTraceFileName, offset);
+        readDataFileHeader(buffer);
+
+        ArrayList<TraceAction> trace = null;
+        if (mClockSource == ClockSource.THREAD_CPU) {
+            trace = new ArrayList<TraceAction>();
+        }
+
+        final boolean haveThreadClock = mClockSource != ClockSource.WALL;
+        final boolean haveGlobalClock = mClockSource != ClockSource.THREAD_CPU;
+
+        // Parse all call records to obtain elapsed time information.
+        ThreadData prevThreadData = null;
+        for (;;) {
+            int threadId;
+            int methodId;
+            long threadTime, globalTime;
+            try {
+                int recordSize = mRecordSize;
+
+                if (mVersionNumber == 1) {
+                    threadId = buffer.get();
+                    recordSize -= 1;
+                } else {
+                    threadId = buffer.getShort();
+                    recordSize -= 2;
+                }
+
+                methodId = buffer.getInt();
+                recordSize -= 4;
+
+                switch (mClockSource) {
+                    case WALL:
+                        threadTime = 0;
+                        globalTime = buffer.getInt();
+                        recordSize -= 4;
+                        break;
+                    case DUAL:
+                        threadTime = buffer.getInt();
+                        globalTime = buffer.getInt();
+                        recordSize -= 8;
+                        break;
+                    default:
+                    case THREAD_CPU:
+                        threadTime = buffer.getInt();
+                        globalTime = 0;
+                        recordSize -= 4;
+                        break;
+                }
+
+                while (recordSize-- > 0) {
+                    buffer.get();
+                }
+            } catch (BufferUnderflowException ex) {
+                break;
+            }
+
+            int methodAction = methodId & 0x03;
+            methodId = methodId & ~0x03;
+            MethodData methodData = mMethodMap.get(methodId);
+            if (methodData == null) {
+                String name = String.format("(0x%1$x)", methodId);  //$NON-NLS-1$
+                methodData = new MethodData(methodId, name);
+                mMethodMap.put(methodId, methodData);
+            }
+
+            ThreadData threadData = mThreadMap.get(threadId);
+            if (threadData == null) {
+                String name = String.format("[%1$d]", threadId);  //$NON-NLS-1$
+                threadData = new ThreadData(threadId, name, mTopLevel);
+                mThreadMap.put(threadId, threadData);
+            }
+
+            long elapsedGlobalTime = 0;
+            if (haveGlobalClock) {
+                if (!threadData.mHaveGlobalTime) {
+                    threadData.mGlobalStartTime = globalTime;
+                    threadData.mHaveGlobalTime = true;
+                } else {
+                    elapsedGlobalTime = globalTime - threadData.mGlobalEndTime;
+                }
+                threadData.mGlobalEndTime = globalTime;
+            }
+
+            if (haveThreadClock) {
+                long elapsedThreadTime = 0;
+                if (!threadData.mHaveThreadTime) {
+                    threadData.mThreadStartTime = threadTime;
+                    threadData.mThreadCurrentTime = threadTime;
+                    threadData.mHaveThreadTime = true;
+                } else {
+                    elapsedThreadTime = threadTime - threadData.mThreadEndTime;
+                }
+                threadData.mThreadEndTime = threadTime;
+
+                if (!haveGlobalClock) {
+                    // Detect context switches whenever execution appears to switch from one
+                    // thread to another.  This assumption is only valid on uniprocessor
+                    // systems (which is why we now have a dual clock mode).
+                    // We represent context switches in the trace by pushing a call record
+                    // with MethodData mContextSwitch onto the stack of the previous
+                    // thread.  We arbitrarily set the start and end time of the context
+                    // switch such that the context switch occurs in the middle of the thread
+                    // time and itself accounts for zero thread time.
+                    if (prevThreadData != null && prevThreadData != threadData) {
+                        // Begin context switch from previous thread.
+                        Call switchCall = prevThreadData.enter(mContextSwitch, trace);
+                        switchCall.mThreadStartTime = prevThreadData.mThreadEndTime;
+                        mCallList.add(switchCall);
+
+                        // Return from context switch to current thread.
+                        Call top = threadData.top();
+                        if (top.getMethodData() == mContextSwitch) {
+                            threadData.exit(mContextSwitch, trace);
+                            long beforeSwitch = elapsedThreadTime / 2;
+                            top.mThreadStartTime += beforeSwitch;
+                            top.mThreadEndTime = top.mThreadStartTime;
+                        }
+                    }
+                    prevThreadData = threadData;
+                } else {
+                    // If we have a global clock, then we can detect context switches (or blocking
+                    // calls or cpu suspensions or clock anomalies) by comparing global time to
+                    // thread time for successive calls that occur on the same thread.
+                    // As above, we represent the context switch using a special method call.
+                    long sleepTime = elapsedGlobalTime - elapsedThreadTime;
+                    if (sleepTime > MIN_CONTEXT_SWITCH_TIME_USEC) {
+                        Call switchCall = threadData.enter(mContextSwitch, trace);
+                        long beforeSwitch = elapsedThreadTime / 2;
+                        long afterSwitch = elapsedThreadTime - beforeSwitch;
+                        switchCall.mGlobalStartTime = globalTime - elapsedGlobalTime + beforeSwitch;
+                        switchCall.mGlobalEndTime = globalTime - afterSwitch;
+                        switchCall.mThreadStartTime = threadTime - afterSwitch;
+                        switchCall.mThreadEndTime = switchCall.mThreadStartTime;
+                        threadData.exit(mContextSwitch, trace);
+                        mCallList.add(switchCall);
+                    }
+                }
+
+                // Add thread CPU time.
+                Call top = threadData.top();
+                top.addCpuTime(elapsedThreadTime);
+            }
+
+            switch (methodAction) {
+                case METHOD_TRACE_ENTER: {
+                    Call call = threadData.enter(methodData, trace);
+                    if (haveGlobalClock) {
+                        call.mGlobalStartTime = globalTime;
+                    }
+                    if (haveThreadClock) {
+                        call.mThreadStartTime = threadTime;
+                    }
+                    mCallList.add(call);
+                    break;
+                }
+                case METHOD_TRACE_EXIT:
+                case METHOD_TRACE_UNROLL: {
+                    Call call = threadData.exit(methodData, trace);
+                    if (call != null) {
+                        if (haveGlobalClock) {
+                            call.mGlobalEndTime = globalTime;
+                        }
+                        if (haveThreadClock) {
+                            call.mThreadEndTime = threadTime;
+                        }
+                    }
+                    break;
+                }
+                default:
+                    throw new RuntimeException("Unrecognized method action: " + methodAction);
+            }
+        }
+
+        // Exit any pending open-ended calls.
+        for (ThreadData threadData : mThreadMap.values()) {
+            threadData.endTrace(trace);
+        }
+
+        // Recreate the global timeline from thread times, if needed.
+        if (!haveGlobalClock) {
+            long globalTime = 0;
+            prevThreadData = null;
+            for (TraceAction traceAction : trace) {
+                Call call = traceAction.mCall;
+                ThreadData threadData = call.getThreadData();
+
+                if (traceAction.mAction == TraceAction.ACTION_ENTER) {
+                    long threadTime = call.mThreadStartTime;
+                    globalTime += call.mThreadStartTime - threadData.mThreadCurrentTime;
+                    call.mGlobalStartTime = globalTime;
+                    if (!threadData.mHaveGlobalTime) {
+                        threadData.mHaveGlobalTime = true;
+                        threadData.mGlobalStartTime = globalTime;
+                    }
+                    threadData.mThreadCurrentTime = threadTime;
+                } else if (traceAction.mAction == TraceAction.ACTION_EXIT) {
+                    long threadTime = call.mThreadEndTime;
+                    globalTime += call.mThreadEndTime - threadData.mThreadCurrentTime;
+                    call.mGlobalEndTime = globalTime;
+                    threadData.mGlobalEndTime = globalTime;
+                    threadData.mThreadCurrentTime = threadTime;
+                } // else, ignore ACTION_INCOMPLETE calls, nothing to do
+                prevThreadData = threadData;
+            }
+        }
+
+        // Finish updating all calls and calculate the total time spent.
+        for (int i = mCallList.size() - 1; i >= 0; i--) {
+            Call call = mCallList.get(i);
+
+            // Calculate exclusive real-time by subtracting inclusive real time
+            // accumulated by children from the total span.
+            long realTime = call.mGlobalEndTime - call.mGlobalStartTime;
+            call.mExclusiveRealTime = Math.max(realTime - call.mInclusiveRealTime, 0);
+            call.mInclusiveRealTime = realTime;
+
+            call.finish();
+        }
+        mTotalCpuTime = 0;
+        mTotalRealTime = 0;
+        for (ThreadData threadData : mThreadMap.values()) {
+            Call rootCall = threadData.getRootCall();
+            threadData.updateRootCallTimeBounds();
+            rootCall.finish();
+            mTotalCpuTime += rootCall.mInclusiveCpuTime;
+            mTotalRealTime += rootCall.mInclusiveRealTime;
+        }
+
+        if (mRegression) {
+            System.out.format("totalCpuTime %dus\n", mTotalCpuTime);
+            System.out.format("totalRealTime %dus\n", mTotalRealTime);
+
+            dumpThreadTimes();
+            dumpCallTimes();
+        }
+    }
+
+    static final int PARSE_VERSION = 0;
+    static final int PARSE_THREADS = 1;
+    static final int PARSE_METHODS = 2;
+    static final int PARSE_OPTIONS = 4;
+
+    long parseKeys() throws IOException {
+        long offset = 0;
+        BufferedReader in = null;
+        try {
+            in = new BufferedReader(new InputStreamReader(
+                    new FileInputStream(mTraceFileName), "US-ASCII"));
+
+            int mode = PARSE_VERSION;
+            String line = null;
+            while (true) {
+                line = in.readLine();
+                if (line == null) {
+                    throw new IOException("Key section does not have an *end marker");
+                }
+
+                // Calculate how much we have read from the file so far.  The
+                // extra byte is for the line ending not included by readLine().
+                offset += line.length() + 1;
+                if (line.startsWith("*")) {
+                    if (line.equals("*version")) {
+                        mode = PARSE_VERSION;
+                        continue;
+                    }
+                    if (line.equals("*threads")) {
+                        mode = PARSE_THREADS;
+                        continue;
+                    }
+                    if (line.equals("*methods")) {
+                        mode = PARSE_METHODS;
+                        continue;
+                    }
+                    if (line.equals("*end")) {
+                        break;
+                    }
+                }
+                switch (mode) {
+                case PARSE_VERSION:
+                    mVersionNumber = Integer.decode(line);
+                    mode = PARSE_OPTIONS;
+                    break;
+                case PARSE_THREADS:
+                    parseThread(line);
+                    break;
+                case PARSE_METHODS:
+                    parseMethod(line);
+                    break;
+                case PARSE_OPTIONS:
+                    parseOption(line);
+                    break;
+                }
+            }
+        } catch (FileNotFoundException ex) {
+            System.err.println(ex.getMessage());
+        } finally {
+            if (in != null) {
+                in.close();
+            }
+        }
+
+        if (mClockSource == null) {
+            mClockSource = ClockSource.THREAD_CPU;
+        }
+
+        return offset;
+    }
+
+    void parseOption(String line) {
+        String[] tokens = line.split("=");
+        if (tokens.length == 2) {
+            String key = tokens[0];
+            String value = tokens[1];
+            mPropertiesMap.put(key, value);
+
+            if (key.equals("clock")) {
+                if (value.equals("thread-cpu")) {
+                    mClockSource = ClockSource.THREAD_CPU;
+                } else if (value.equals("wall")) {
+                    mClockSource = ClockSource.WALL;
+                } else if (value.equals("dual")) {
+                    mClockSource = ClockSource.DUAL;
+                }
+            }
+        }
+    }
+
+    void parseThread(String line) {
+        String idStr = null;
+        String name = null;
+        Matcher matcher = mIdNamePattern.matcher(line);
+        if (matcher.find()) {
+            idStr = matcher.group(1);
+            name = matcher.group(2);
+        }
+        if (idStr == null) return;
+        if (name == null) name = "(unknown)";
+
+        int id = Integer.decode(idStr);
+        mThreadMap.put(id, new ThreadData(id, name, mTopLevel));
+    }
+
+    void parseMethod(String line) {
+        String[] tokens = line.split("\t");
+        int id = Long.decode(tokens[0]).intValue();
+        String className = tokens[1];
+        String methodName = null;
+        String signature = null;
+        String pathname = null;
+        int lineNumber = -1;
+        if (tokens.length == 6) {
+            methodName = tokens[2];
+            signature = tokens[3];
+            pathname = tokens[4];
+            lineNumber = Integer.decode(tokens[5]);
+            pathname = constructPathname(className, pathname);
+        } else if (tokens.length > 2) {
+            if (tokens[3].startsWith("(")) {
+                methodName = tokens[2];
+                signature = tokens[3];
+            } else {
+                pathname = tokens[2];
+                lineNumber = Integer.decode(tokens[3]);
+            }
+        }
+
+        mMethodMap.put(id, new MethodData(id, className, methodName, signature,
+                pathname, lineNumber));
+    }
+
+    private String constructPathname(String className, String pathname) {
+        int index = className.lastIndexOf('/');
+        if (index > 0 && index < className.length() - 1
+                && pathname.endsWith(".java"))
+            pathname = className.substring(0, index + 1) + pathname;
+        return pathname;
+    }
+
+    private void analyzeData() {
+        final TimeBase timeBase = getPreferredTimeBase();
+
+        // Sort the threads into decreasing cpu time
+        Collection<ThreadData> tv = mThreadMap.values();
+        mSortedThreads = tv.toArray(new ThreadData[tv.size()]);
+        Arrays.sort(mSortedThreads, new Comparator<ThreadData>() {
+            @Override
+            public int compare(ThreadData td1, ThreadData td2) {
+                if (timeBase.getTime(td2) > timeBase.getTime(td1))
+                    return 1;
+                if (timeBase.getTime(td2) < timeBase.getTime(td1))
+                    return -1;
+                return td2.getName().compareTo(td1.getName());
+            }
+        });
+
+        // Sort the methods into decreasing inclusive time
+        Collection<MethodData> mv = mMethodMap.values();
+        MethodData[] methods;
+        methods = mv.toArray(new MethodData[mv.size()]);
+        Arrays.sort(methods, new Comparator<MethodData>() {
+            @Override
+            public int compare(MethodData md1, MethodData md2) {
+                if (timeBase.getElapsedInclusiveTime(md2) > timeBase.getElapsedInclusiveTime(md1))
+                    return 1;
+                if (timeBase.getElapsedInclusiveTime(md2) < timeBase.getElapsedInclusiveTime(md1))
+                    return -1;
+                return md1.getName().compareTo(md2.getName());
+            }
+        });
+
+        // Count the number of methods with non-zero inclusive time
+        int nonZero = 0;
+        for (MethodData md : methods) {
+            if (timeBase.getElapsedInclusiveTime(md) == 0)
+                break;
+            nonZero += 1;
+        }
+
+        // Copy the methods with non-zero time
+        mSortedMethods = new MethodData[nonZero];
+        int ii = 0;
+        for (MethodData md : methods) {
+            if (timeBase.getElapsedInclusiveTime(md) == 0)
+                break;
+            md.setRank(ii);
+            mSortedMethods[ii++] = md;
+        }
+
+        // Let each method analyze its profile data
+        for (MethodData md : mSortedMethods) {
+            md.analyzeData(timeBase);
+        }
+
+        // Update all the calls to include the method rank in
+        // their name.
+        for (Call call : mCallList) {
+            call.updateName();
+        }
+
+        if (mRegression) {
+            dumpMethodStats();
+        }
+    }
+
+    /*
+     * This method computes a list of records that describe the the execution
+     * timeline for each thread. Each record is a pair: (row, block) where: row:
+     * is the ThreadData object block: is the call (containing the start and end
+     * times)
+     */
+    @Override
+    public ArrayList<TimeLineView.Record> getThreadTimeRecords() {
+        TimeLineView.Record record;
+        ArrayList<TimeLineView.Record> timeRecs;
+        timeRecs = new ArrayList<TimeLineView.Record>();
+
+        // For each thread, push a "toplevel" call that encompasses the
+        // entire execution of the thread.
+        for (ThreadData threadData : mSortedThreads) {
+            if (!threadData.isEmpty() && threadData.getId() != 0) {
+                record = new TimeLineView.Record(threadData, threadData.getRootCall());
+                timeRecs.add(record);
+            }
+        }
+
+        for (Call call : mCallList) {
+            record = new TimeLineView.Record(call.getThreadData(), call);
+            timeRecs.add(record);
+        }
+
+        if (mRegression) {
+            dumpTimeRecs(timeRecs);
+            System.exit(0);
+        }
+        return timeRecs;
+    }
+
+    private void dumpThreadTimes() {
+        System.out.print("\nThread Times\n");
+        System.out.print("id  t-start    t-end  g-start    g-end     name\n");
+        for (ThreadData threadData : mThreadMap.values()) {
+            System.out.format("%2d %8d %8d %8d %8d  %s\n",
+                    threadData.getId(),
+                    threadData.mThreadStartTime, threadData.mThreadEndTime,
+                    threadData.mGlobalStartTime, threadData.mGlobalEndTime,
+                    threadData.getName());
+        }
+    }
+
+    private void dumpCallTimes() {
+        System.out.print("\nCall Times\n");
+        System.out.print("id  t-start    t-end  g-start    g-end    excl.    incl.  method\n");
+        for (Call call : mCallList) {
+            System.out.format("%2d %8d %8d %8d %8d %8d %8d  %s\n",
+                    call.getThreadId(), call.mThreadStartTime, call.mThreadEndTime,
+                    call.mGlobalStartTime, call.mGlobalEndTime,
+                    call.mExclusiveCpuTime, call.mInclusiveCpuTime,
+                    call.getMethodData().getName());
+        }
+    }
+
+    private void dumpMethodStats() {
+        System.out.print("\nMethod Stats\n");
+        System.out.print("Excl Cpu  Incl Cpu  Excl Real Incl Real    Calls  Method\n");
+        for (MethodData md : mSortedMethods) {
+            System.out.format("%9d %9d %9d %9d %9s  %s\n",
+                    md.getElapsedExclusiveCpuTime(), md.getElapsedInclusiveCpuTime(),
+                    md.getElapsedExclusiveRealTime(), md.getElapsedInclusiveRealTime(),
+                    md.getCalls(), md.getProfileName());
+        }
+    }
+
+    private void dumpTimeRecs(ArrayList<TimeLineView.Record> timeRecs) {
+        System.out.print("\nTime Records\n");
+        System.out.print("id  t-start    t-end  g-start    g-end  method\n");
+        for (TimeLineView.Record record : timeRecs) {
+            Call call = (Call) record.block;
+            System.out.format("%2d %8d %8d %8d %8d  %s\n",
+                    call.getThreadId(), call.mThreadStartTime, call.mThreadEndTime,
+                    call.mGlobalStartTime, call.mGlobalEndTime,
+                    call.getMethodData().getName());
+        }
+    }
+
+    @Override
+    public HashMap<Integer, String> getThreadLabels() {
+        HashMap<Integer, String> labels = new HashMap<Integer, String>();
+        for (ThreadData t : mThreadMap.values()) {
+            labels.put(t.getId(), t.getName());
+        }
+        return labels;
+    }
+
+    @Override
+    public MethodData[] getMethods() {
+        return mSortedMethods;
+    }
+
+    @Override
+    public ThreadData[] getThreads() {
+        return mSortedThreads;
+    }
+
+    @Override
+    public long getTotalCpuTime() {
+        return mTotalCpuTime;
+    }
+
+    @Override
+    public long getTotalRealTime() {
+        return mTotalRealTime;
+    }
+
+    @Override
+    public boolean haveCpuTime() {
+        return mClockSource != ClockSource.WALL;
+    }
+
+    @Override
+    public boolean haveRealTime() {
+        return mClockSource != ClockSource.THREAD_CPU;
+    }
+
+    @Override
+    public HashMap<String, String> getProperties() {
+        return mPropertiesMap;
+    }
+
+    @Override
+    public TimeBase getPreferredTimeBase() {
+        if (mClockSource == ClockSource.WALL) {
+            return TimeBase.REAL_TIME;
+        }
+        return TimeBase.CPU_TIME;
+    }
+
+    @Override
+    public String getClockSource() {
+        switch (mClockSource) {
+            case THREAD_CPU:
+                return "cpu time";
+            case WALL:
+                return "real time";
+            case DUAL:
+                return "real time, dual clock";
+        }
+        return null;
+    }
+}
diff --git a/traceview/src/main/java/com/android/traceview/MainWindow.java b/traceview/src/main/java/com/android/traceview/MainWindow.java
new file mode 100644
index 0000000..ebab72b
--- /dev/null
+++ b/traceview/src/main/java/com/android/traceview/MainWindow.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright (C) 2006 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.traceview;
+
+import com.android.sdkstats.SdkStatsService;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.window.ApplicationWindow;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.channels.FileChannel;
+import java.util.HashMap;
+import java.util.Properties;
+
+public class MainWindow extends ApplicationWindow {
+
+    private final static String PING_NAME = "Traceview";
+
+    private TraceReader mReader;
+    private String mTraceName;
+
+    // A global cache of string names.
+    public static HashMap<String, String> sStringCache = new HashMap<String, String>();
+
+    public MainWindow(String traceName, TraceReader reader) {
+        super(null);
+        mReader = reader;
+        mTraceName = traceName;
+
+        addMenuBar();
+    }
+
+    public void run() {
+        setBlockOnOpen(true);
+        open();
+    }
+
+    @Override
+    protected void configureShell(Shell shell) {
+        super.configureShell(shell);
+        shell.setText("Traceview: " + mTraceName);
+
+        InputStream in = getClass().getClassLoader().getResourceAsStream(
+                "icons/traceview-128.png");
+        if (in != null) {
+            shell.setImage(new Image(shell.getDisplay(), in));
+        }
+
+        shell.setBounds(100, 10, 1282, 900);
+    }
+
+    @Override
+    protected Control createContents(Composite parent) {
+        ColorController.assignMethodColors(parent.getDisplay(), mReader.getMethods());
+        SelectionController selectionController = new SelectionController();
+
+        GridLayout gridLayout = new GridLayout(1, false);
+        gridLayout.marginWidth = 0;
+        gridLayout.marginHeight = 0;
+        gridLayout.horizontalSpacing = 0;
+        gridLayout.verticalSpacing = 0;
+        parent.setLayout(gridLayout);
+
+        Display display = parent.getDisplay();
+        Color darkGray = display.getSystemColor(SWT.COLOR_DARK_GRAY);
+
+        // Create a sash form to separate the timeline view (on top)
+        // and the profile view (on bottom)
+        SashForm sashForm1 = new SashForm(parent, SWT.VERTICAL);
+        sashForm1.setBackground(darkGray);
+        sashForm1.SASH_WIDTH = 3;
+        GridData data = new GridData(GridData.FILL_BOTH);
+        sashForm1.setLayoutData(data);
+
+        // Create the timeline view
+        new TimeLineView(sashForm1, mReader, selectionController);
+
+        // Create the profile view
+        new ProfileView(sashForm1, mReader, selectionController);
+        return sashForm1;
+    }
+
+    @Override
+    protected MenuManager createMenuManager() {
+        MenuManager manager = super.createMenuManager();
+
+        MenuManager viewMenu = new MenuManager("View");
+        manager.add(viewMenu);
+
+        Action showPropertiesAction = new Action("Show Properties...") {
+            @Override
+            public void run() {
+                showProperties();
+            }
+        };
+        viewMenu.add(showPropertiesAction);
+
+        return manager;
+    }
+
+    private void showProperties() {
+        PropertiesDialog dialog = new PropertiesDialog(getShell());
+        dialog.setProperties(mReader.getProperties());
+        dialog.open();
+    }
+
+    /**
+     * Convert the old two-file format into the current concatenated one.
+     *
+     * @param base Base path of the two files, i.e. base.key and base.data
+     * @return Path to a temporary file that will be deleted on exit.
+     * @throws IOException
+     */
+    private static String makeTempTraceFile(String base) throws IOException {
+        // Make a temporary file that will go away on exit and prepare to
+        // write into it.
+        File temp = File.createTempFile(base, ".trace");
+        temp.deleteOnExit();
+
+        FileOutputStream dstStream = null;
+        FileInputStream keyStream = null;
+        FileInputStream dataStream = null;
+
+        try {
+            dstStream = new FileOutputStream(temp);
+            FileChannel dstChannel = dstStream.getChannel();
+
+            // First copy the contents of the key file into our temp file.
+            keyStream = new FileInputStream(base + ".key");
+            FileChannel srcChannel = keyStream.getChannel();
+            long size = dstChannel.transferFrom(srcChannel, 0, srcChannel.size());
+            srcChannel.close();
+
+            // Then concatenate the data file.
+            dataStream = new FileInputStream(base + ".data");
+            srcChannel = dataStream.getChannel();
+            dstChannel.transferFrom(srcChannel, size, srcChannel.size());
+        } finally {
+            if (dstStream != null) {
+                dstStream.close(); // also closes dstChannel
+            }
+            if (keyStream != null) {
+                keyStream.close(); // also closes srcChannel
+            }
+            if (dataStream != null) {
+                dataStream.close();
+            }
+        }
+
+        // Return the path of the temp file.
+        return temp.getPath();
+    }
+
+    /**
+     * Returns the tools revision number.
+     */
+    private static String getRevision() {
+        Properties p = new Properties();
+        try{
+            String toolsdir = System.getProperty("com.android.traceview.toolsdir"); //$NON-NLS-1$
+            File sourceProp;
+            if (toolsdir == null || toolsdir.length() == 0) {
+                sourceProp = new File("source.properties"); //$NON-NLS-1$
+            } else {
+                sourceProp = new File(toolsdir, "source.properties"); //$NON-NLS-1$
+            }
+
+            FileInputStream fis = null;
+            try {
+                fis = new FileInputStream(sourceProp);
+                p.load(fis);
+            } finally {
+                if (fis != null) {
+                    try {
+                        fis.close();
+                    } catch (IOException ignore) {
+                    }
+                }
+            }
+
+            String revision = p.getProperty("Pkg.Revision"); //$NON-NLS-1$
+            if (revision != null && revision.length() > 0) {
+                return revision;
+            }
+        } catch (FileNotFoundException e) {
+            // couldn't find the file? don't ping.
+        } catch (IOException e) {
+            // couldn't find the file? don't ping.
+        }
+
+        return null;
+    }
+
+
+    public static void main(String[] args) {
+        TraceReader reader = null;
+        boolean regression = false;
+
+        // ping the usage server
+
+        String revision = getRevision();
+        if (revision != null) {
+            new SdkStatsService().ping(PING_NAME, revision);
+        }
+
+        // Process command line arguments
+        int argc = 0;
+        int len = args.length;
+        while (argc < len) {
+            String arg = args[argc];
+            if (arg.charAt(0) != '-') {
+                break;
+            }
+            if (arg.equals("-r")) {
+                regression = true;
+            } else {
+                break;
+            }
+            argc++;
+        }
+        if (argc != len - 1) {
+            System.out.printf("Usage: java %s [-r] trace%n", MainWindow.class.getName());
+            System.out.printf("  -r   regression only%n");
+            return;
+        }
+
+        String traceName = args[len - 1];
+        File file = new File(traceName);
+        if (file.exists() && file.isDirectory()) {
+            System.out.printf("Qemu trace files not supported yet.\n");
+            System.exit(1);
+            // reader = new QtraceReader(traceName);
+        } else {
+            // If the filename as given doesn't exist...
+            if (!file.exists()) {
+                // Try appending .trace.
+                if (new File(traceName + ".trace").exists()) {
+                    traceName = traceName + ".trace";
+                // Next, see if it is the old two-file trace.
+                } else if (new File(traceName + ".data").exists()
+                    && new File(traceName + ".key").exists()) {
+                    try {
+                        traceName = makeTempTraceFile(traceName);
+                    } catch (IOException e) {
+                        System.err.printf("cannot convert old trace file '%s'\n", traceName);
+                        System.exit(1);
+                    }
+                // Otherwise, give up.
+                } else {
+                    System.err.printf("trace file '%s' not found\n", traceName);
+                    System.exit(1);
+                }
+            }
+
+            try {
+                reader = new DmTraceReader(traceName, regression);
+            } catch (IOException e) {
+                System.err.printf("Failed to read the trace file");
+                e.printStackTrace();
+                System.exit(1);
+                return;
+            }
+        }
+
+        reader.getTraceUnits().setTimeScale(TraceUnits.TimeScale.MilliSeconds);
+
+        Display.setAppName("Traceview");
+        new MainWindow(traceName, reader).run();
+    }
+}
diff --git a/traceview/src/main/java/com/android/traceview/MethodData.java b/traceview/src/main/java/com/android/traceview/MethodData.java
new file mode 100644
index 0000000..69c5247
--- /dev/null
+++ b/traceview/src/main/java/com/android/traceview/MethodData.java
@@ -0,0 +1,513 @@
+/*
+ * Copyright (C) 2006 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.traceview;
+
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Image;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+
+public class MethodData {
+
+    private int mId;
+    private int mRank = -1;
+    private String mClassName;
+    private String mMethodName;
+    private String mSignature;
+    private String mName;
+    private String mProfileName;
+    private String mPathname;
+    private int mLineNumber;
+    private long mElapsedExclusiveCpuTime;
+    private long mElapsedInclusiveCpuTime;
+    private long mTopExclusiveCpuTime;
+    private long mElapsedExclusiveRealTime;
+    private long mElapsedInclusiveRealTime;
+    private long mTopExclusiveRealTime;
+    private int[] mNumCalls = new int[2]; // index 0=normal, 1=recursive
+    private Color mColor;
+    private Color mFadedColor;
+    private Image mImage;
+    private Image mFadedImage;
+    private HashMap<Integer, ProfileData> mParents;
+    private HashMap<Integer, ProfileData> mChildren;
+
+    // The parents of this method when this method was in a recursive call
+    private HashMap<Integer, ProfileData> mRecursiveParents;
+
+    // The children of this method when this method was in a recursive call
+    private HashMap<Integer, ProfileData> mRecursiveChildren;
+
+    private ProfileNode[] mProfileNodes;
+    private int mX;
+    private int mY;
+    private double mWeight;
+
+    public MethodData(int id, String className) {
+        mId = id;
+        mClassName = className;
+        mMethodName = null;
+        mSignature = null;
+        mPathname = null;
+        mLineNumber = -1;
+        computeName();
+        computeProfileName();
+    }
+
+    public MethodData(int id, String className, String methodName,
+            String signature, String pathname, int lineNumber) {
+        mId = id;
+        mClassName = className;
+        mMethodName = methodName;
+        mSignature = signature;
+        mPathname = pathname;
+        mLineNumber = lineNumber;
+        computeName();
+        computeProfileName();
+    }
+
+    public double addWeight(int x, int y, double weight) {
+        if (mX == x && mY == y)
+            mWeight += weight;
+        else {
+            mX = x;
+            mY = y;
+            mWeight = weight;
+        }
+        return mWeight;
+    }
+
+    public void clearWeight() {
+        mWeight = 0;
+    }
+
+    public int getRank() {
+        return mRank;
+    }
+
+    public void setRank(int rank) {
+        mRank = rank;
+        computeProfileName();
+    }
+
+    public void addElapsedExclusive(long cpuTime, long realTime) {
+        mElapsedExclusiveCpuTime += cpuTime;
+        mElapsedExclusiveRealTime += realTime;
+    }
+
+    public void addElapsedInclusive(long cpuTime, long realTime,
+            boolean isRecursive, Call parent) {
+        if (isRecursive == false) {
+            mElapsedInclusiveCpuTime += cpuTime;
+            mElapsedInclusiveRealTime += realTime;
+            mNumCalls[0] += 1;
+        } else {
+            mNumCalls[1] += 1;
+        }
+
+        if (parent == null)
+            return;
+
+        // Find the child method in the parent
+        MethodData parentMethod = parent.getMethodData();
+        if (parent.isRecursive()) {
+            parentMethod.mRecursiveChildren = updateInclusive(cpuTime, realTime,
+                    parentMethod, this, false,
+                    parentMethod.mRecursiveChildren);
+        } else {
+            parentMethod.mChildren = updateInclusive(cpuTime, realTime,
+                    parentMethod, this, false, parentMethod.mChildren);
+        }
+
+        // Find the parent method in the child
+        if (isRecursive) {
+            mRecursiveParents = updateInclusive(cpuTime, realTime, this, parentMethod, true,
+                    mRecursiveParents);
+        } else {
+            mParents = updateInclusive(cpuTime, realTime, this, parentMethod, true,
+                    mParents);
+        }
+    }
+
+    private HashMap<Integer, ProfileData> updateInclusive(long cpuTime, long realTime,
+            MethodData contextMethod, MethodData elementMethod,
+            boolean elementIsParent, HashMap<Integer, ProfileData> map) {
+        if (map == null) {
+            map = new HashMap<Integer, ProfileData>(4);
+        } else {
+            ProfileData profileData = map.get(elementMethod.mId);
+            if (profileData != null) {
+                profileData.addElapsedInclusive(cpuTime, realTime);
+                return map;
+            }
+        }
+
+        ProfileData elementData = new ProfileData(contextMethod,
+                elementMethod, elementIsParent);
+        elementData.setElapsedInclusive(cpuTime, realTime);
+        elementData.setNumCalls(1);
+        map.put(elementMethod.mId, elementData);
+        return map;
+    }
+
+    public void analyzeData(TimeBase timeBase) {
+        // Sort the parents and children into decreasing inclusive time
+        ProfileData[] sortedParents;
+        ProfileData[] sortedChildren;
+        ProfileData[] sortedRecursiveParents;
+        ProfileData[] sortedRecursiveChildren;
+
+        sortedParents = sortProfileData(mParents, timeBase);
+        sortedChildren = sortProfileData(mChildren, timeBase);
+        sortedRecursiveParents = sortProfileData(mRecursiveParents, timeBase);
+        sortedRecursiveChildren = sortProfileData(mRecursiveChildren, timeBase);
+
+        // Add "self" time to the top of the sorted children
+        sortedChildren = addSelf(sortedChildren);
+
+        // Create the ProfileNode objects that we need
+        ArrayList<ProfileNode> nodes = new ArrayList<ProfileNode>();
+        ProfileNode profileNode;
+        if (mParents != null) {
+            profileNode = new ProfileNode("Parents", this, sortedParents,
+                    true, false);
+            nodes.add(profileNode);
+        }
+        if (mChildren != null) {
+            profileNode = new ProfileNode("Children", this, sortedChildren,
+                    false, false);
+            nodes.add(profileNode);
+        }
+        if (mRecursiveParents!= null) {
+            profileNode = new ProfileNode("Parents while recursive", this,
+                    sortedRecursiveParents, true, true);
+            nodes.add(profileNode);
+        }
+        if (mRecursiveChildren != null) {
+            profileNode = new ProfileNode("Children while recursive", this,
+                    sortedRecursiveChildren, false, true);
+            nodes.add(profileNode);
+        }
+        mProfileNodes = nodes.toArray(new ProfileNode[nodes.size()]);
+    }
+
+    // Create and return a ProfileData[] array that is a sorted copy
+    // of the given HashMap values.
+    private ProfileData[] sortProfileData(HashMap<Integer, ProfileData> map,
+            final TimeBase timeBase) {
+        if (map == null)
+            return null;
+
+        // Convert the hash values to an array of ProfileData
+        Collection<ProfileData> values = map.values();
+        ProfileData[] sorted = values.toArray(new ProfileData[values.size()]);
+
+        // Sort the array by elapsed inclusive time
+        Arrays.sort(sorted, new Comparator<ProfileData>() {
+            @Override
+            public int compare(ProfileData pd1, ProfileData pd2) {
+                if (timeBase.getElapsedInclusiveTime(pd2) > timeBase.getElapsedInclusiveTime(pd1))
+                    return 1;
+                if (timeBase.getElapsedInclusiveTime(pd2) < timeBase.getElapsedInclusiveTime(pd1))
+                    return -1;
+                return 0;
+            }
+        });
+        return sorted;
+    }
+
+    private ProfileData[] addSelf(ProfileData[] children) {
+        ProfileData[] pdata;
+        if (children == null) {
+            pdata = new ProfileData[1];
+        } else {
+            pdata = new ProfileData[children.length + 1];
+            System.arraycopy(children, 0, pdata, 1, children.length);
+        }
+        pdata[0] = new ProfileSelf(this);
+        return pdata;
+    }
+
+    public void addTopExclusive(long cpuTime, long realTime) {
+        mTopExclusiveCpuTime += cpuTime;
+        mTopExclusiveRealTime += realTime;
+    }
+
+    public long getTopExclusiveCpuTime() {
+        return mTopExclusiveCpuTime;
+    }
+
+    public long getTopExclusiveRealTime() {
+        return mTopExclusiveRealTime;
+    }
+
+    public int getId() {
+        return mId;
+    }
+
+    private void computeName() {
+        if (mMethodName == null) {
+            mName = mClassName;
+            return;
+        }
+
+        StringBuilder sb = new StringBuilder();
+        sb.append(mClassName);
+        sb.append(".");  //$NON-NLS-1$
+        sb.append(mMethodName);
+        sb.append(" ");  //$NON-NLS-1$
+        sb.append(mSignature);
+        mName = sb.toString();
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public String getClassName() {
+        return mClassName;
+    }
+
+    public String getMethodName() {
+        return mMethodName;
+    }
+
+    public String getProfileName() {
+        return mProfileName;
+    }
+
+    public String getSignature() {
+        return mSignature;
+    }
+
+    public void computeProfileName() {
+        if (mRank == -1) {
+            mProfileName = mName;
+            return;
+        }
+
+        StringBuilder sb = new StringBuilder();
+        sb.append(mRank);
+        sb.append(" ");  //$NON-NLS-1$
+        sb.append(getName());
+        mProfileName = sb.toString();
+    }
+
+    public String getCalls() {
+        return String.format("%d+%d", mNumCalls[0], mNumCalls[1]);
+    }
+
+    public int getTotalCalls() {
+        return mNumCalls[0] + mNumCalls[1];
+    }
+
+    public Color getColor() {
+        return mColor;
+    }
+
+    public void setColor(Color color) {
+        mColor = color;
+    }
+
+    public void setImage(Image image) {
+        mImage = image;
+    }
+
+    public Image getImage() {
+        return mImage;
+    }
+
+    @Override
+    public String toString() {
+        return getName();
+    }
+
+    public long getElapsedExclusiveCpuTime() {
+        return mElapsedExclusiveCpuTime;
+    }
+
+    public long getElapsedExclusiveRealTime() {
+        return mElapsedExclusiveRealTime;
+    }
+
+    public long getElapsedInclusiveCpuTime() {
+        return mElapsedInclusiveCpuTime;
+    }
+
+    public long getElapsedInclusiveRealTime() {
+        return mElapsedInclusiveRealTime;
+    }
+
+    public void setFadedColor(Color fadedColor) {
+        mFadedColor = fadedColor;
+    }
+
+    public Color getFadedColor() {
+        return mFadedColor;
+    }
+
+    public void setFadedImage(Image fadedImage) {
+        mFadedImage = fadedImage;
+    }
+
+    public Image getFadedImage() {
+        return mFadedImage;
+    }
+
+    public void setPathname(String pathname) {
+        mPathname = pathname;
+    }
+
+    public String getPathname() {
+        return mPathname;
+    }
+
+    public void setLineNumber(int lineNumber) {
+        mLineNumber = lineNumber;
+    }
+
+    public int getLineNumber() {
+        return mLineNumber;
+    }
+
+    public ProfileNode[] getProfileNodes() {
+        return mProfileNodes;
+    }
+
+    public static class Sorter implements Comparator<MethodData> {
+        @Override
+        public int compare(MethodData md1, MethodData md2) {
+            if (mColumn == Column.BY_NAME) {
+                int result = md1.getName().compareTo(md2.getName());
+                return (mDirection == Direction.INCREASING) ? result : -result;
+            }
+            if (mColumn == Column.BY_INCLUSIVE_CPU_TIME) {
+                if (md2.getElapsedInclusiveCpuTime() > md1.getElapsedInclusiveCpuTime())
+                    return (mDirection == Direction.INCREASING) ? -1 : 1;
+                if (md2.getElapsedInclusiveCpuTime() < md1.getElapsedInclusiveCpuTime())
+                    return (mDirection == Direction.INCREASING) ? 1 : -1;
+                return md1.getName().compareTo(md2.getName());
+            }
+            if (mColumn == Column.BY_EXCLUSIVE_CPU_TIME) {
+                if (md2.getElapsedExclusiveCpuTime() > md1.getElapsedExclusiveCpuTime())
+                    return (mDirection == Direction.INCREASING) ? -1 : 1;
+                if (md2.getElapsedExclusiveCpuTime() < md1.getElapsedExclusiveCpuTime())
+                    return (mDirection == Direction.INCREASING) ? 1 : -1;
+                return md1.getName().compareTo(md2.getName());
+            }
+            if (mColumn == Column.BY_INCLUSIVE_REAL_TIME) {
+                if (md2.getElapsedInclusiveRealTime() > md1.getElapsedInclusiveRealTime())
+                    return (mDirection == Direction.INCREASING) ? -1 : 1;
+                if (md2.getElapsedInclusiveRealTime() < md1.getElapsedInclusiveRealTime())
+                    return (mDirection == Direction.INCREASING) ? 1 : -1;
+                return md1.getName().compareTo(md2.getName());
+            }
+            if (mColumn == Column.BY_EXCLUSIVE_REAL_TIME) {
+                if (md2.getElapsedExclusiveRealTime() > md1.getElapsedExclusiveRealTime())
+                    return (mDirection == Direction.INCREASING) ? -1 : 1;
+                if (md2.getElapsedExclusiveRealTime() < md1.getElapsedExclusiveRealTime())
+                    return (mDirection == Direction.INCREASING) ? 1 : -1;
+                return md1.getName().compareTo(md2.getName());
+            }
+            if (mColumn == Column.BY_CALLS) {
+                int result = md1.getTotalCalls() - md2.getTotalCalls();
+                if (result == 0)
+                    return md1.getName().compareTo(md2.getName());
+                return (mDirection == Direction.INCREASING) ? result : -result;
+            }
+            if (mColumn == Column.BY_CPU_TIME_PER_CALL) {
+                double time1 = md1.getElapsedInclusiveCpuTime();
+                time1 = time1 / md1.getTotalCalls();
+                double time2 = md2.getElapsedInclusiveCpuTime();
+                time2 = time2 / md2.getTotalCalls();
+                double diff = time1 - time2;
+                int result = 0;
+                if (diff < 0)
+                    result = -1;
+                else if (diff > 0)
+                    result = 1;
+                if (result == 0)
+                    return md1.getName().compareTo(md2.getName());
+                return (mDirection == Direction.INCREASING) ? result : -result;
+            }
+            if (mColumn == Column.BY_REAL_TIME_PER_CALL) {
+                double time1 = md1.getElapsedInclusiveRealTime();
+                time1 = time1 / md1.getTotalCalls();
+                double time2 = md2.getElapsedInclusiveRealTime();
+                time2 = time2 / md2.getTotalCalls();
+                double diff = time1 - time2;
+                int result = 0;
+                if (diff < 0)
+                    result = -1;
+                else if (diff > 0)
+                    result = 1;
+                if (result == 0)
+                    return md1.getName().compareTo(md2.getName());
+                return (mDirection == Direction.INCREASING) ? result : -result;
+            }
+            return 0;
+        }
+
+        public void setColumn(Column column) {
+            // If the sort column specified is the same as last time,
+            // then reverse the sort order.
+            if (mColumn == column) {
+                // Reverse the sort order
+                if (mDirection == Direction.INCREASING)
+                    mDirection = Direction.DECREASING;
+                else
+                    mDirection = Direction.INCREASING;
+            } else {
+                // Sort names into increasing order, data into decreasing order.
+                if (column == Column.BY_NAME)
+                    mDirection = Direction.INCREASING;
+                else
+                    mDirection = Direction.DECREASING;
+            }
+            mColumn = column;
+        }
+
+        public Column getColumn() {
+            return mColumn;
+        }
+
+        public void setDirection(Direction direction) {
+            mDirection = direction;
+        }
+
+        public Direction getDirection() {
+            return mDirection;
+        }
+
+        public static enum Column {
+            BY_NAME, BY_EXCLUSIVE_CPU_TIME, BY_EXCLUSIVE_REAL_TIME,
+            BY_INCLUSIVE_CPU_TIME, BY_INCLUSIVE_REAL_TIME, BY_CALLS,
+            BY_REAL_TIME_PER_CALL, BY_CPU_TIME_PER_CALL,
+        };
+
+        public static enum Direction {
+            INCREASING, DECREASING
+        };
+
+        private Column mColumn;
+        private Direction mDirection;
+    }
+}
diff --git a/traceview/src/main/java/com/android/traceview/ProfileData.java b/traceview/src/main/java/com/android/traceview/ProfileData.java
new file mode 100644
index 0000000..e3c47fb
--- /dev/null
+++ b/traceview/src/main/java/com/android/traceview/ProfileData.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2006 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.traceview;
+
+
+public class ProfileData {
+
+    protected MethodData mElement;
+    
+    /** mContext is either the parent or child of mElement */
+    protected MethodData mContext;
+    protected boolean mElementIsParent;
+    protected long mElapsedInclusiveCpuTime;
+    protected long mElapsedInclusiveRealTime;
+    protected int mNumCalls;
+
+    public ProfileData() {
+    }
+
+    public ProfileData(MethodData context, MethodData element,
+            boolean elementIsParent) {
+        mContext = context;
+        mElement = element;
+        mElementIsParent = elementIsParent;
+    }
+
+    public String getProfileName() {
+        return mElement.getProfileName();
+    }
+
+    public MethodData getMethodData() {
+        return mElement;
+    }
+
+    public void addElapsedInclusive(long cpuTime, long realTime) {
+        mElapsedInclusiveCpuTime += cpuTime;
+        mElapsedInclusiveRealTime += realTime;
+        mNumCalls += 1;
+    }
+
+    public void setElapsedInclusive(long cpuTime, long realTime) {
+        mElapsedInclusiveCpuTime = cpuTime;
+        mElapsedInclusiveRealTime = realTime;
+    }
+
+    public long getElapsedInclusiveCpuTime() {
+        return mElapsedInclusiveCpuTime;
+    }
+
+    public long getElapsedInclusiveRealTime() {
+        return mElapsedInclusiveRealTime;
+    }
+
+    public void setNumCalls(int numCalls) {
+        mNumCalls = numCalls;
+    }
+
+    public String getNumCalls() {
+        int totalCalls;
+        if (mElementIsParent)
+            totalCalls = mContext.getTotalCalls();
+        else
+            totalCalls = mElement.getTotalCalls();
+        return String.format("%d/%d", mNumCalls, totalCalls);
+    }
+
+    public boolean isParent() {
+        return mElementIsParent;
+    }
+
+    public MethodData getContext() {
+        return mContext;
+    }
+}
diff --git a/traceview/src/main/java/com/android/traceview/ProfileNode.java b/traceview/src/main/java/com/android/traceview/ProfileNode.java
new file mode 100644
index 0000000..7cb0b5d
--- /dev/null
+++ b/traceview/src/main/java/com/android/traceview/ProfileNode.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2006 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.traceview;
+
+public class ProfileNode {
+
+    private String mLabel;
+    private MethodData mMethodData;
+    private ProfileData[] mChildren;
+    private boolean mIsParent;
+    private boolean mIsRecursive;
+
+    public ProfileNode(String label, MethodData methodData,
+            ProfileData[] children, boolean isParent, boolean isRecursive) {
+        mLabel = label;
+        mMethodData = methodData;
+        mChildren = children;
+        mIsParent = isParent;
+        mIsRecursive = isRecursive;
+    }
+
+    public String getLabel() {
+        return mLabel;
+    }
+    
+    public ProfileData[] getChildren() {
+        return mChildren;
+    }
+
+    public boolean isParent() {
+        return mIsParent;
+    }
+
+    public boolean isRecursive() {
+        return mIsRecursive;
+    }
+}
diff --git a/traceview/src/main/java/com/android/traceview/ProfileProvider.java b/traceview/src/main/java/com/android/traceview/ProfileProvider.java
new file mode 100644
index 0000000..995e606
--- /dev/null
+++ b/traceview/src/main/java/com/android/traceview/ProfileProvider.java
@@ -0,0 +1,467 @@
+/*
+ * Copyright (C) 2006 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.traceview;
+
+import com.android.utils.SdkUtils;
+
+import org.eclipse.jface.viewers.IColorProvider;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeColumn;
+import org.eclipse.swt.widgets.TreeItem;
+
+import java.io.InputStream;
+import java.util.Arrays;
+
+class ProfileProvider implements ITreeContentProvider {
+
+    private MethodData[] mRoots;
+    private SelectionAdapter mListener;
+    private TreeViewer mTreeViewer;
+    private TraceReader mReader;
+    private Image mSortUp;
+    private Image mSortDown;
+    private String mColumnNames[] = { "Name",
+            "Incl Cpu Time %", "Incl Cpu Time", "Excl Cpu Time %", "Excl Cpu Time",
+            "Incl Real Time %", "Incl Real Time", "Excl Real Time %", "Excl Real Time",
+            "Calls+Recur\nCalls/Total", "Cpu Time/Call", "Real Time/Call" };
+    private int mColumnWidths[] = { 370,
+            100, 100, 100, 100,
+            100, 100, 100, 100,
+            100, 100, 100 };
+    private int mColumnAlignments[] = { SWT.LEFT,
+            SWT.RIGHT, SWT.RIGHT, SWT.RIGHT, SWT.RIGHT,
+            SWT.RIGHT, SWT.RIGHT, SWT.RIGHT, SWT.RIGHT,
+            SWT.CENTER, SWT.RIGHT, SWT.RIGHT };
+    private static final int COL_NAME = 0;
+    private static final int COL_INCLUSIVE_CPU_TIME_PER = 1;
+    private static final int COL_INCLUSIVE_CPU_TIME = 2;
+    private static final int COL_EXCLUSIVE_CPU_TIME_PER = 3;
+    private static final int COL_EXCLUSIVE_CPU_TIME = 4;
+    private static final int COL_INCLUSIVE_REAL_TIME_PER = 5;
+    private static final int COL_INCLUSIVE_REAL_TIME = 6;
+    private static final int COL_EXCLUSIVE_REAL_TIME_PER = 7;
+    private static final int COL_EXCLUSIVE_REAL_TIME = 8;
+    private static final int COL_CALLS = 9;
+    private static final int COL_CPU_TIME_PER_CALL = 10;
+    private static final int COL_REAL_TIME_PER_CALL = 11;
+    private long mTotalCpuTime;
+    private long mTotalRealTime;
+    private int mPrevMatchIndex = -1;
+
+    public ProfileProvider(TraceReader reader) {
+        mRoots = reader.getMethods();
+        mReader = reader;
+        mTotalCpuTime = reader.getTotalCpuTime();
+        mTotalRealTime = reader.getTotalRealTime();
+        Display display = Display.getCurrent();
+        InputStream in = getClass().getClassLoader().getResourceAsStream(
+                "icons/sort_up.png");
+        mSortUp = new Image(display, in);
+        in = getClass().getClassLoader().getResourceAsStream(
+                "icons/sort_down.png");
+        mSortDown = new Image(display, in);
+    }
+
+    private MethodData doMatchName(String name, int startIndex) {
+        // Check if the given "name" has any uppercase letters
+        boolean hasUpper = SdkUtils.hasUpperCaseCharacter(name);
+        for (int ii = startIndex; ii < mRoots.length; ++ii) {
+            MethodData md = mRoots[ii];
+            String fullName = md.getName();
+            // If there were no upper case letters in the given name,
+            // then ignore case when matching.
+            if (!hasUpper)
+                fullName = fullName.toLowerCase();
+            if (fullName.indexOf(name) != -1) {
+                mPrevMatchIndex = ii;
+                return md;
+            }
+        }
+        mPrevMatchIndex = -1;
+        return null;
+    }
+
+    public MethodData findMatchingName(String name) {
+        return doMatchName(name, 0);
+    }
+
+    public MethodData findNextMatchingName(String name) {
+        return doMatchName(name, mPrevMatchIndex + 1);
+    }
+
+    public MethodData findMatchingTreeItem(TreeItem item) {
+        if (item == null)
+            return null;
+        String text = item.getText();
+        if (Character.isDigit(text.charAt(0)) == false)
+            return null;
+        int spaceIndex = text.indexOf(' ');
+        String numstr = text.substring(0, spaceIndex);
+        int rank = Integer.valueOf(numstr);
+        for (MethodData md : mRoots) {
+            if (md.getRank() == rank)
+                return md;
+        }
+        return null;
+    }
+
+    public void setTreeViewer(TreeViewer treeViewer) {
+        mTreeViewer = treeViewer;
+    }
+
+    public String[] getColumnNames() {
+        return mColumnNames;
+    }
+
+    public int[] getColumnWidths() {
+        int[] widths = Arrays.copyOf(mColumnWidths, mColumnWidths.length);
+        if (!mReader.haveCpuTime()) {
+            widths[COL_EXCLUSIVE_CPU_TIME] = 0;
+            widths[COL_EXCLUSIVE_CPU_TIME_PER] = 0;
+            widths[COL_INCLUSIVE_CPU_TIME] = 0;
+            widths[COL_INCLUSIVE_CPU_TIME_PER] = 0;
+            widths[COL_CPU_TIME_PER_CALL] = 0;
+        }
+        if (!mReader.haveRealTime()) {
+            widths[COL_EXCLUSIVE_REAL_TIME] = 0;
+            widths[COL_EXCLUSIVE_REAL_TIME_PER] = 0;
+            widths[COL_INCLUSIVE_REAL_TIME] = 0;
+            widths[COL_INCLUSIVE_REAL_TIME_PER] = 0;
+            widths[COL_REAL_TIME_PER_CALL] = 0;
+        }
+        return widths;
+    }
+
+    public int[] getColumnAlignments() {
+        return mColumnAlignments;
+    }
+
+    @Override
+    public Object[] getChildren(Object element) {
+        if (element instanceof MethodData) {
+            MethodData md = (MethodData) element;
+            return md.getProfileNodes();
+        }
+        if (element instanceof ProfileNode) {
+            ProfileNode pn = (ProfileNode) element;
+            return pn.getChildren();
+        }
+        return new Object[0];
+    }
+
+    @Override
+    public Object getParent(Object element) {
+        return null;
+    }
+
+    @Override
+    public boolean hasChildren(Object element) {
+        if (element instanceof MethodData)
+            return true;
+        if (element instanceof ProfileNode)
+            return true;
+        return false;
+    }
+
+    @Override
+    public Object[] getElements(Object element) {
+        return mRoots;
+    }
+
+    @Override
+    public void dispose() {
+    }
+
+    @Override
+    public void inputChanged(Viewer arg0, Object arg1, Object arg2) {
+    }
+
+    public Object getRoot() {
+        return "root";
+    }
+
+    public SelectionAdapter getColumnListener() {
+        if (mListener == null)
+            mListener = new ColumnListener();
+        return mListener;
+    }
+
+    public LabelProvider getLabelProvider() {
+        return new ProfileLabelProvider();
+    }
+
+    class ProfileLabelProvider extends LabelProvider implements
+            ITableLabelProvider, IColorProvider {
+        Color colorRed;
+        Color colorParentsBack;
+        Color colorChildrenBack;
+        TraceUnits traceUnits;
+
+        public ProfileLabelProvider() {
+            Display display = Display.getCurrent();
+            colorRed = display.getSystemColor(SWT.COLOR_RED);
+            colorParentsBack = new Color(display, 230, 230, 255); // blue
+            colorChildrenBack = new Color(display, 255, 255, 210); // yellow
+            traceUnits = mReader.getTraceUnits();
+        }
+
+        @Override
+        public String getColumnText(Object element, int col) {
+            if (element instanceof MethodData) {
+                MethodData md = (MethodData) element;
+                if (col == COL_NAME)
+                    return md.getProfileName();
+                if (col == COL_EXCLUSIVE_CPU_TIME) {
+                    double val = md.getElapsedExclusiveCpuTime();
+                    val = traceUnits.getScaledValue(val);
+                    return String.format("%.3f", val);
+                }
+                if (col == COL_EXCLUSIVE_CPU_TIME_PER) {
+                    double val = md.getElapsedExclusiveCpuTime();
+                    double per = val * 100.0 / mTotalCpuTime;
+                    return String.format("%.1f%%", per);
+                }
+                if (col == COL_INCLUSIVE_CPU_TIME) {
+                    double val = md.getElapsedInclusiveCpuTime();
+                    val = traceUnits.getScaledValue(val);
+                    return String.format("%.3f", val);
+                }
+                if (col == COL_INCLUSIVE_CPU_TIME_PER) {
+                    double val = md.getElapsedInclusiveCpuTime();
+                    double per = val * 100.0 / mTotalCpuTime;
+                    return String.format("%.1f%%", per);
+                }
+                if (col == COL_EXCLUSIVE_REAL_TIME) {
+                    double val = md.getElapsedExclusiveRealTime();
+                    val = traceUnits.getScaledValue(val);
+                    return String.format("%.3f", val);
+                }
+                if (col == COL_EXCLUSIVE_REAL_TIME_PER) {
+                    double val = md.getElapsedExclusiveRealTime();
+                    double per = val * 100.0 / mTotalRealTime;
+                    return String.format("%.1f%%", per);
+                }
+                if (col == COL_INCLUSIVE_REAL_TIME) {
+                    double val = md.getElapsedInclusiveRealTime();
+                    val = traceUnits.getScaledValue(val);
+                    return String.format("%.3f", val);
+                }
+                if (col == COL_INCLUSIVE_REAL_TIME_PER) {
+                    double val = md.getElapsedInclusiveRealTime();
+                    double per = val * 100.0 / mTotalRealTime;
+                    return String.format("%.1f%%", per);
+                }
+                if (col == COL_CALLS)
+                    return md.getCalls();
+                if (col == COL_CPU_TIME_PER_CALL) {
+                    int numCalls = md.getTotalCalls();
+                    double val = md.getElapsedInclusiveCpuTime();
+                    val = val / numCalls;
+                    val = traceUnits.getScaledValue(val);
+                    return String.format("%.3f", val);
+                }
+                if (col == COL_REAL_TIME_PER_CALL) {
+                    int numCalls = md.getTotalCalls();
+                    double val = md.getElapsedInclusiveRealTime();
+                    val = val / numCalls;
+                    val = traceUnits.getScaledValue(val);
+                    return String.format("%.3f", val);
+                }
+            } else if (element instanceof ProfileSelf) {
+                ProfileSelf ps = (ProfileSelf) element;
+                if (col == COL_NAME)
+                    return ps.getProfileName();
+                if (col == COL_INCLUSIVE_CPU_TIME) {
+                    double val = ps.getElapsedInclusiveCpuTime();
+                    val = traceUnits.getScaledValue(val);
+                    return String.format("%.3f", val);
+                }
+                if (col == COL_INCLUSIVE_CPU_TIME_PER) {
+                    double total;
+                    double val = ps.getElapsedInclusiveCpuTime();
+                    MethodData context = ps.getContext();
+                    total = context.getElapsedInclusiveCpuTime();
+                    double per = val * 100.0 / total;
+                    return String.format("%.1f%%", per);
+                }
+                if (col == COL_INCLUSIVE_REAL_TIME) {
+                    double val = ps.getElapsedInclusiveRealTime();
+                    val = traceUnits.getScaledValue(val);
+                    return String.format("%.3f", val);
+                }
+                if (col == COL_INCLUSIVE_REAL_TIME_PER) {
+                    double total;
+                    double val = ps.getElapsedInclusiveRealTime();
+                    MethodData context = ps.getContext();
+                    total = context.getElapsedInclusiveRealTime();
+                    double per = val * 100.0 / total;
+                    return String.format("%.1f%%", per);
+                }
+                return "";
+            } else if (element instanceof ProfileData) {
+                ProfileData pd = (ProfileData) element;
+                if (col == COL_NAME)
+                    return pd.getProfileName();
+                if (col == COL_INCLUSIVE_CPU_TIME) {
+                    double val = pd.getElapsedInclusiveCpuTime();
+                    val = traceUnits.getScaledValue(val);
+                    return String.format("%.3f", val);
+                }
+                if (col == COL_INCLUSIVE_CPU_TIME_PER) {
+                    double total;
+                    double val = pd.getElapsedInclusiveCpuTime();
+                    MethodData context = pd.getContext();
+                    total = context.getElapsedInclusiveCpuTime();
+                    double per = val * 100.0 / total;
+                    return String.format("%.1f%%", per);
+                }
+                if (col == COL_INCLUSIVE_REAL_TIME) {
+                    double val = pd.getElapsedInclusiveRealTime();
+                    val = traceUnits.getScaledValue(val);
+                    return String.format("%.3f", val);
+                }
+                if (col == COL_INCLUSIVE_REAL_TIME_PER) {
+                    double total;
+                    double val = pd.getElapsedInclusiveRealTime();
+                    MethodData context = pd.getContext();
+                    total = context.getElapsedInclusiveRealTime();
+                    double per = val * 100.0 / total;
+                    return String.format("%.1f%%", per);
+                }
+                if (col == COL_CALLS)
+                    return pd.getNumCalls();
+                return "";
+            } else if (element instanceof ProfileNode) {
+                ProfileNode pn = (ProfileNode) element;
+                if (col == COL_NAME)
+                    return pn.getLabel();
+                return "";
+            }
+            return "col" + col;
+        }
+
+        @Override
+        public Image getColumnImage(Object element, int col) {
+            if (col != COL_NAME)
+                return null;
+            if (element instanceof MethodData) {
+                MethodData md = (MethodData) element;
+                return md.getImage();
+            }
+            if (element instanceof ProfileData) {
+                ProfileData pd = (ProfileData) element;
+                MethodData md = pd.getMethodData();
+                return md.getImage();
+            }
+            return null;
+        }
+
+        @Override
+        public Color getForeground(Object element) {
+            return null;
+        }
+
+        @Override
+        public Color getBackground(Object element) {
+            if (element instanceof ProfileData) {
+                ProfileData pd = (ProfileData) element;
+                if (pd.isParent())
+                    return colorParentsBack;
+                return colorChildrenBack;
+            }
+            if (element instanceof ProfileNode) {
+                ProfileNode pn = (ProfileNode) element;
+                if (pn.isParent())
+                    return colorParentsBack;
+                return colorChildrenBack;
+            }
+            return null;
+        }
+    }
+
+    class ColumnListener extends SelectionAdapter {
+        MethodData.Sorter sorter = new MethodData.Sorter();
+
+        @Override
+        public void widgetSelected(SelectionEvent event) {
+            TreeColumn column = (TreeColumn) event.widget;
+            String name = column.getText();
+            Tree tree = column.getParent();
+            tree.setRedraw(false);
+            TreeColumn[] columns = tree.getColumns();
+            for (TreeColumn col : columns) {
+                col.setImage(null);
+            }
+            if (name == mColumnNames[COL_NAME]) {
+                // Sort names alphabetically
+                sorter.setColumn(MethodData.Sorter.Column.BY_NAME);
+                Arrays.sort(mRoots, sorter);
+            } else if (name == mColumnNames[COL_EXCLUSIVE_CPU_TIME]) {
+                sorter.setColumn(MethodData.Sorter.Column.BY_EXCLUSIVE_CPU_TIME);
+                Arrays.sort(mRoots, sorter);
+            } else if (name == mColumnNames[COL_EXCLUSIVE_CPU_TIME_PER]) {
+                sorter.setColumn(MethodData.Sorter.Column.BY_EXCLUSIVE_CPU_TIME);
+                Arrays.sort(mRoots, sorter);
+            } else if (name == mColumnNames[COL_INCLUSIVE_CPU_TIME]) {
+                sorter.setColumn(MethodData.Sorter.Column.BY_INCLUSIVE_CPU_TIME);
+                Arrays.sort(mRoots, sorter);
+            } else if (name == mColumnNames[COL_INCLUSIVE_CPU_TIME_PER]) {
+                sorter.setColumn(MethodData.Sorter.Column.BY_INCLUSIVE_CPU_TIME);
+                Arrays.sort(mRoots, sorter);
+            } else if (name == mColumnNames[COL_EXCLUSIVE_REAL_TIME]) {
+                sorter.setColumn(MethodData.Sorter.Column.BY_EXCLUSIVE_REAL_TIME);
+                Arrays.sort(mRoots, sorter);
+            } else if (name == mColumnNames[COL_EXCLUSIVE_REAL_TIME_PER]) {
+                sorter.setColumn(MethodData.Sorter.Column.BY_EXCLUSIVE_REAL_TIME);
+                Arrays.sort(mRoots, sorter);
+            } else if (name == mColumnNames[COL_INCLUSIVE_REAL_TIME]) {
+                sorter.setColumn(MethodData.Sorter.Column.BY_INCLUSIVE_REAL_TIME);
+                Arrays.sort(mRoots, sorter);
+            } else if (name == mColumnNames[COL_INCLUSIVE_REAL_TIME_PER]) {
+                sorter.setColumn(MethodData.Sorter.Column.BY_INCLUSIVE_REAL_TIME);
+                Arrays.sort(mRoots, sorter);
+            } else if (name == mColumnNames[COL_CALLS]) {
+                sorter.setColumn(MethodData.Sorter.Column.BY_CALLS);
+                Arrays.sort(mRoots, sorter);
+            } else if (name == mColumnNames[COL_CPU_TIME_PER_CALL]) {
+                sorter.setColumn(MethodData.Sorter.Column.BY_CPU_TIME_PER_CALL);
+                Arrays.sort(mRoots, sorter);
+            } else if (name == mColumnNames[COL_REAL_TIME_PER_CALL]) {
+                sorter.setColumn(MethodData.Sorter.Column.BY_REAL_TIME_PER_CALL);
+                Arrays.sort(mRoots, sorter);
+            }
+            MethodData.Sorter.Direction direction = sorter.getDirection();
+            if (direction == MethodData.Sorter.Direction.INCREASING)
+                column.setImage(mSortDown);
+            else
+                column.setImage(mSortUp);
+            tree.setRedraw(true);
+            mTreeViewer.refresh();
+        }
+    }
+}
diff --git a/traceview/src/main/java/com/android/traceview/ProfileSelf.java b/traceview/src/main/java/com/android/traceview/ProfileSelf.java
new file mode 100644
index 0000000..45543b2
--- /dev/null
+++ b/traceview/src/main/java/com/android/traceview/ProfileSelf.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2006 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.traceview;
+
+public class ProfileSelf extends ProfileData {
+    public ProfileSelf(MethodData methodData) {
+        mElement = methodData;
+        mContext = methodData;
+    }
+
+    @Override
+    public String getProfileName() {
+        return "self";
+    }
+
+    @Override
+    public long getElapsedInclusiveCpuTime() {
+        return mElement.getTopExclusiveCpuTime();
+    }
+
+    @Override
+    public long getElapsedInclusiveRealTime() {
+        return mElement.getTopExclusiveRealTime();
+    }
+}
diff --git a/traceview/src/main/java/com/android/traceview/ProfileView.java b/traceview/src/main/java/com/android/traceview/ProfileView.java
new file mode 100644
index 0000000..683a2c7
--- /dev/null
+++ b/traceview/src/main/java/com/android/traceview/ProfileView.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) 2006 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.traceview;
+
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.ITreeViewerListener;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.viewers.TreeExpansionEvent;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.KeyAdapter;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeColumn;
+import org.eclipse.swt.widgets.TreeItem;
+
+import java.util.ArrayList;
+import java.util.Observable;
+import java.util.Observer;
+
+public class ProfileView extends Composite implements Observer {
+
+    private TreeViewer mTreeViewer;
+    private Text mSearchBox;
+    private SelectionController mSelectionController;
+    private ProfileProvider mProfileProvider;
+    private Color mColorNoMatch;
+    private Color mColorMatch;
+    private MethodData mCurrentHighlightedMethod;
+    private MethodHandler mMethodHandler;
+
+    public interface MethodHandler {
+        void handleMethod(MethodData method);
+    }
+
+    public ProfileView(Composite parent, TraceReader reader,
+            SelectionController selectController) {
+        super(parent, SWT.NONE);
+        setLayout(new GridLayout(1, false));
+        this.mSelectionController = selectController;
+        mSelectionController.addObserver(this);
+
+        // Add a tree viewer at the top
+        mTreeViewer = new TreeViewer(this, SWT.MULTI | SWT.NONE);
+        mTreeViewer.setUseHashlookup(true);
+        mProfileProvider = reader.getProfileProvider();
+        mProfileProvider.setTreeViewer(mTreeViewer);
+        SelectionAdapter listener = mProfileProvider.getColumnListener();
+        final Tree tree = mTreeViewer.getTree();
+        tree.setHeaderVisible(true);
+        tree.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        // Get the column names from the ProfileProvider
+        String[] columnNames = mProfileProvider.getColumnNames();
+        int[] columnWidths = mProfileProvider.getColumnWidths();
+        int[] columnAlignments = mProfileProvider.getColumnAlignments();
+        for (int ii = 0; ii < columnWidths.length; ++ii) {
+            TreeColumn column = new TreeColumn(tree, SWT.LEFT);
+            column.setText(columnNames[ii]);
+            column.setWidth(columnWidths[ii]);
+            column.setMoveable(true);
+            column.addSelectionListener(listener);
+            column.setAlignment(columnAlignments[ii]);
+        }
+
+        // Add a listener to the tree so that we can make the row
+        // height smaller.
+        tree.addListener(SWT.MeasureItem, new Listener() {
+            @Override
+            public void handleEvent(Event event) {
+                int fontHeight = event.gc.getFontMetrics().getHeight();
+                event.height = fontHeight;
+            }
+        });
+
+        mTreeViewer.setContentProvider(mProfileProvider);
+        mTreeViewer.setLabelProvider(mProfileProvider.getLabelProvider());
+        mTreeViewer.setInput(mProfileProvider.getRoot());
+
+        // Create another composite to hold the label and text box
+        Composite composite = new Composite(this, SWT.NONE);
+        composite.setLayout(new GridLayout(2, false));
+        composite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        // Add a label for the search box
+        Label label = new Label(composite, SWT.NONE);
+        label.setText("Find:");
+
+        // Add a text box for searching for method names
+        mSearchBox = new Text(composite, SWT.BORDER);
+        mSearchBox.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        Display display = getDisplay();
+        mColorNoMatch = new Color(display, 255, 200, 200);
+        mColorMatch = mSearchBox.getBackground();
+
+        mSearchBox.addModifyListener(new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent ev) {
+                String query = mSearchBox.getText();
+                if (query.length() == 0)
+                    return;
+                findName(query);
+            }
+        });
+
+        // Add a key listener to the text box so that we can clear
+        // the text box if the user presses <ESC>.
+        mSearchBox.addKeyListener(new KeyAdapter() {
+            @Override
+            public void keyPressed(KeyEvent event) {
+                if (event.keyCode == SWT.ESC) {
+                    mSearchBox.setText("");
+                } else if (event.keyCode == SWT.CR) {
+                    String query = mSearchBox.getText();
+                    if (query.length() == 0)
+                        return;
+                    findNextName(query);
+                }
+            }
+        });
+
+        // Also add a key listener to the tree viewer so that the
+        // user can just start typing anywhere in the tree view.
+        tree.addKeyListener(new KeyAdapter() {
+            @Override
+            public void keyPressed(KeyEvent event) {
+                if (event.keyCode == SWT.ESC) {
+                    mSearchBox.setText("");
+                } else if (event.keyCode == SWT.BS) {
+                    // Erase the last character from the search box
+                    String text = mSearchBox.getText();
+                    int len = text.length();
+                    String chopped;
+                    if (len <= 1)
+                        chopped = "";
+                    else
+                        chopped = text.substring(0, len - 1);
+                    mSearchBox.setText(chopped);
+                } else if (event.keyCode == SWT.CR) {
+                    String query = mSearchBox.getText();
+                    if (query.length() == 0)
+                        return;
+                    findNextName(query);
+                } else {
+                    // Append the given character to the search box
+                    String str = String.valueOf(event.character);
+                    mSearchBox.append(str);
+                }
+                event.doit = false;
+            }
+        });
+
+        // Add a selection listener to the tree so that the user can click
+        // on a method that is a child or parent and jump to that method.
+        mTreeViewer
+                .addSelectionChangedListener(new ISelectionChangedListener() {
+                    @Override
+                    public void selectionChanged(SelectionChangedEvent ev) {
+                        ISelection sel = ev.getSelection();
+                        if (sel.isEmpty())
+                            return;
+                        if (sel instanceof IStructuredSelection) {
+                            IStructuredSelection selection = (IStructuredSelection) sel;
+                            Object element = selection.getFirstElement();
+                            if (element == null)
+                                return;
+                            if (element instanceof MethodData) {
+                                MethodData md = (MethodData) element;
+                                highlightMethod(md, true);
+                            }
+                            if (element instanceof ProfileData) {
+                                MethodData md = ((ProfileData) element)
+                                        .getMethodData();
+                                highlightMethod(md, true);
+                            }
+                        }
+                    }
+                });
+
+        // Add a tree listener so that we can expand the parents and children
+        // of a method when a method is expanded.
+        mTreeViewer.addTreeListener(new ITreeViewerListener() {
+            @Override
+            public void treeExpanded(TreeExpansionEvent event) {
+                Object element = event.getElement();
+                if (element instanceof MethodData) {
+                    MethodData md = (MethodData) element;
+                    expandNode(md);
+                }
+            }
+            @Override
+            public void treeCollapsed(TreeExpansionEvent event) {
+            }
+        });
+
+        tree.addListener(SWT.MouseDown, new Listener() {
+            @Override
+            public void handleEvent(Event event) {
+                Point point = new Point(event.x, event.y);
+                TreeItem treeItem = tree.getItem(point);
+                MethodData md = mProfileProvider.findMatchingTreeItem(treeItem);
+                if (md == null)
+                    return;
+                ArrayList<Selection> selections = new ArrayList<Selection>();
+                selections.add(Selection.highlight("MethodData", md));
+                mSelectionController.change(selections, "ProfileView");
+
+                if (mMethodHandler != null && (event.stateMask & SWT.MOD1) != 0) {
+                    mMethodHandler.handleMethod(md);
+                }
+            }
+        });
+    }
+
+    public void setMethodHandler(MethodHandler handler) {
+        mMethodHandler = handler;
+    }
+
+    private void findName(String query) {
+        MethodData md = mProfileProvider.findMatchingName(query);
+        selectMethod(md);
+    }
+
+    private void findNextName(String query) {
+        MethodData md = mProfileProvider.findNextMatchingName(query);
+        selectMethod(md);
+    }
+
+    private void selectMethod(MethodData md) {
+        if (md == null) {
+            mSearchBox.setBackground(mColorNoMatch);
+            return;
+        }
+        mSearchBox.setBackground(mColorMatch);
+        highlightMethod(md, false);
+    }
+
+    @Override
+    public void update(Observable objservable, Object arg) {
+        // Ignore updates from myself
+        if (arg == "ProfileView")
+            return;
+        // System.out.printf("profileview update from %s\n", arg);
+        ArrayList<Selection> selections;
+        selections = mSelectionController.getSelections();
+        for (Selection selection : selections) {
+            Selection.Action action = selection.getAction();
+            if (action != Selection.Action.Highlight)
+                continue;
+            String name = selection.getName();
+            if (name == "MethodData") {
+                MethodData md = (MethodData) selection.getValue();
+                highlightMethod(md, true);
+                return;
+            }
+            if (name == "Call") {
+                Call call = (Call) selection.getValue();
+                MethodData md = call.getMethodData();
+                highlightMethod(md, true);
+                return;
+            }
+        }
+    }
+
+    private void highlightMethod(MethodData md, boolean clearSearch) {
+        if (md == null)
+            return;
+        // Avoid an infinite recursion
+        if (md == mCurrentHighlightedMethod)
+            return;
+        if (clearSearch) {
+            mSearchBox.setText("");
+            mSearchBox.setBackground(mColorMatch);
+        }
+        mCurrentHighlightedMethod = md;
+        mTreeViewer.collapseAll();
+        // Expand this node and its children
+        expandNode(md);
+        StructuredSelection sel = new StructuredSelection(md);
+        mTreeViewer.setSelection(sel, true);
+        Tree tree = mTreeViewer.getTree();
+        TreeItem[] items = tree.getSelection();
+        if (items.length != 0) {
+            tree.setTopItem(items[0]);
+            // workaround a Mac bug by adding showItem().
+            tree.showItem(items[0]);
+        }
+    }
+
+    private void expandNode(MethodData md) {
+        ProfileNode[] nodes = md.getProfileNodes();
+        mTreeViewer.setExpandedState(md, true);
+        // Also expand the "Parents" and "Children" nodes.
+        if (nodes != null) {
+            for (ProfileNode node : nodes) {
+                if (node.isRecursive() == false)
+                    mTreeViewer.setExpandedState(node, true);
+            }
+        }
+    }
+}
diff --git a/traceview/src/main/java/com/android/traceview/PropertiesDialog.java b/traceview/src/main/java/com/android/traceview/PropertiesDialog.java
new file mode 100644
index 0000000..9f5eff9
--- /dev/null
+++ b/traceview/src/main/java/com/android/traceview/PropertiesDialog.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.traceview;
+
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.viewers.ArrayContentProvider;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Shell;
+
+import java.util.HashMap;
+import java.util.Map.Entry;
+
+public class PropertiesDialog extends Dialog {
+    private HashMap<String, String> mProperties;
+
+    public PropertiesDialog(Shell parent) {
+        super(parent);
+
+        setShellStyle(SWT.DIALOG_TRIM | SWT.RESIZE);
+    }
+
+    public void setProperties(HashMap<String, String> properties) {
+        mProperties = properties;
+    }
+
+    @Override
+    protected void createButtonsForButtonBar(Composite parent) {
+        createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true);
+    }
+
+    @Override
+    protected Control createDialogArea(Composite parent) {
+        Composite container = (Composite) super.createDialogArea(parent);
+        GridLayout gridLayout = new GridLayout(1, false);
+        gridLayout.marginWidth = 0;
+        gridLayout.marginHeight = 0;
+        gridLayout.horizontalSpacing = 0;
+        gridLayout.verticalSpacing = 0;
+        container.setLayout(gridLayout);
+
+        TableViewer tableViewer = new TableViewer(container, SWT.HIDE_SELECTION
+                | SWT.V_SCROLL | SWT.BORDER);
+        tableViewer.getTable().setLinesVisible(true);
+        tableViewer.getTable().setHeaderVisible(true);
+
+        TableViewerColumn propertyColumn = new TableViewerColumn(tableViewer, SWT.NONE);
+        propertyColumn.getColumn().setText("Property");
+        propertyColumn.setLabelProvider(new ColumnLabelProvider() {
+            @Override
+            @SuppressWarnings("unchecked")
+            public String getText(Object element) {
+                Entry<String, String> entry = (Entry<String, String>) element;
+                return entry.getKey();
+            }
+        });
+        propertyColumn.getColumn().setWidth(400);
+
+        TableViewerColumn valueColumn = new TableViewerColumn(tableViewer, SWT.NONE);
+        valueColumn.getColumn().setText("Value");
+        valueColumn.setLabelProvider(new ColumnLabelProvider() {
+            @Override
+            @SuppressWarnings("unchecked")
+            public String getText(Object element) {
+                Entry<String, String> entry = (Entry<String, String>) element;
+                return entry.getValue();
+            }
+        });
+        valueColumn.getColumn().setWidth(200);
+
+        tableViewer.setContentProvider(new ArrayContentProvider());
+        tableViewer.setInput(mProperties.entrySet().toArray());
+
+        GridData gridData = new GridData();
+        gridData.verticalAlignment = GridData.FILL;
+        gridData.horizontalAlignment = GridData.FILL;
+        gridData.grabExcessHorizontalSpace = true;
+        gridData.grabExcessVerticalSpace = true;
+        tableViewer.getControl().setLayoutData(gridData);
+
+        return container;
+    }
+}
diff --git a/traceview/src/main/java/com/android/traceview/Selection.java b/traceview/src/main/java/com/android/traceview/Selection.java
new file mode 100644
index 0000000..3764619
--- /dev/null
+++ b/traceview/src/main/java/com/android/traceview/Selection.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2006 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.traceview;
+
+public class Selection {
+
+    private Action mAction;
+    private String mName;
+    private Object mValue;
+
+    public Selection(Action action, String name, Object value) {
+        mAction = action;
+        mName = name;
+        mValue = value;
+    }
+
+    public static Selection highlight(String name, Object value) {
+        return new Selection(Action.Highlight, name, value);
+    }
+
+    public static Selection include(String name, Object value) {
+        return new Selection(Action.Include, name, value);
+    }
+
+    public static Selection exclude(String name, Object value) {
+        return new Selection(Action.Exclude, name, value);
+    }
+
+    public void setName(String name) {
+        mName = name;
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public void setValue(Object value) {
+        mValue = value;
+    }
+
+    public Object getValue() {
+        return mValue;
+    }
+
+    public void setAction(Action action) {
+        mAction = action;
+    }
+
+    public Action getAction() {
+        return mAction;
+    }
+
+    public static enum Action {
+        Highlight, Include, Exclude, Aggregate
+    };
+}
diff --git a/traceview/src/main/java/com/android/traceview/SelectionController.java b/traceview/src/main/java/com/android/traceview/SelectionController.java
new file mode 100644
index 0000000..4c930ea
--- /dev/null
+++ b/traceview/src/main/java/com/android/traceview/SelectionController.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2006 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.traceview;
+
+import java.util.ArrayList;
+import java.util.Observable;
+
+public class SelectionController extends Observable {
+
+    private ArrayList<Selection> mSelections;
+
+    public void change(ArrayList<Selection> selections, Object arg) {
+        this.mSelections = selections;
+        setChanged();
+        notifyObservers(arg);
+    }
+
+    public ArrayList<Selection> getSelections() {
+        return mSelections;
+    }
+}
diff --git a/traceview/src/main/java/com/android/traceview/ThreadData.java b/traceview/src/main/java/com/android/traceview/ThreadData.java
new file mode 100644
index 0000000..05e54e8
--- /dev/null
+++ b/traceview/src/main/java/com/android/traceview/ThreadData.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2006 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.traceview;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+class ThreadData implements TimeLineView.Row {
+
+    private int mId;
+    private String mName;
+    private boolean mIsEmpty;
+
+    private Call mRootCall;
+    private ArrayList<Call> mStack = new ArrayList<Call>();
+
+    // This is a hash of all the methods that are currently on the stack.
+    private HashMap<MethodData, Integer> mStackMethods = new HashMap<MethodData, Integer>();
+
+    boolean mHaveGlobalTime;
+    long mGlobalStartTime;
+    long mGlobalEndTime;
+
+    boolean mHaveThreadTime;
+    long mThreadStartTime;
+    long mThreadEndTime;
+
+    long mThreadCurrentTime; // only used while parsing thread-cpu clock
+
+    ThreadData(int id, String name, MethodData topLevel) {
+        mId = id;
+        mName = String.format("[%d] %s", id, name);
+        mIsEmpty = true;
+        mRootCall = new Call(this, topLevel, null);
+        mRootCall.setName(mName);
+        mStack.add(mRootCall);
+    }
+
+    @Override
+    public String getName() {
+        return mName;
+    }
+
+    public Call getRootCall() {
+        return mRootCall;
+    }
+
+    /**
+     * Returns true if no calls have ever been recorded for this thread.
+     */
+    public boolean isEmpty() {
+        return mIsEmpty;
+    }
+
+    Call enter(MethodData method, ArrayList<TraceAction> trace) {
+        if (mIsEmpty) {
+            mIsEmpty = false;
+            if (trace != null) {
+                trace.add(new TraceAction(TraceAction.ACTION_ENTER, mRootCall));
+            }
+        }
+
+        Call caller = top();
+        Call call = new Call(this, method, caller);
+        mStack.add(call);
+
+        if (trace != null) {
+            trace.add(new TraceAction(TraceAction.ACTION_ENTER, call));
+        }
+
+        Integer num = mStackMethods.get(method);
+        if (num == null) {
+            num = 0;
+        } else if (num > 0) {
+            call.setRecursive(true);
+        }
+        mStackMethods.put(method, num + 1);
+
+        return call;
+    }
+
+    Call exit(MethodData method, ArrayList<TraceAction> trace) {
+        Call call = top();
+        if (call.mCaller == null) {
+            return null;
+        }
+
+        if (call.getMethodData() != method) {
+            String error = "Method exit (" + method.getName()
+                    + ") does not match current method (" + call.getMethodData().getName()
+                    + ")";
+            throw new RuntimeException(error);
+        }
+
+        mStack.remove(mStack.size() - 1);
+
+        if (trace != null) {
+            trace.add(new TraceAction(TraceAction.ACTION_EXIT, call));
+        }
+
+        Integer num = mStackMethods.get(method);
+        if (num != null) {
+            if (num == 1) {
+                mStackMethods.remove(method);
+            } else {
+                mStackMethods.put(method, num - 1);
+            }
+        }
+
+        return call;
+    }
+
+    Call top() {
+        return mStack.get(mStack.size() - 1);
+    }
+
+    void endTrace(ArrayList<TraceAction> trace) {
+        for (int i = mStack.size() - 1; i >= 1; i--) {
+            Call call = mStack.get(i);
+            call.mGlobalEndTime = mGlobalEndTime;
+            call.mThreadEndTime = mThreadEndTime;
+            if (trace != null) {
+                trace.add(new TraceAction(TraceAction.ACTION_INCOMPLETE, call));
+            }
+        }
+        mStack.clear();
+        mStackMethods.clear();
+    }
+
+    void updateRootCallTimeBounds() {
+        if (!mIsEmpty) {
+            mRootCall.mGlobalStartTime = mGlobalStartTime;
+            mRootCall.mGlobalEndTime = mGlobalEndTime;
+            mRootCall.mThreadStartTime = mThreadStartTime;
+            mRootCall.mThreadEndTime = mThreadEndTime;
+        }
+    }
+
+    @Override
+    public String toString() {
+        return mName;
+    }
+
+    @Override
+    public int getId() {
+        return mId;
+    }
+
+    public long getCpuTime() {
+        return mRootCall.mInclusiveCpuTime;
+    }
+
+    public long getRealTime() {
+        return mRootCall.mInclusiveRealTime;
+    }
+}
diff --git a/traceview/src/main/java/com/android/traceview/TickScaler.java b/traceview/src/main/java/com/android/traceview/TickScaler.java
new file mode 100644
index 0000000..79fa160
--- /dev/null
+++ b/traceview/src/main/java/com/android/traceview/TickScaler.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2006 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.traceview;
+
+class TickScaler {
+
+    private double mMinVal; // required input
+    private double mMaxVal; // required input
+    private double mRangeVal;
+    private int mNumPixels; // required input
+    private int mPixelsPerTick; // required input
+    private double mPixelsPerRange;
+    private double mTickIncrement;
+    private double mMinMajorTick;
+
+    TickScaler(double minVal, double maxVal, int numPixels, int pixelsPerTick) {
+        mMinVal = minVal;
+        mMaxVal = maxVal;
+        mNumPixels = numPixels;
+        mPixelsPerTick = pixelsPerTick;
+    }
+
+    public void setMinVal(double minVal) {
+        mMinVal = minVal;
+    }
+
+    public double getMinVal() {
+        return mMinVal;
+    }
+
+    public void setMaxVal(double maxVal) {
+        mMaxVal = maxVal;
+    }
+
+    public double getMaxVal() {
+        return mMaxVal;
+    }
+
+    public void setNumPixels(int numPixels) {
+        mNumPixels = numPixels;
+    }
+
+    public int getNumPixels() {
+        return mNumPixels;
+    }
+
+    public void setPixelsPerTick(int pixelsPerTick) {
+        mPixelsPerTick = pixelsPerTick;
+    }
+
+    public int getPixelsPerTick() {
+        return mPixelsPerTick;
+    }
+
+    public void setPixelsPerRange(double pixelsPerRange) {
+        mPixelsPerRange = pixelsPerRange;
+    }
+
+    public double getPixelsPerRange() {
+        return mPixelsPerRange;
+    }
+
+    public void setTickIncrement(double tickIncrement) {
+        mTickIncrement = tickIncrement;
+    }
+
+    public double getTickIncrement() {
+        return mTickIncrement;
+    }
+
+    public void setMinMajorTick(double minMajorTick) {
+        mMinMajorTick = minMajorTick;
+    }
+
+    public double getMinMajorTick() {
+        return mMinMajorTick;
+    }
+
+    // Convert a time value to a 0-based pixel value
+    public int valueToPixel(double value) {
+        return (int) Math.ceil(mPixelsPerRange * (value - mMinVal) - 0.5);
+    }
+
+    // Convert a time value to a 0-based fractional pixel
+    public double valueToPixelFraction(double value) {
+        return mPixelsPerRange * (value - mMinVal);
+    }
+
+    // Convert a 0-based pixel value to a time value
+    public double pixelToValue(int pixel) {
+        return mMinVal + (pixel / mPixelsPerRange);
+    }
+
+    public void computeTicks(boolean useGivenEndPoints) {
+        int numTicks = mNumPixels / mPixelsPerTick;
+        mRangeVal = mMaxVal - mMinVal;
+        mTickIncrement = mRangeVal / numTicks;
+        double dlogTickIncrement = Math.log10(mTickIncrement);
+        int logTickIncrement = (int) Math.floor(dlogTickIncrement);
+        double scale = Math.pow(10, logTickIncrement);
+        double scaledTickIncr = mTickIncrement / scale;
+        if (scaledTickIncr > 5.0)
+            scaledTickIncr = 10;
+        else if (scaledTickIncr > 2)
+            scaledTickIncr = 5;
+        else if (scaledTickIncr > 1)
+            scaledTickIncr = 2;
+        else
+            scaledTickIncr = 1;
+        mTickIncrement = scaledTickIncr * scale;
+
+        if (!useGivenEndPoints) {
+            // Round up the max val to the next minor tick
+            double minorTickIncrement = mTickIncrement / 5;
+            double dval = mMaxVal / minorTickIncrement;
+            int ival = (int) dval;
+            if (ival != dval)
+                mMaxVal = (ival + 1) * minorTickIncrement;
+
+            // Round down the min val to a multiple of tickIncrement
+            ival = (int) (mMinVal / mTickIncrement);
+            mMinVal = ival * mTickIncrement;
+            mMinMajorTick = mMinVal;
+        } else {
+            int ival = (int) (mMinVal / mTickIncrement);
+            mMinMajorTick = ival * mTickIncrement;
+            if (mMinMajorTick < mMinVal)
+                mMinMajorTick = mMinMajorTick + mTickIncrement;
+        }
+
+        mRangeVal = mMaxVal - mMinVal;
+        mPixelsPerRange = (double) mNumPixels / mRangeVal;
+    }
+}
diff --git a/traceview/src/main/java/com/android/traceview/TimeBase.java b/traceview/src/main/java/com/android/traceview/TimeBase.java
new file mode 100644
index 0000000..b6b23cb
--- /dev/null
+++ b/traceview/src/main/java/com/android/traceview/TimeBase.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2006 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.traceview;
+
+interface TimeBase {
+    public static final TimeBase CPU_TIME = new CpuTimeBase();
+    public static final TimeBase REAL_TIME = new RealTimeBase();
+
+    public long getTime(ThreadData threadData);
+    public long getElapsedInclusiveTime(MethodData methodData);
+    public long getElapsedExclusiveTime(MethodData methodData);
+    public long getElapsedInclusiveTime(ProfileData profileData);
+
+    public static final class CpuTimeBase implements TimeBase {
+        @Override
+        public long getTime(ThreadData threadData) {
+            return threadData.getCpuTime();
+        }
+
+        @Override
+        public long getElapsedInclusiveTime(MethodData methodData) {
+            return methodData.getElapsedInclusiveCpuTime();
+        }
+
+        @Override
+        public long getElapsedExclusiveTime(MethodData methodData) {
+            return methodData.getElapsedExclusiveCpuTime();
+        }
+
+        @Override
+        public long getElapsedInclusiveTime(ProfileData profileData) {
+            return profileData.getElapsedInclusiveCpuTime();
+        }
+    }
+
+    public static final class RealTimeBase implements TimeBase {
+        @Override
+        public long getTime(ThreadData threadData) {
+            return threadData.getRealTime();
+        }
+
+        @Override
+        public long getElapsedInclusiveTime(MethodData methodData) {
+            return methodData.getElapsedInclusiveRealTime();
+        }
+
+        @Override
+        public long getElapsedExclusiveTime(MethodData methodData) {
+            return methodData.getElapsedExclusiveRealTime();
+        }
+
+        @Override
+        public long getElapsedInclusiveTime(ProfileData profileData) {
+            return profileData.getElapsedInclusiveRealTime();
+        }
+    }
+}
diff --git a/traceview/src/main/java/com/android/traceview/TimeLineView.java b/traceview/src/main/java/com/android/traceview/TimeLineView.java
new file mode 100644
index 0000000..cc9613a
--- /dev/null
+++ b/traceview/src/main/java/com/android/traceview/TimeLineView.java
@@ -0,0 +1,2154 @@
+/*
+ * Copyright (C) 2006 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.traceview;
+
+import org.eclipse.jface.resource.FontRegistry;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseMoveListener;
+import org.eclipse.swt.events.MouseWheelListener;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Cursor;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.ScrollBar;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Observable;
+import java.util.Observer;
+
+public class TimeLineView extends Composite implements Observer {
+
+    private HashMap<String, RowData> mRowByName;
+    private RowData[] mRows;
+    private Segment[] mSegments;
+    private HashMap<Integer, String> mThreadLabels;
+    private Timescale mTimescale;
+    private Surface mSurface;
+    private RowLabels mLabels;
+    private SashForm mSashForm;
+    private int mScrollOffsetY;
+
+    public static final int PixelsPerTick = 50;
+    private TickScaler mScaleInfo = new TickScaler(0, 0, 0, PixelsPerTick);
+    private static final int LeftMargin = 10; // blank space on left
+    private static final int RightMargin = 60; // blank space on right
+
+    private Color mColorBlack;
+    private Color mColorGray;
+    private Color mColorDarkGray;
+    private Color mColorForeground;
+    private Color mColorRowBack;
+    private Color mColorZoomSelection;
+    private FontRegistry mFontRegistry;
+
+    /** vertical height of drawn blocks in each row */
+    private static final int rowHeight = 20;
+
+    /** the blank space between rows */
+    private static final int rowYMargin = 12;
+    private static final int rowYMarginHalf = rowYMargin / 2;
+
+    /** total vertical space for row */
+    private static final int rowYSpace = rowHeight + rowYMargin;
+    private static final int majorTickLength = 8;
+    private static final int minorTickLength = 4;
+    private static final int timeLineOffsetY = 58;
+    private static final int tickToFontSpacing = 2;
+
+    /** start of first row */
+    private static final int topMargin = 90;
+    private int mMouseRow = -1;
+    private int mNumRows;
+    private int mStartRow;
+    private int mEndRow;
+    private TraceUnits mUnits;
+    private String mClockSource;
+    private boolean mHaveCpuTime;
+    private boolean mHaveRealTime;
+    private int mSmallFontWidth;
+    private int mSmallFontHeight;
+    private SelectionController mSelectionController;
+    private MethodData mHighlightMethodData;
+    private Call mHighlightCall;
+    private static final int MinInclusiveRange = 3;
+
+    /** Setting the fonts looks good on Linux but bad on Macs */
+    private boolean mSetFonts = false;
+
+    public static interface Block {
+        public String getName();
+        public MethodData getMethodData();
+        public long getStartTime();
+        public long getEndTime();
+        public Color getColor();
+        public double addWeight(int x, int y, double weight);
+        public void clearWeight();
+        public long getExclusiveCpuTime();
+        public long getInclusiveCpuTime();
+        public long getExclusiveRealTime();
+        public long getInclusiveRealTime();
+        public boolean isContextSwitch();
+        public boolean isIgnoredBlock();
+        public Block getParentBlock();
+    }
+
+    public static interface Row {
+        public int getId();
+        public String getName();
+    }
+
+    public static class Record {
+        Row row;
+        Block block;
+
+        public Record(Row row, Block block) {
+            this.row = row;
+            this.block = block;
+        }
+    }
+
+    public TimeLineView(Composite parent, TraceReader reader,
+            SelectionController selectionController) {
+        super(parent, SWT.NONE);
+        mRowByName = new HashMap<String, RowData>();
+        this.mSelectionController = selectionController;
+        selectionController.addObserver(this);
+        mUnits = reader.getTraceUnits();
+        mClockSource = reader.getClockSource();
+        mHaveCpuTime = reader.haveCpuTime();
+        mHaveRealTime = reader.haveRealTime();
+        mThreadLabels = reader.getThreadLabels();
+
+        Display display = getDisplay();
+        mColorGray = display.getSystemColor(SWT.COLOR_GRAY);
+        mColorDarkGray = display.getSystemColor(SWT.COLOR_DARK_GRAY);
+        mColorBlack = display.getSystemColor(SWT.COLOR_BLACK);
+        // mColorBackground = display.getSystemColor(SWT.COLOR_WHITE);
+        mColorForeground = display.getSystemColor(SWT.COLOR_BLACK);
+        mColorRowBack = new Color(display, 240, 240, 255);
+        mColorZoomSelection = new Color(display, 230, 230, 230);
+
+        mFontRegistry = new FontRegistry(display);
+        mFontRegistry.put("small",  //$NON-NLS-1$
+                new FontData[] { new FontData("Arial", 8, SWT.NORMAL) });  //$NON-NLS-1$
+        mFontRegistry.put("courier8",  //$NON-NLS-1$
+                new FontData[] { new FontData("Courier New", 8, SWT.BOLD) });  //$NON-NLS-1$
+        mFontRegistry.put("medium",  //$NON-NLS-1$
+                new FontData[] { new FontData("Courier New", 10, SWT.NORMAL) });  //$NON-NLS-1$
+
+        Image image = new Image(display, new Rectangle(100, 100, 100, 100));
+        GC gc = new GC(image);
+        if (mSetFonts) {
+            gc.setFont(mFontRegistry.get("small"));  //$NON-NLS-1$
+        }
+        mSmallFontWidth = gc.getFontMetrics().getAverageCharWidth();
+        mSmallFontHeight = gc.getFontMetrics().getHeight();
+
+        image.dispose();
+        gc.dispose();
+
+        setLayout(new FillLayout());
+
+        // Create a sash form for holding two canvas views, one for the
+        // thread labels and one for the thread timeline.
+        mSashForm = new SashForm(this, SWT.HORIZONTAL);
+        mSashForm.setBackground(mColorGray);
+        mSashForm.SASH_WIDTH = 3;
+
+        // Create a composite for the left side of the sash
+        Composite composite = new Composite(mSashForm, SWT.NONE);
+        GridLayout layout = new GridLayout(1, true /* make columns equal width */);
+        layout.marginHeight = 0;
+        layout.marginWidth = 0;
+        layout.verticalSpacing = 1;
+        composite.setLayout(layout);
+
+        // Create a blank corner space in the upper left corner
+        BlankCorner corner = new BlankCorner(composite);
+        GridData gridData = new GridData(GridData.FILL_HORIZONTAL);
+        gridData.heightHint = topMargin;
+        corner.setLayoutData(gridData);
+
+        // Add the thread labels below the blank corner.
+        mLabels = new RowLabels(composite);
+        gridData = new GridData(GridData.FILL_BOTH);
+        mLabels.setLayoutData(gridData);
+
+        // Create another composite for the right side of the sash
+        composite = new Composite(mSashForm, SWT.NONE);
+        layout = new GridLayout(1, true /* make columns equal width */);
+        layout.marginHeight = 0;
+        layout.marginWidth = 0;
+        layout.verticalSpacing = 1;
+        composite.setLayout(layout);
+
+        mTimescale = new Timescale(composite);
+        gridData = new GridData(GridData.FILL_HORIZONTAL);
+        gridData.heightHint = topMargin;
+        mTimescale.setLayoutData(gridData);
+
+        mSurface = new Surface(composite);
+        gridData = new GridData(GridData.FILL_BOTH);
+        mSurface.setLayoutData(gridData);
+        mSashForm.setWeights(new int[] { 1, 5 });
+
+        final ScrollBar vBar = mSurface.getVerticalBar();
+        vBar.addListener(SWT.Selection, new Listener() {
+           @Override
+        public void handleEvent(Event e) {
+               mScrollOffsetY = vBar.getSelection();
+               Point dim = mSurface.getSize();
+               int newScrollOffsetY = computeVisibleRows(dim.y);
+               if (newScrollOffsetY != mScrollOffsetY) {
+                   mScrollOffsetY = newScrollOffsetY;
+                   vBar.setSelection(newScrollOffsetY);
+               }
+               mLabels.redraw();
+               mSurface.redraw();
+           }
+        });
+
+        final ScrollBar hBar = mSurface.getHorizontalBar();
+        hBar.addListener(SWT.Selection, new Listener() {
+            @Override
+            public void handleEvent(Event e) {
+                mSurface.setScaleFromHorizontalScrollBar(hBar.getSelection());
+                mSurface.redraw();
+            }
+        });
+
+        mSurface.addListener(SWT.Resize, new Listener() {
+            @Override
+            public void handleEvent(Event e) {
+                Point dim = mSurface.getSize();
+
+                // If we don't need the scroll bar then don't display it.
+                if (dim.y >= mNumRows * rowYSpace) {
+                    vBar.setVisible(false);
+                } else {
+                    vBar.setVisible(true);
+                }
+                int newScrollOffsetY = computeVisibleRows(dim.y);
+                if (newScrollOffsetY != mScrollOffsetY) {
+                    mScrollOffsetY = newScrollOffsetY;
+                    vBar.setSelection(newScrollOffsetY);
+                }
+
+                int spaceNeeded = mNumRows * rowYSpace;
+                vBar.setMaximum(spaceNeeded);
+                vBar.setThumb(dim.y);
+
+                mLabels.redraw();
+                mSurface.redraw();
+            }
+        });
+
+        mSurface.addMouseListener(new MouseAdapter() {
+            @Override
+            public void mouseUp(MouseEvent me) {
+                mSurface.mouseUp(me);
+            }
+
+            @Override
+            public void mouseDown(MouseEvent me) {
+                mSurface.mouseDown(me);
+            }
+
+            @Override
+            public void mouseDoubleClick(MouseEvent me) {
+                mSurface.mouseDoubleClick(me);
+            }
+        });
+
+        mSurface.addMouseMoveListener(new MouseMoveListener() {
+            @Override
+            public void mouseMove(MouseEvent me) {
+                mSurface.mouseMove(me);
+            }
+        });
+
+        mSurface.addMouseWheelListener(new MouseWheelListener() {
+            @Override
+            public void mouseScrolled(MouseEvent me) {
+                mSurface.mouseScrolled(me);
+            }
+        });
+
+        mTimescale.addMouseListener(new MouseAdapter() {
+            @Override
+            public void mouseUp(MouseEvent me) {
+                mTimescale.mouseUp(me);
+            }
+
+            @Override
+            public void mouseDown(MouseEvent me) {
+                mTimescale.mouseDown(me);
+            }
+
+            @Override
+            public void mouseDoubleClick(MouseEvent me) {
+                mTimescale.mouseDoubleClick(me);
+            }
+        });
+
+        mTimescale.addMouseMoveListener(new MouseMoveListener() {
+            @Override
+            public void mouseMove(MouseEvent me) {
+                mTimescale.mouseMove(me);
+            }
+        });
+
+        mLabels.addMouseMoveListener(new MouseMoveListener() {
+            @Override
+            public void mouseMove(MouseEvent me) {
+                mLabels.mouseMove(me);
+            }
+        });
+
+        setData(reader.getThreadTimeRecords());
+    }
+
+    @Override
+    public void update(Observable objservable, Object arg) {
+        // Ignore updates from myself
+        if (arg == "TimeLineView")  //$NON-NLS-1$
+            return;
+        // System.out.printf("timeline update from %s\n", arg);
+        boolean foundHighlight = false;
+        ArrayList<Selection> selections;
+        selections = mSelectionController.getSelections();
+        for (Selection selection : selections) {
+            Selection.Action action = selection.getAction();
+            if (action != Selection.Action.Highlight)
+                continue;
+            String name = selection.getName();
+            // System.out.printf(" timeline highlight %s from %s\n", name, arg);
+            if (name == "MethodData") {  //$NON-NLS-1$
+                foundHighlight = true;
+                mHighlightMethodData = (MethodData) selection.getValue();
+                // System.out.printf(" method %s\n",
+                // highlightMethodData.getName());
+                mHighlightCall = null;
+                startHighlighting();
+            } else if (name == "Call") {  //$NON-NLS-1$
+                foundHighlight = true;
+                mHighlightCall = (Call) selection.getValue();
+                // System.out.printf(" call %s\n", highlightCall.getName());
+                mHighlightMethodData = null;
+                startHighlighting();
+            }
+        }
+        if (foundHighlight == false)
+            mSurface.clearHighlights();
+    }
+
+    public void setData(ArrayList<Record> records) {
+        if (records == null)
+            records = new ArrayList<Record>();
+
+        if (false) {
+            System.out.println("TimelineView() list of records:");  //$NON-NLS-1$
+            for (Record r : records) {
+                System.out.printf("row '%s' block '%s' [%d, %d]\n", r.row  //$NON-NLS-1$
+                        .getName(), r.block.getName(), r.block.getStartTime(),
+                        r.block.getEndTime());
+                if (r.block.getStartTime() > r.block.getEndTime()) {
+                    System.err.printf("Error: block startTime > endTime\n");  //$NON-NLS-1$
+                    System.exit(1);
+                }
+            }
+        }
+
+        // Sort the records into increasing start time, and decreasing end time
+        Collections.sort(records, new Comparator<Record>() {
+            @Override
+            public int compare(Record r1, Record r2) {
+                long start1 = r1.block.getStartTime();
+                long start2 = r2.block.getStartTime();
+                if (start1 > start2)
+                    return 1;
+                if (start1 < start2)
+                    return -1;
+
+                // The start times are the same, so compare the end times
+                long end1 = r1.block.getEndTime();
+                long end2 = r2.block.getEndTime();
+                if (end1 > end2)
+                    return -1;
+                if (end1 < end2)
+                    return 1;
+
+                return 0;
+            }
+        });
+
+        ArrayList<Segment> segmentList = new ArrayList<Segment>();
+
+        // The records are sorted into increasing start time,
+        // so the minimum start time is the start time of the first record.
+        double minVal = 0;
+        if (records.size() > 0)
+            minVal = records.get(0).block.getStartTime();
+
+        // Sum the time spent in each row and block, and
+        // keep track of the maximum end time.
+        double maxVal = 0;
+        for (Record rec : records) {
+            Row row = rec.row;
+            Block block = rec.block;
+            if (block.isIgnoredBlock()) {
+                continue;
+            }
+
+            String rowName = row.getName();
+            RowData rd = mRowByName.get(rowName);
+            if (rd == null) {
+                rd = new RowData(row);
+                mRowByName.put(rowName, rd);
+            }
+            long blockStartTime = block.getStartTime();
+            long blockEndTime = block.getEndTime();
+            if (blockEndTime > rd.mEndTime) {
+                long start = Math.max(blockStartTime, rd.mEndTime);
+                rd.mElapsed += blockEndTime - start;
+                rd.mEndTime = blockEndTime;
+            }
+            if (blockEndTime > maxVal)
+                maxVal = blockEndTime;
+
+            // Keep track of nested blocks by using a stack (for each row).
+            // Create a Segment object for each visible part of a block.
+            Block top = rd.top();
+            if (top == null) {
+                rd.push(block);
+                continue;
+            }
+
+            long topStartTime = top.getStartTime();
+            long topEndTime = top.getEndTime();
+            if (topEndTime >= blockStartTime) {
+                // Add this segment if it has a non-zero elapsed time.
+                if (topStartTime < blockStartTime) {
+                    Segment segment = new Segment(rd, top, topStartTime,
+                            blockStartTime);
+                    segmentList.add(segment);
+                }
+
+                // If this block starts where the previous (top) block ends,
+                // then pop off the top block.
+                if (topEndTime == blockStartTime)
+                    rd.pop();
+                rd.push(block);
+            } else {
+                // We may have to pop several frames here.
+                popFrames(rd, top, blockStartTime, segmentList);
+                rd.push(block);
+            }
+        }
+
+        // Clean up the stack of each row
+        for (RowData rd : mRowByName.values()) {
+            Block top = rd.top();
+            popFrames(rd, top, Integer.MAX_VALUE, segmentList);
+        }
+
+        mSurface.setRange(minVal, maxVal);
+        mSurface.setLimitRange(minVal, maxVal);
+
+        // Sort the rows into decreasing elapsed time
+        Collection<RowData> rv = mRowByName.values();
+        mRows = rv.toArray(new RowData[rv.size()]);
+        Arrays.sort(mRows, new Comparator<RowData>() {
+            @Override
+            public int compare(RowData rd1, RowData rd2) {
+                return (int) (rd2.mElapsed - rd1.mElapsed);
+            }
+        });
+
+        // Assign ranks to the sorted rows
+        for (int ii = 0; ii < mRows.length; ++ii) {
+            mRows[ii].mRank = ii;
+        }
+
+        // Compute the number of rows with data
+        mNumRows = 0;
+        for (int ii = 0; ii < mRows.length; ++ii) {
+            if (mRows[ii].mElapsed == 0)
+                break;
+            mNumRows += 1;
+        }
+
+        // Sort the blocks into increasing rows, and within rows into
+        // increasing start values.
+        mSegments = segmentList.toArray(new Segment[segmentList.size()]);
+        Arrays.sort(mSegments, new Comparator<Segment>() {
+            @Override
+            public int compare(Segment bd1, Segment bd2) {
+                RowData rd1 = bd1.mRowData;
+                RowData rd2 = bd2.mRowData;
+                int diff = rd1.mRank - rd2.mRank;
+                if (diff == 0) {
+                    long timeDiff = bd1.mStartTime - bd2.mStartTime;
+                    if (timeDiff == 0)
+                        timeDiff = bd1.mEndTime - bd2.mEndTime;
+                    return (int) timeDiff;
+                }
+                return diff;
+            }
+        });
+
+        if (false) {
+            for (Segment segment : mSegments) {
+                System.out.printf("seg '%s' [%6d, %6d] %s\n",
+                        segment.mRowData.mName, segment.mStartTime,
+                        segment.mEndTime, segment.mBlock.getName());
+                if (segment.mStartTime > segment.mEndTime) {
+                    System.err.printf("Error: segment startTime > endTime\n");
+                    System.exit(1);
+                }
+            }
+        }
+    }
+
+    private static void popFrames(RowData rd, Block top, long startTime,
+            ArrayList<Segment> segmentList) {
+        long topEndTime = top.getEndTime();
+        long lastEndTime = top.getStartTime();
+        while (topEndTime <= startTime) {
+            if (topEndTime > lastEndTime) {
+                Segment segment = new Segment(rd, top, lastEndTime, topEndTime);
+                segmentList.add(segment);
+                lastEndTime = topEndTime;
+            }
+            rd.pop();
+            top = rd.top();
+            if (top == null)
+                return;
+            topEndTime = top.getEndTime();
+        }
+
+        // If we get here, then topEndTime > startTime
+        if (lastEndTime < startTime) {
+            Segment bd = new Segment(rd, top, lastEndTime, startTime);
+            segmentList.add(bd);
+        }
+    }
+
+    private class RowLabels extends Canvas {
+
+        /** The space between the row label and the sash line */
+        private static final int labelMarginX = 2;
+
+        public RowLabels(Composite parent) {
+            super(parent, SWT.NO_BACKGROUND);
+            addPaintListener(new PaintListener() {
+                @Override
+                public void paintControl(PaintEvent pe) {
+                    draw(pe.display, pe.gc);
+                }
+            });
+        }
+
+        private void mouseMove(MouseEvent me) {
+            int rownum = (me.y + mScrollOffsetY) / rowYSpace;
+            if (mMouseRow != rownum) {
+                mMouseRow = rownum;
+                redraw();
+                mSurface.redraw();
+            }
+        }
+
+        private void draw(Display display, GC gc) {
+            if (mSegments.length == 0) {
+                // gc.setBackground(colorBackground);
+                // gc.fillRectangle(getBounds());
+                return;
+            }
+            Point dim = getSize();
+
+            // Create an image for double-buffering
+            Image image = new Image(display, getBounds());
+
+            // Set up the off-screen gc
+            GC gcImage = new GC(image);
+            if (mSetFonts)
+                gcImage.setFont(mFontRegistry.get("medium"));  //$NON-NLS-1$
+
+            if (mNumRows > 2) {
+                // Draw the row background stripes
+                gcImage.setBackground(mColorRowBack);
+                for (int ii = 1; ii < mNumRows; ii += 2) {
+                    RowData rd = mRows[ii];
+                    int y1 = rd.mRank * rowYSpace - mScrollOffsetY;
+                    gcImage.fillRectangle(0, y1, dim.x, rowYSpace);
+                }
+            }
+
+            // Draw the row labels
+            int offsetY = rowYMarginHalf - mScrollOffsetY;
+            for (int ii = mStartRow; ii <= mEndRow; ++ii) {
+                RowData rd = mRows[ii];
+                int y1 = rd.mRank * rowYSpace + offsetY;
+                Point extent = gcImage.stringExtent(rd.mName);
+                int x1 = dim.x - extent.x - labelMarginX;
+                gcImage.drawString(rd.mName, x1, y1, true);
+            }
+
+            // Draw a highlight box on the row where the mouse is.
+            if (mMouseRow >= mStartRow && mMouseRow <= mEndRow) {
+                gcImage.setForeground(mColorGray);
+                int y1 = mMouseRow * rowYSpace - mScrollOffsetY;
+                gcImage.drawRectangle(0, y1, dim.x, rowYSpace);
+            }
+
+            // Draw the off-screen buffer to the screen
+            gc.drawImage(image, 0, 0);
+
+            // Clean up
+            image.dispose();
+            gcImage.dispose();
+        }
+    }
+
+    private class BlankCorner extends Canvas {
+        public BlankCorner(Composite parent) {
+            //super(parent, SWT.NO_BACKGROUND);
+            super(parent, SWT.NONE);
+            addPaintListener(new PaintListener() {
+                @Override
+                public void paintControl(PaintEvent pe) {
+                    draw(pe.display, pe.gc);
+                }
+            });
+        }
+
+        private void draw(Display display, GC gc) {
+            // Create a blank image and draw it to the canvas
+            Image image = new Image(display, getBounds());
+            gc.drawImage(image, 0, 0);
+
+            // Clean up
+            image.dispose();
+        }
+    }
+
+    private class Timescale extends Canvas {
+        private Point mMouse = new Point(LeftMargin, 0);
+        private Cursor mZoomCursor;
+        private String mMethodName = null;
+        private Color mMethodColor = null;
+        private String mDetails;
+        private int mMethodStartY;
+        private int mDetailsStartY;
+        private int mMarkStartX;
+        private int mMarkEndX;
+
+        /** The space between the colored block and the method name */
+        private static final int METHOD_BLOCK_MARGIN = 10;
+
+        public Timescale(Composite parent) {
+            //super(parent, SWT.NO_BACKGROUND);
+            super(parent, SWT.NONE);
+            Display display = getDisplay();
+            mZoomCursor = new Cursor(display, SWT.CURSOR_SIZEWE);
+            setCursor(mZoomCursor);
+            mMethodStartY = mSmallFontHeight + 1;
+            mDetailsStartY = mMethodStartY + mSmallFontHeight + 1;
+            addPaintListener(new PaintListener() {
+                @Override
+                public void paintControl(PaintEvent pe) {
+                    draw(pe.display, pe.gc);
+                }
+            });
+        }
+
+        public void setVbarPosition(int x) {
+            mMouse.x = x;
+        }
+
+        public void setMarkStart(int x) {
+            mMarkStartX = x;
+        }
+
+        public void setMarkEnd(int x) {
+            mMarkEndX = x;
+        }
+
+        public void setMethodName(String name) {
+            mMethodName = name;
+        }
+
+        public void setMethodColor(Color color) {
+            mMethodColor = color;
+        }
+
+        public void setDetails(String details) {
+            mDetails = details;
+        }
+
+        private void mouseMove(MouseEvent me) {
+            me.y = -1;
+            mSurface.mouseMove(me);
+        }
+
+        private void mouseDown(MouseEvent me) {
+            mSurface.startScaling(me.x);
+            mSurface.redraw();
+        }
+
+        private void mouseUp(MouseEvent me) {
+            mSurface.stopScaling(me.x);
+        }
+
+        private void mouseDoubleClick(MouseEvent me) {
+            mSurface.resetScale();
+            mSurface.redraw();
+        }
+
+        private void draw(Display display, GC gc) {
+            Point dim = getSize();
+
+            // Create an image for double-buffering
+            Image image = new Image(display, getBounds());
+
+            // Set up the off-screen gc
+            GC gcImage = new GC(image);
+            if (mSetFonts)
+                gcImage.setFont(mFontRegistry.get("medium"));  //$NON-NLS-1$
+
+            if (mSurface.drawingSelection()) {
+                drawSelection(display, gcImage);
+            }
+
+            drawTicks(display, gcImage);
+
+            // Draw the vertical bar where the mouse is
+            gcImage.setForeground(mColorDarkGray);
+            gcImage.drawLine(mMouse.x, timeLineOffsetY, mMouse.x, dim.y);
+
+            // Draw the current millseconds
+            drawTickLegend(display, gcImage);
+
+            // Draw the method name and color, if needed
+            drawMethod(display, gcImage);
+
+            // Draw the details, if needed
+            drawDetails(display, gcImage);
+
+            // Draw the off-screen buffer to the screen
+            gc.drawImage(image, 0, 0);
+
+            // Clean up
+            image.dispose();
+            gcImage.dispose();
+        }
+
+        private void drawSelection(Display display, GC gc) {
+            Point dim = getSize();
+            gc.setForeground(mColorGray);
+            gc.drawLine(mMarkStartX, timeLineOffsetY, mMarkStartX, dim.y);
+            gc.setBackground(mColorZoomSelection);
+            int x, width;
+            if (mMarkStartX < mMarkEndX) {
+                x = mMarkStartX;
+                width = mMarkEndX - mMarkStartX;
+            } else {
+                x = mMarkEndX;
+                width = mMarkStartX - mMarkEndX;
+            }
+            if (width > 1) {
+                gc.fillRectangle(x, timeLineOffsetY, width, dim.y);
+            }
+        }
+
+        private void drawTickLegend(Display display, GC gc) {
+            int mouseX = mMouse.x - LeftMargin;
+            double mouseXval = mScaleInfo.pixelToValue(mouseX);
+            String info = mUnits.labelledString(mouseXval);
+            gc.setForeground(mColorForeground);
+            gc.drawString(info, LeftMargin + 2, 1, true);
+
+            // Display the maximum data value
+            double maxVal = mScaleInfo.getMaxVal();
+            info = mUnits.labelledString(maxVal);
+            if (mClockSource != null) {
+                info = String.format(" max %s (%s)", info, mClockSource);  //$NON-NLS-1$
+            } else {
+                info = String.format(" max %s ", info);  //$NON-NLS-1$
+            }
+            Point extent = gc.stringExtent(info);
+            Point dim = getSize();
+            int x1 = dim.x - RightMargin - extent.x;
+            gc.drawString(info, x1, 1, true);
+        }
+
+        private void drawMethod(Display display, GC gc) {
+            if (mMethodName == null) {
+                return;
+            }
+
+            int x1 = LeftMargin;
+            int y1 = mMethodStartY;
+            gc.setBackground(mMethodColor);
+            int width = 2 * mSmallFontWidth;
+            gc.fillRectangle(x1, y1, width, mSmallFontHeight);
+            x1 += width + METHOD_BLOCK_MARGIN;
+            gc.drawString(mMethodName, x1, y1, true);
+        }
+
+        private void drawDetails(Display display, GC gc) {
+            if (mDetails == null) {
+                return;
+            }
+
+            int x1 = LeftMargin + 2 * mSmallFontWidth + METHOD_BLOCK_MARGIN;
+            int y1 = mDetailsStartY;
+            gc.drawString(mDetails, x1, y1, true);
+        }
+
+        private void drawTicks(Display display, GC gc) {
+            Point dim = getSize();
+            int y2 = majorTickLength + timeLineOffsetY;
+            int y3 = minorTickLength + timeLineOffsetY;
+            int y4 = y2 + tickToFontSpacing;
+            gc.setForeground(mColorForeground);
+            gc.drawLine(LeftMargin, timeLineOffsetY, dim.x - RightMargin,
+                    timeLineOffsetY);
+            double minVal = mScaleInfo.getMinVal();
+            double maxVal = mScaleInfo.getMaxVal();
+            double minMajorTick = mScaleInfo.getMinMajorTick();
+            double tickIncrement = mScaleInfo.getTickIncrement();
+            double minorTickIncrement = tickIncrement / 5;
+            double pixelsPerRange = mScaleInfo.getPixelsPerRange();
+
+            // Draw the initial minor ticks, if any
+            if (minVal < minMajorTick) {
+                gc.setForeground(mColorGray);
+                double xMinor = minMajorTick;
+                for (int ii = 1; ii <= 4; ++ii) {
+                    xMinor -= minorTickIncrement;
+                    if (xMinor < minVal)
+                        break;
+                    int x1 = LeftMargin
+                            + (int) (0.5 + (xMinor - minVal) * pixelsPerRange);
+                    gc.drawLine(x1, timeLineOffsetY, x1, y3);
+                }
+            }
+
+            if (tickIncrement <= 10) {
+                // TODO avoid rendering the loop when tickIncrement is invalid. It can be zero
+                // or too small.
+                // System.out.println(String.format("Timescale.drawTicks error: tickIncrement=%1f", tickIncrement));
+                return;
+            }
+            for (double x = minMajorTick; x <= maxVal; x += tickIncrement) {
+                int x1 = LeftMargin
+                        + (int) (0.5 + (x - minVal) * pixelsPerRange);
+
+                // Draw a major tick
+                gc.setForeground(mColorForeground);
+                gc.drawLine(x1, timeLineOffsetY, x1, y2);
+                if (x > maxVal)
+                    break;
+
+                // Draw the tick text
+                String tickString = mUnits.valueOf(x);
+                gc.drawString(tickString, x1, y4, true);
+
+                // Draw 4 minor ticks between major ticks
+                gc.setForeground(mColorGray);
+                double xMinor = x;
+                for (int ii = 1; ii <= 4; ii++) {
+                    xMinor += minorTickIncrement;
+                    if (xMinor > maxVal)
+                        break;
+                    x1 = LeftMargin
+                            + (int) (0.5 + (xMinor - minVal) * pixelsPerRange);
+                    gc.drawLine(x1, timeLineOffsetY, x1, y3);
+                }
+            }
+        }
+    }
+
+    private static enum GraphicsState {
+        Normal, Marking, Scaling, Animating, Scrolling
+    };
+
+    private class Surface extends Canvas {
+
+        public Surface(Composite parent) {
+            super(parent, SWT.NO_BACKGROUND | SWT.V_SCROLL | SWT.H_SCROLL);
+            Display display = getDisplay();
+            mNormalCursor = new Cursor(display, SWT.CURSOR_CROSS);
+            mIncreasingCursor = new Cursor(display, SWT.CURSOR_SIZEE);
+            mDecreasingCursor = new Cursor(display, SWT.CURSOR_SIZEW);
+
+            initZoomFractionsWithExp();
+
+            addPaintListener(new PaintListener() {
+                @Override
+                public void paintControl(PaintEvent pe) {
+                    draw(pe.display, pe.gc);
+                }
+            });
+
+            mZoomAnimator = new Runnable() {
+                @Override
+                public void run() {
+                    animateZoom();
+                }
+            };
+
+            mHighlightAnimator = new Runnable() {
+                @Override
+                public void run() {
+                    animateHighlight();
+                }
+            };
+        }
+
+        private void initZoomFractionsWithExp() {
+            mZoomFractions = new double[ZOOM_STEPS];
+            int next = 0;
+            for (int ii = 0; ii < ZOOM_STEPS / 2; ++ii, ++next) {
+                mZoomFractions[next] = (double) (1 << ii)
+                        / (double) (1 << (ZOOM_STEPS / 2));
+                // System.out.printf("%d %f\n", next, zoomFractions[next]);
+            }
+            for (int ii = 2; ii < 2 + ZOOM_STEPS / 2; ++ii, ++next) {
+                mZoomFractions[next] = (double) ((1 << ii) - 1)
+                        / (double) (1 << ii);
+                // System.out.printf("%d %f\n", next, zoomFractions[next]);
+            }
+        }
+
+        @SuppressWarnings("unused")
+        private void initZoomFractionsWithSinWave() {
+            mZoomFractions = new double[ZOOM_STEPS];
+            for (int ii = 0; ii < ZOOM_STEPS; ++ii) {
+                double offset = Math.PI * ii / ZOOM_STEPS;
+                mZoomFractions[ii] = (Math.sin((1.5 * Math.PI + offset)) + 1.0) / 2.0;
+                // System.out.printf("%d %f\n", ii, zoomFractions[ii]);
+            }
+        }
+
+        public void setRange(double minVal, double maxVal) {
+            mMinDataVal = minVal;
+            mMaxDataVal = maxVal;
+            mScaleInfo.setMinVal(minVal);
+            mScaleInfo.setMaxVal(maxVal);
+        }
+
+        public void setLimitRange(double minVal, double maxVal) {
+            mLimitMinVal = minVal;
+            mLimitMaxVal = maxVal;
+        }
+
+        public void resetScale() {
+            mScaleInfo.setMinVal(mLimitMinVal);
+            mScaleInfo.setMaxVal(mLimitMaxVal);
+        }
+
+        public void setScaleFromHorizontalScrollBar(int selection) {
+            double minVal = mScaleInfo.getMinVal();
+            double maxVal = mScaleInfo.getMaxVal();
+            double visibleRange = maxVal - minVal;
+
+            minVal = mLimitMinVal + selection;
+            maxVal = minVal + visibleRange;
+            if (maxVal > mLimitMaxVal) {
+                maxVal = mLimitMaxVal;
+                minVal = maxVal - visibleRange;
+            }
+            mScaleInfo.setMinVal(minVal);
+            mScaleInfo.setMaxVal(maxVal);
+
+            mGraphicsState = GraphicsState.Scrolling;
+        }
+
+        private void updateHorizontalScrollBar() {
+            double minVal = mScaleInfo.getMinVal();
+            double maxVal = mScaleInfo.getMaxVal();
+            double visibleRange = maxVal - minVal;
+            double fullRange = mLimitMaxVal - mLimitMinVal;
+
+            ScrollBar hBar = getHorizontalBar();
+            if (fullRange > visibleRange) {
+                hBar.setVisible(true);
+                hBar.setMinimum(0);
+                hBar.setMaximum((int)Math.ceil(fullRange));
+                hBar.setThumb((int)Math.ceil(visibleRange));
+                hBar.setSelection((int)Math.floor(minVal - mLimitMinVal));
+            } else {
+                hBar.setVisible(false);
+            }
+        }
+
+        private void draw(Display display, GC gc) {
+            if (mSegments.length == 0) {
+                // gc.setBackground(colorBackground);
+                // gc.fillRectangle(getBounds());
+                return;
+            }
+
+            // Create an image for double-buffering
+            Image image = new Image(display, getBounds());
+
+            // Set up the off-screen gc
+            GC gcImage = new GC(image);
+            if (mSetFonts)
+                gcImage.setFont(mFontRegistry.get("small"));  //$NON-NLS-1$
+
+            // Draw the background
+            // gcImage.setBackground(colorBackground);
+            // gcImage.fillRectangle(image.getBounds());
+
+            if (mGraphicsState == GraphicsState.Scaling) {
+                double diff = mMouse.x - mMouseMarkStartX;
+                if (diff > 0) {
+                    double newMinVal = mScaleMinVal - diff / mScalePixelsPerRange;
+                    if (newMinVal < mLimitMinVal)
+                        newMinVal = mLimitMinVal;
+                    mScaleInfo.setMinVal(newMinVal);
+                    // System.out.printf("diff %f scaleMin %f newMin %f\n",
+                    // diff, scaleMinVal, newMinVal);
+                } else if (diff < 0) {
+                    double newMaxVal = mScaleMaxVal - diff / mScalePixelsPerRange;
+                    if (newMaxVal > mLimitMaxVal)
+                        newMaxVal = mLimitMaxVal;
+                    mScaleInfo.setMaxVal(newMaxVal);
+                    // System.out.printf("diff %f scaleMax %f newMax %f\n",
+                    // diff, scaleMaxVal, newMaxVal);
+                }
+            }
+
+            // Recompute the ticks and strips only if the size has changed,
+            // or we scrolled so that a new row is visible.
+            Point dim = getSize();
+            if (mStartRow != mCachedStartRow || mEndRow != mCachedEndRow
+                    || mScaleInfo.getMinVal() != mCachedMinVal
+                    || mScaleInfo.getMaxVal() != mCachedMaxVal) {
+                mCachedStartRow = mStartRow;
+                mCachedEndRow = mEndRow;
+                int xdim = dim.x - TotalXMargin;
+                mScaleInfo.setNumPixels(xdim);
+                boolean forceEndPoints = (mGraphicsState == GraphicsState.Scaling
+                        || mGraphicsState == GraphicsState.Animating
+                        || mGraphicsState == GraphicsState.Scrolling);
+                mScaleInfo.computeTicks(forceEndPoints);
+                mCachedMinVal = mScaleInfo.getMinVal();
+                mCachedMaxVal = mScaleInfo.getMaxVal();
+                if (mLimitMinVal > mScaleInfo.getMinVal())
+                    mLimitMinVal = mScaleInfo.getMinVal();
+                if (mLimitMaxVal < mScaleInfo.getMaxVal())
+                    mLimitMaxVal = mScaleInfo.getMaxVal();
+
+                // Compute the strips
+                computeStrips();
+
+                // Update the horizontal scrollbar.
+                updateHorizontalScrollBar();
+            }
+
+            if (mNumRows > 2) {
+                // Draw the row background stripes
+                gcImage.setBackground(mColorRowBack);
+                for (int ii = 1; ii < mNumRows; ii += 2) {
+                    RowData rd = mRows[ii];
+                    int y1 = rd.mRank * rowYSpace - mScrollOffsetY;
+                    gcImage.fillRectangle(0, y1, dim.x, rowYSpace);
+                }
+            }
+
+            if (drawingSelection()) {
+                drawSelection(display, gcImage);
+            }
+
+            String blockName = null;
+            Color blockColor = null;
+            String blockDetails = null;
+
+            if (mDebug) {
+                double pixelsPerRange = mScaleInfo.getPixelsPerRange();
+                System.out
+                        .printf(
+                                "dim.x %d pixels %d minVal %f, maxVal %f ppr %f rpp %f\n",
+                                dim.x, dim.x - TotalXMargin, mScaleInfo
+                                        .getMinVal(), mScaleInfo.getMaxVal(),
+                                pixelsPerRange, 1.0 / pixelsPerRange);
+            }
+
+            // Draw the strips
+            Block selectBlock = null;
+            for (Strip strip : mStripList) {
+                if (strip.mColor == null) {
+                    // System.out.printf("strip.color is null\n");
+                    continue;
+                }
+                gcImage.setBackground(strip.mColor);
+                gcImage.fillRectangle(strip.mX, strip.mY - mScrollOffsetY, strip.mWidth,
+                        strip.mHeight);
+                if (mMouseRow == strip.mRowData.mRank) {
+                    if (mMouse.x >= strip.mX
+                            && mMouse.x < strip.mX + strip.mWidth) {
+                        Block block = strip.mSegment.mBlock;
+                        blockName = block.getName();
+                        blockColor = strip.mColor;
+                        if (mHaveCpuTime) {
+                            if (mHaveRealTime) {
+                                blockDetails = String.format(
+                                        "excl cpu %s, incl cpu %s, "
+                                        + "excl real %s, incl real %s",
+                                        mUnits.labelledString(block.getExclusiveCpuTime()),
+                                        mUnits.labelledString(block.getInclusiveCpuTime()),
+                                        mUnits.labelledString(block.getExclusiveRealTime()),
+                                        mUnits.labelledString(block.getInclusiveRealTime()));
+                            } else {
+                                blockDetails = String.format(
+                                        "excl cpu %s, incl cpu %s",
+                                        mUnits.labelledString(block.getExclusiveCpuTime()),
+                                        mUnits.labelledString(block.getInclusiveCpuTime()));
+                            }
+                        } else {
+                            blockDetails = String.format(
+                                    "excl real %s, incl real %s",
+                                    mUnits.labelledString(block.getExclusiveRealTime()),
+                                    mUnits.labelledString(block.getInclusiveRealTime()));
+                        }
+                    }
+                    if (mMouseSelect.x >= strip.mX
+                            && mMouseSelect.x < strip.mX + strip.mWidth) {
+                        selectBlock = strip.mSegment.mBlock;
+                    }
+                }
+            }
+            mMouseSelect.x = 0;
+            mMouseSelect.y = 0;
+
+            if (selectBlock != null) {
+                ArrayList<Selection> selections = new ArrayList<Selection>();
+                // Get the row label
+                RowData rd = mRows[mMouseRow];
+                selections.add(Selection.highlight("Thread", rd.mName));  //$NON-NLS-1$
+                selections.add(Selection.highlight("Call", selectBlock));  //$NON-NLS-1$
+
+                int mouseX = mMouse.x - LeftMargin;
+                double mouseXval = mScaleInfo.pixelToValue(mouseX);
+                selections.add(Selection.highlight("Time", mouseXval));  //$NON-NLS-1$
+
+                mSelectionController.change(selections, "TimeLineView");  //$NON-NLS-1$
+                mHighlightMethodData = null;
+                mHighlightCall = (Call) selectBlock;
+                startHighlighting();
+            }
+
+            // Draw a highlight box on the row where the mouse is.
+            // Except don't draw the box if we are animating the
+            // highlighing of a call or method because the inclusive
+            // highlight bar passes through the highlight box and
+            // causes an annoying flashing artifact.
+            if (mMouseRow >= 0 && mMouseRow < mNumRows && mHighlightStep == 0) {
+                gcImage.setForeground(mColorGray);
+                int y1 = mMouseRow * rowYSpace - mScrollOffsetY;
+                gcImage.drawLine(0, y1, dim.x, y1);
+                gcImage.drawLine(0, y1 + rowYSpace, dim.x, y1 + rowYSpace);
+            }
+
+            // Highlight a selected method, if any
+            drawHighlights(gcImage, dim);
+
+            // Draw a vertical line where the mouse is.
+            gcImage.setForeground(mColorDarkGray);
+            int lineEnd = Math.min(dim.y, mNumRows * rowYSpace);
+            gcImage.drawLine(mMouse.x, 0, mMouse.x, lineEnd);
+
+            if (blockName != null) {
+                mTimescale.setMethodName(blockName);
+                mTimescale.setMethodColor(blockColor);
+                mTimescale.setDetails(blockDetails);
+                mShowHighlightName = false;
+            } else if (mShowHighlightName) {
+                // Draw the highlighted method name
+                MethodData md = mHighlightMethodData;
+                if (md == null && mHighlightCall != null)
+                    md = mHighlightCall.getMethodData();
+                if (md == null)
+                    System.out.printf("null highlight?\n");  //$NON-NLS-1$
+                if (md != null) {
+                    mTimescale.setMethodName(md.getProfileName());
+                    mTimescale.setMethodColor(md.getColor());
+                    mTimescale.setDetails(null);
+                }
+            } else {
+                mTimescale.setMethodName(null);
+                mTimescale.setMethodColor(null);
+                mTimescale.setDetails(null);
+            }
+            mTimescale.redraw();
+
+            // Draw the off-screen buffer to the screen
+            gc.drawImage(image, 0, 0);
+
+            // Clean up
+            image.dispose();
+            gcImage.dispose();
+        }
+
+        private void drawHighlights(GC gc, Point dim) {
+            int height = mHighlightHeight;
+            if (height <= 0)
+                return;
+            for (Range range : mHighlightExclusive) {
+                gc.setBackground(range.mColor);
+                int xStart = range.mXdim.x;
+                int width = range.mXdim.y;
+                gc.fillRectangle(xStart, range.mY - height - mScrollOffsetY, width, height);
+            }
+
+            // Draw the inclusive lines a bit shorter
+            height -= 1;
+            if (height <= 0)
+                height = 1;
+
+            // Highlight the inclusive ranges
+            gc.setForeground(mColorDarkGray);
+            gc.setBackground(mColorDarkGray);
+            for (Range range : mHighlightInclusive) {
+                int x1 = range.mXdim.x;
+                int x2 = range.mXdim.y;
+                boolean drawLeftEnd = false;
+                boolean drawRightEnd = false;
+                if (x1 >= LeftMargin)
+                    drawLeftEnd = true;
+                else
+                    x1 = LeftMargin;
+                if (x2 >= LeftMargin)
+                    drawRightEnd = true;
+                else
+                    x2 = dim.x - RightMargin;
+                int y1 = range.mY + rowHeight + 2 - mScrollOffsetY;
+
+                // If the range is very narrow, then just draw a small
+                // rectangle.
+                if (x2 - x1 < MinInclusiveRange) {
+                    int width = x2 - x1;
+                    if (width < 2)
+                        width = 2;
+                    gc.fillRectangle(x1, y1, width, height);
+                    continue;
+                }
+                if (drawLeftEnd) {
+                    if (drawRightEnd) {
+                        // Draw both ends
+                        int[] points = { x1, y1, x1, y1 + height, x2,
+                                y1 + height, x2, y1 };
+                        gc.drawPolyline(points);
+                    } else {
+                        // Draw the left end
+                        int[] points = { x1, y1, x1, y1 + height, x2,
+                                y1 + height };
+                        gc.drawPolyline(points);
+                    }
+                } else {
+                    if (drawRightEnd) {
+                        // Draw the right end
+                        int[] points = { x1, y1 + height, x2, y1 + height, x2,
+                                y1 };
+                        gc.drawPolyline(points);
+                    } else {
+                        // Draw neither end, just the line
+                        int[] points = { x1, y1 + height, x2, y1 + height };
+                        gc.drawPolyline(points);
+                    }
+                }
+
+                // Draw the arrowheads, if necessary
+                if (drawLeftEnd == false) {
+                    int[] points = { x1 + 7, y1 + height - 4, x1, y1 + height,
+                            x1 + 7, y1 + height + 4 };
+                    gc.fillPolygon(points);
+                }
+                if (drawRightEnd == false) {
+                    int[] points = { x2 - 7, y1 + height - 4, x2, y1 + height,
+                            x2 - 7, y1 + height + 4 };
+                    gc.fillPolygon(points);
+                }
+            }
+        }
+
+        private boolean drawingSelection() {
+            return mGraphicsState == GraphicsState.Marking
+                    || mGraphicsState == GraphicsState.Animating;
+        }
+
+        private void drawSelection(Display display, GC gc) {
+            Point dim = getSize();
+            gc.setForeground(mColorGray);
+            gc.drawLine(mMouseMarkStartX, 0, mMouseMarkStartX, dim.y);
+            gc.setBackground(mColorZoomSelection);
+            int width;
+            int mouseX = (mGraphicsState == GraphicsState.Animating) ? mMouseMarkEndX : mMouse.x;
+            int x;
+            if (mMouseMarkStartX < mouseX) {
+                x = mMouseMarkStartX;
+                width = mouseX - mMouseMarkStartX;
+            } else {
+                x = mouseX;
+                width = mMouseMarkStartX - mouseX;
+            }
+            gc.fillRectangle(x, 0, width, dim.y);
+        }
+
+        private void computeStrips() {
+            double minVal = mScaleInfo.getMinVal();
+            double maxVal = mScaleInfo.getMaxVal();
+
+            // Allocate space for the pixel data
+            Pixel[] pixels = new Pixel[mNumRows];
+            for (int ii = 0; ii < mNumRows; ++ii)
+                pixels[ii] = new Pixel();
+
+            // Clear the per-block pixel data
+            for (int ii = 0; ii < mSegments.length; ++ii) {
+                mSegments[ii].mBlock.clearWeight();
+            }
+
+            mStripList.clear();
+            mHighlightExclusive.clear();
+            mHighlightInclusive.clear();
+            MethodData callMethod = null;
+            long callStart = 0;
+            long callEnd = -1;
+            RowData callRowData = null;
+            int prevMethodStart = -1;
+            int prevMethodEnd = -1;
+            int prevCallStart = -1;
+            int prevCallEnd = -1;
+            if (mHighlightCall != null) {
+                int callPixelStart = -1;
+                int callPixelEnd = -1;
+                callStart = mHighlightCall.getStartTime();
+                callEnd = mHighlightCall.getEndTime();
+                callMethod = mHighlightCall.getMethodData();
+                if (callStart >= minVal)
+                    callPixelStart = mScaleInfo.valueToPixel(callStart);
+                if (callEnd <= maxVal)
+                    callPixelEnd = mScaleInfo.valueToPixel(callEnd);
+                // System.out.printf("callStart,End %d,%d minVal,maxVal %f,%f
+                // callPixelStart,End %d,%d\n",
+                // callStart, callEnd, minVal, maxVal, callPixelStart,
+                // callPixelEnd);
+                int threadId = mHighlightCall.getThreadId();
+                String threadName = mThreadLabels.get(threadId);
+                callRowData = mRowByName.get(threadName);
+                int y1 = callRowData.mRank * rowYSpace + rowYMarginHalf;
+                Color color = callMethod.getColor();
+                mHighlightInclusive.add(new Range(callPixelStart + LeftMargin,
+                        callPixelEnd + LeftMargin, y1, color));
+            }
+            for (Segment segment : mSegments) {
+                if (segment.mEndTime <= minVal)
+                    continue;
+                if (segment.mStartTime >= maxVal)
+                    continue;
+
+                Block block = segment.mBlock;
+
+                // Skip over blocks that were not assigned a color, including the
+                // top level block and others that have zero inclusive time.
+                Color color = block.getColor();
+                if (color == null)
+                    continue;
+
+                double recordStart = Math.max(segment.mStartTime, minVal);
+                double recordEnd = Math.min(segment.mEndTime, maxVal);
+                if (recordStart == recordEnd)
+                    continue;
+                int pixelStart = mScaleInfo.valueToPixel(recordStart);
+                int pixelEnd = mScaleInfo.valueToPixel(recordEnd);
+                int width = pixelEnd - pixelStart;
+                boolean isContextSwitch = segment.mIsContextSwitch;
+
+                RowData rd = segment.mRowData;
+                MethodData md = block.getMethodData();
+
+                // We will add the scroll offset later when we draw the strips
+                int y1 = rd.mRank * rowYSpace + rowYMarginHalf;
+
+                // If we can't display any more rows, then quit
+                if (rd.mRank > mEndRow)
+                    break;
+
+                // System.out.printf("segment %s val: [%.1f, %.1f] frac [%f, %f]
+                // pixel: [%d, %d] pix.start %d weight %.2f %s\n",
+                // block.getName(), recordStart, recordEnd,
+                // scaleInfo.valueToPixelFraction(recordStart),
+                // scaleInfo.valueToPixelFraction(recordEnd),
+                // pixelStart, pixelEnd, pixels[rd.rank].start,
+                // pixels[rd.rank].maxWeight,
+                // pixels[rd.rank].segment != null
+                // ? pixels[rd.rank].segment.block.getName()
+                // : "null");
+
+                if (mHighlightMethodData != null) {
+                    if (mHighlightMethodData == md) {
+                        if (prevMethodStart != pixelStart || prevMethodEnd != pixelEnd) {
+                            prevMethodStart = pixelStart;
+                            prevMethodEnd = pixelEnd;
+                            int rangeWidth = width;
+                            if (rangeWidth == 0)
+                                rangeWidth = 1;
+                            mHighlightExclusive.add(new Range(pixelStart
+                                    + LeftMargin, rangeWidth, y1, color));
+                            callStart = block.getStartTime();
+                            int callPixelStart = -1;
+                            if (callStart >= minVal)
+                                callPixelStart = mScaleInfo.valueToPixel(callStart);
+                            int callPixelEnd = -1;
+                            callEnd = block.getEndTime();
+                            if (callEnd <= maxVal)
+                                callPixelEnd = mScaleInfo.valueToPixel(callEnd);
+                            if (prevCallStart != callPixelStart || prevCallEnd != callPixelEnd) {
+                                prevCallStart = callPixelStart;
+                                prevCallEnd = callPixelEnd;
+                                mHighlightInclusive.add(new Range(
+                                        callPixelStart + LeftMargin,
+                                        callPixelEnd + LeftMargin, y1, color));
+                            }
+                        }
+                    } else if (mFadeColors) {
+                        color = md.getFadedColor();
+                    }
+                } else if (mHighlightCall != null) {
+                    if (segment.mStartTime >= callStart
+                            && segment.mEndTime <= callEnd && callMethod == md
+                            && callRowData == rd) {
+                        if (prevMethodStart != pixelStart || prevMethodEnd != pixelEnd) {
+                            prevMethodStart = pixelStart;
+                            prevMethodEnd = pixelEnd;
+                            int rangeWidth = width;
+                            if (rangeWidth == 0)
+                                rangeWidth = 1;
+                            mHighlightExclusive.add(new Range(pixelStart
+                                    + LeftMargin, rangeWidth, y1, color));
+                        }
+                    } else if (mFadeColors) {
+                        color = md.getFadedColor();
+                    }
+                }
+
+                // Cases:
+                // 1. This segment starts on a different pixel than the
+                // previous segment started on. In this case, emit
+                // the pixel strip, if any, and:
+                // A. If the width is 0, then add this segment's
+                // weight to the Pixel.
+                // B. If the width > 0, then emit a strip for this
+                // segment (no partial Pixel data).
+                //
+                // 2. Otherwise (the new segment starts on the same
+                // pixel as the previous segment): add its "weight"
+                // to the current pixel, and:
+                // A. If the new segment has width 1,
+                // then emit the pixel strip and then
+                // add the segment's weight to the pixel.
+                // B. If the new segment has width > 1,
+                // then emit the pixel strip, and emit the rest
+                // of the strip for this segment (no partial Pixel
+                // data).
+
+                Pixel pix = pixels[rd.mRank];
+                if (pix.mStart != pixelStart) {
+                    if (pix.mSegment != null) {
+                        // Emit the pixel strip. This also clears the pixel.
+                        emitPixelStrip(rd, y1, pix);
+                    }
+
+                    if (width == 0) {
+                        // Compute the "weight" of this segment for the first
+                        // pixel. For a pixel N, the "weight" of a segment is
+                        // how much of the region [N - 0.5, N + 0.5] is covered
+                        // by the segment.
+                        double weight = computeWeight(recordStart, recordEnd,
+                                isContextSwitch, pixelStart);
+                        weight = block.addWeight(pixelStart, rd.mRank, weight);
+                        if (weight > pix.mMaxWeight) {
+                            pix.setFields(pixelStart, weight, segment, color,
+                                    rd);
+                        }
+                    } else {
+                        int x1 = pixelStart + LeftMargin;
+                        Strip strip = new Strip(
+                                x1, isContextSwitch ? y1 + rowHeight - 1 : y1,
+                                width, isContextSwitch ? 1 : rowHeight,
+                                rd, segment, color);
+                        mStripList.add(strip);
+                    }
+                } else {
+                    double weight = computeWeight(recordStart, recordEnd,
+                            isContextSwitch, pixelStart);
+                    weight = block.addWeight(pixelStart, rd.mRank, weight);
+                    if (weight > pix.mMaxWeight) {
+                        pix.setFields(pixelStart, weight, segment, color, rd);
+                    }
+                    if (width == 1) {
+                        // Emit the pixel strip. This also clears the pixel.
+                        emitPixelStrip(rd, y1, pix);
+
+                        // Compute the weight for the next pixel
+                        pixelStart += 1;
+                        weight = computeWeight(recordStart, recordEnd,
+                                isContextSwitch, pixelStart);
+                        weight = block.addWeight(pixelStart, rd.mRank, weight);
+                        pix.setFields(pixelStart, weight, segment, color, rd);
+                    } else if (width > 1) {
+                        // Emit the pixel strip. This also clears the pixel.
+                        emitPixelStrip(rd, y1, pix);
+
+                        // Emit a strip for the rest of the segment.
+                        pixelStart += 1;
+                        width -= 1;
+                        int x1 = pixelStart + LeftMargin;
+                        Strip strip = new Strip(
+                                x1, isContextSwitch ? y1 + rowHeight - 1 : y1,
+                                width, isContextSwitch ? 1 : rowHeight,
+                                rd,segment, color);
+                        mStripList.add(strip);
+                    }
+                }
+            }
+
+            // Emit the last pixels of each row, if any
+            for (int ii = 0; ii < mNumRows; ++ii) {
+                Pixel pix = pixels[ii];
+                if (pix.mSegment != null) {
+                    RowData rd = pix.mRowData;
+                    int y1 = rd.mRank * rowYSpace + rowYMarginHalf;
+                    // Emit the pixel strip. This also clears the pixel.
+                    emitPixelStrip(rd, y1, pix);
+                }
+            }
+
+            if (false) {
+                System.out.printf("computeStrips()\n");
+                for (Strip strip : mStripList) {
+                    System.out.printf("%3d, %3d width %3d height %d %s\n",
+                            strip.mX, strip.mY, strip.mWidth, strip.mHeight,
+                            strip.mSegment.mBlock.getName());
+                }
+            }
+        }
+
+        private double computeWeight(double start, double end,
+                boolean isContextSwitch, int pixel) {
+            if (isContextSwitch) {
+                return 0;
+            }
+            double pixelStartFraction = mScaleInfo.valueToPixelFraction(start);
+            double pixelEndFraction = mScaleInfo.valueToPixelFraction(end);
+            double leftEndPoint = Math.max(pixelStartFraction, pixel - 0.5);
+            double rightEndPoint = Math.min(pixelEndFraction, pixel + 0.5);
+            double weight = rightEndPoint - leftEndPoint;
+            return weight;
+        }
+
+        private void emitPixelStrip(RowData rd, int y, Pixel pixel) {
+            Strip strip;
+
+            if (pixel.mSegment == null)
+                return;
+
+            int x = pixel.mStart + LeftMargin;
+            // Compute the percentage of the row height proportional to
+            // the weight of this pixel. But don't let the proportion
+            // exceed 3/4 of the row height so that we can easily see
+            // if a given time range includes more than one method.
+            int height = (int) (pixel.mMaxWeight * rowHeight * 0.75);
+            if (height < mMinStripHeight)
+                height = mMinStripHeight;
+            int remainder = rowHeight - height;
+            if (remainder > 0) {
+                strip = new Strip(x, y, 1, remainder, rd, pixel.mSegment,
+                        mFadeColors ? mColorGray : mColorBlack);
+                mStripList.add(strip);
+                // System.out.printf("emitPixel (%d, %d) height %d black\n",
+                // x, y, remainder);
+            }
+            strip = new Strip(x, y + remainder, 1, height, rd, pixel.mSegment,
+                    pixel.mColor);
+            mStripList.add(strip);
+            // System.out.printf("emitPixel (%d, %d) height %d %s\n",
+            // x, y + remainder, height, pixel.segment.block.getName());
+            pixel.mSegment = null;
+            pixel.mMaxWeight = 0.0;
+        }
+
+        private void mouseMove(MouseEvent me) {
+            if (false) {
+                if (mHighlightMethodData != null) {
+                    mHighlightMethodData = null;
+                    // Force a recomputation of the strip colors
+                    mCachedEndRow = -1;
+                }
+            }
+            Point dim = mSurface.getSize();
+            int x = me.x;
+            if (x < LeftMargin)
+                x = LeftMargin;
+            if (x > dim.x - RightMargin)
+                x = dim.x - RightMargin;
+            mMouse.x = x;
+            mMouse.y = me.y;
+            mTimescale.setVbarPosition(x);
+            if (mGraphicsState == GraphicsState.Marking) {
+                mTimescale.setMarkEnd(x);
+            }
+
+            if (mGraphicsState == GraphicsState.Normal) {
+                // Set the cursor to the normal state.
+                mSurface.setCursor(mNormalCursor);
+            } else if (mGraphicsState == GraphicsState.Marking) {
+                // Make the cursor point in the direction of the sweep
+                if (mMouse.x >= mMouseMarkStartX)
+                    mSurface.setCursor(mIncreasingCursor);
+                else
+                    mSurface.setCursor(mDecreasingCursor);
+            }
+            int rownum = (mMouse.y + mScrollOffsetY) / rowYSpace;
+            if (me.y < 0 || me.y >= dim.y) {
+                rownum = -1;
+            }
+            if (mMouseRow != rownum) {
+                mMouseRow = rownum;
+                mLabels.redraw();
+            }
+            redraw();
+        }
+
+        private void mouseDown(MouseEvent me) {
+            Point dim = mSurface.getSize();
+            int x = me.x;
+            if (x < LeftMargin)
+                x = LeftMargin;
+            if (x > dim.x - RightMargin)
+                x = dim.x - RightMargin;
+            mMouseMarkStartX = x;
+            mGraphicsState = GraphicsState.Marking;
+            mSurface.setCursor(mIncreasingCursor);
+            mTimescale.setMarkStart(mMouseMarkStartX);
+            mTimescale.setMarkEnd(mMouseMarkStartX);
+            redraw();
+        }
+
+        private void mouseUp(MouseEvent me) {
+            mSurface.setCursor(mNormalCursor);
+            if (mGraphicsState != GraphicsState.Marking) {
+                mGraphicsState = GraphicsState.Normal;
+                return;
+            }
+            mGraphicsState = GraphicsState.Animating;
+            Point dim = mSurface.getSize();
+
+            // If the user released the mouse outside the drawing area then
+            // cancel the zoom.
+            if (me.y <= 0 || me.y >= dim.y) {
+                mGraphicsState = GraphicsState.Normal;
+                redraw();
+                return;
+            }
+
+            int x = me.x;
+            if (x < LeftMargin)
+                x = LeftMargin;
+            if (x > dim.x - RightMargin)
+                x = dim.x - RightMargin;
+            mMouseMarkEndX = x;
+
+            // If the user clicked and released the mouse at the same point
+            // (+/- a pixel or two) then cancel the zoom (but select the
+            // method).
+            int dist = mMouseMarkEndX - mMouseMarkStartX;
+            if (dist < 0)
+                dist = -dist;
+            if (dist <= 2) {
+                mGraphicsState = GraphicsState.Normal;
+
+                // Select the method underneath the mouse
+                mMouseSelect.x = mMouseMarkStartX;
+                mMouseSelect.y = me.y;
+                redraw();
+                return;
+            }
+
+            // Make mouseEndX be the higher end point
+            if (mMouseMarkEndX < mMouseMarkStartX) {
+                int temp = mMouseMarkEndX;
+                mMouseMarkEndX = mMouseMarkStartX;
+                mMouseMarkStartX = temp;
+            }
+
+            // If the zoom area is the whole window (or nearly the whole
+            // window) then cancel the zoom.
+            if (mMouseMarkStartX <= LeftMargin + MinZoomPixelMargin
+                    && mMouseMarkEndX >= dim.x - RightMargin - MinZoomPixelMargin) {
+                mGraphicsState = GraphicsState.Normal;
+                redraw();
+                return;
+            }
+
+            // Compute some variables needed for zooming.
+            // It's probably easiest to explain by an example. There
+            // are two scales (or dimensions) involved: one for the pixels
+            // and one for the values (microseconds). To keep the example
+            // simple, suppose we have pixels in the range [0,16] and
+            // values in the range [100, 260], and suppose the user
+            // selects a zoom window from pixel 4 to pixel 8.
+            //
+            // usec: 100 140 180 260
+            // |-------|ZZZZZZZ|---------------|
+            // pixel: 0 4 8 16
+            //
+            // I've drawn the pixels starting at zero for simplicity, but
+            // in fact the drawable area is offset from the left margin
+            // by the value of "LeftMargin".
+            //
+            // The "pixels-per-range" (ppr) in this case is 0.1 (a tenth of
+            // a pixel per usec). What we want is to redraw the screen in
+            // several steps, each time increasing the zoom window until the
+            // zoom window fills the screen. For simplicity, assume that
+            // we want to zoom in four equal steps. Then the snapshots
+            // of the screen at each step would look something like this:
+            //
+            // usec: 100 140 180 260
+            // |-------|ZZZZZZZ|---------------|
+            // pixel: 0 4 8 16
+            //
+            // usec: ? 140 180 ?
+            // |-----|ZZZZZZZZZZZZZ|-----------|
+            // pixel: 0 3 10 16
+            //
+            // usec: ? 140 180 ?
+            // |---|ZZZZZZZZZZZZZZZZZZZ|-------|
+            // pixel: 0 2 12 16
+            //
+            // usec: ?140 180 ?
+            // |-|ZZZZZZZZZZZZZZZZZZZZZZZZZ|---|
+            // pixel: 0 1 14 16
+            //
+            // usec: 140 180
+            // |ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ|
+            // pixel: 0 16
+            //
+            // The problem is how to compute the endpoints (denoted by ?)
+            // for each step. This is a little tricky. We first need to
+            // compute the "fixed point": this is the point in the selection
+            // that doesn't move left or right. Then we can recompute the
+            // "ppr" (pixels per range) at each step and then find the
+            // endpoints. The computation of the end points is done
+            // in animateZoom(). This method computes the fixed point
+            // and some other variables needed in animateZoom().
+
+            double minVal = mScaleInfo.getMinVal();
+            double maxVal = mScaleInfo.getMaxVal();
+            double ppr = mScaleInfo.getPixelsPerRange();
+            mZoomMin = minVal + ((mMouseMarkStartX - LeftMargin) / ppr);
+            mZoomMax = minVal + ((mMouseMarkEndX - LeftMargin) / ppr);
+
+            // Clamp the min and max values to the actual data min and max
+            if (mZoomMin < mMinDataVal)
+                mZoomMin = mMinDataVal;
+            if (mZoomMax > mMaxDataVal)
+                mZoomMax = mMaxDataVal;
+
+            // Snap the min and max points to the grid determined by the
+            // TickScaler
+            // before we zoom.
+            int xdim = dim.x - TotalXMargin;
+            TickScaler scaler = new TickScaler(mZoomMin, mZoomMax, xdim,
+                    PixelsPerTick);
+            scaler.computeTicks(false);
+            mZoomMin = scaler.getMinVal();
+            mZoomMax = scaler.getMaxVal();
+
+            // Also snap the mouse points (in pixel space) to be consistent with
+            // zoomMin and zoomMax (in value space).
+            mMouseMarkStartX = (int) ((mZoomMin - minVal) * ppr + LeftMargin);
+            mMouseMarkEndX = (int) ((mZoomMax - minVal) * ppr + LeftMargin);
+            mTimescale.setMarkStart(mMouseMarkStartX);
+            mTimescale.setMarkEnd(mMouseMarkEndX);
+
+            // Compute the mouse selection end point distances
+            mMouseEndDistance = dim.x - RightMargin - mMouseMarkEndX;
+            mMouseStartDistance = mMouseMarkStartX - LeftMargin;
+            mZoomMouseStart = mMouseMarkStartX;
+            mZoomMouseEnd = mMouseMarkEndX;
+            mZoomStep = 0;
+
+            // Compute the fixed point in both value space and pixel space.
+            mMin2ZoomMin = mZoomMin - minVal;
+            mZoomMax2Max = maxVal - mZoomMax;
+            mZoomFixed = mZoomMin + (mZoomMax - mZoomMin) * mMin2ZoomMin
+                    / (mMin2ZoomMin + mZoomMax2Max);
+            mZoomFixedPixel = (mZoomFixed - minVal) * ppr + LeftMargin;
+            mFixedPixelStartDistance = mZoomFixedPixel - LeftMargin;
+            mFixedPixelEndDistance = dim.x - RightMargin - mZoomFixedPixel;
+
+            mZoomMin2Fixed = mZoomFixed - mZoomMin;
+            mFixed2ZoomMax = mZoomMax - mZoomFixed;
+
+            getDisplay().timerExec(ZOOM_TIMER_INTERVAL, mZoomAnimator);
+            redraw();
+            update();
+        }
+
+        private void mouseScrolled(MouseEvent me) {
+            mGraphicsState = GraphicsState.Scrolling;
+            double tMin = mScaleInfo.getMinVal();
+            double tMax = mScaleInfo.getMaxVal();
+            double zoomFactor = 2;
+            double tMinRef = mLimitMinVal;
+            double tMaxRef = mLimitMaxVal;
+            double t; // the fixed point
+            double tMinNew;
+            double tMaxNew;
+            if (me.count > 0) {
+                // we zoom in
+                Point dim = mSurface.getSize();
+                int x = me.x;
+                if (x < LeftMargin)
+                    x = LeftMargin;
+                if (x > dim.x - RightMargin)
+                    x = dim.x - RightMargin;
+                double ppr = mScaleInfo.getPixelsPerRange();
+                t = tMin + ((x - LeftMargin) / ppr);
+                tMinNew = Math.max(tMinRef, t - (t - tMin) / zoomFactor);
+                tMaxNew = Math.min(tMaxRef, t + (tMax - t) / zoomFactor);
+            } else {
+                // we zoom out
+                double factor = (tMax - tMin) / (tMaxRef - tMinRef);
+                if (factor < 1) {
+                    t = (factor * tMinRef - tMin) / (factor - 1);
+                    tMinNew = Math.max(tMinRef, t - zoomFactor * (t - tMin));
+                    tMaxNew = Math.min(tMaxRef, t + zoomFactor * (tMax - t));
+                } else {
+                    return;
+                }
+            }
+            mScaleInfo.setMinVal(tMinNew);
+            mScaleInfo.setMaxVal(tMaxNew);
+            mSurface.redraw();
+        }
+
+        // No defined behavior yet for double-click.
+        private void mouseDoubleClick(MouseEvent me) {
+        }
+
+        public void startScaling(int mouseX) {
+            Point dim = mSurface.getSize();
+            int x = mouseX;
+            if (x < LeftMargin)
+                x = LeftMargin;
+            if (x > dim.x - RightMargin)
+                x = dim.x - RightMargin;
+            mMouseMarkStartX = x;
+            mGraphicsState = GraphicsState.Scaling;
+            mScalePixelsPerRange = mScaleInfo.getPixelsPerRange();
+            mScaleMinVal = mScaleInfo.getMinVal();
+            mScaleMaxVal = mScaleInfo.getMaxVal();
+        }
+
+        public void stopScaling(int mouseX) {
+            mGraphicsState = GraphicsState.Normal;
+        }
+
+        private void animateHighlight() {
+            mHighlightStep += 1;
+            if (mHighlightStep >= HIGHLIGHT_STEPS) {
+                mFadeColors = false;
+                mHighlightStep = 0;
+                // Force a recomputation of the strip colors
+                mCachedEndRow = -1;
+            } else {
+                mFadeColors = true;
+                mShowHighlightName = true;
+                mHighlightHeight = highlightHeights[mHighlightStep];
+                getDisplay().timerExec(HIGHLIGHT_TIMER_INTERVAL, mHighlightAnimator);
+            }
+            redraw();
+        }
+
+        private void clearHighlights() {
+            // System.out.printf("clearHighlights()\n");
+            mShowHighlightName = false;
+            mHighlightHeight = 0;
+            mHighlightMethodData = null;
+            mHighlightCall = null;
+            mFadeColors = false;
+            mHighlightStep = 0;
+            // Force a recomputation of the strip colors
+            mCachedEndRow = -1;
+            redraw();
+        }
+
+        private void animateZoom() {
+            mZoomStep += 1;
+            if (mZoomStep > ZOOM_STEPS) {
+                mGraphicsState = GraphicsState.Normal;
+                // Force a normal recomputation
+                mCachedMinVal = mScaleInfo.getMinVal() + 1;
+            } else if (mZoomStep == ZOOM_STEPS) {
+                mScaleInfo.setMinVal(mZoomMin);
+                mScaleInfo.setMaxVal(mZoomMax);
+                mMouseMarkStartX = LeftMargin;
+                Point dim = getSize();
+                mMouseMarkEndX = dim.x - RightMargin;
+                mTimescale.setMarkStart(mMouseMarkStartX);
+                mTimescale.setMarkEnd(mMouseMarkEndX);
+                getDisplay().timerExec(ZOOM_TIMER_INTERVAL, mZoomAnimator);
+            } else {
+                // Zoom in slowly at first, then speed up, then slow down.
+                // The zoom fractions are precomputed to save time.
+                double fraction = mZoomFractions[mZoomStep];
+                mMouseMarkStartX = (int) (mZoomMouseStart - fraction * mMouseStartDistance);
+                mMouseMarkEndX = (int) (mZoomMouseEnd + fraction * mMouseEndDistance);
+                mTimescale.setMarkStart(mMouseMarkStartX);
+                mTimescale.setMarkEnd(mMouseMarkEndX);
+
+                // Compute the new pixels-per-range. Avoid division by zero.
+                double ppr;
+                if (mZoomMin2Fixed >= mFixed2ZoomMax)
+                    ppr = (mZoomFixedPixel - mMouseMarkStartX) / mZoomMin2Fixed;
+                else
+                    ppr = (mMouseMarkEndX - mZoomFixedPixel) / mFixed2ZoomMax;
+                double newMin = mZoomFixed - mFixedPixelStartDistance / ppr;
+                double newMax = mZoomFixed + mFixedPixelEndDistance / ppr;
+                mScaleInfo.setMinVal(newMin);
+                mScaleInfo.setMaxVal(newMax);
+
+                getDisplay().timerExec(ZOOM_TIMER_INTERVAL, mZoomAnimator);
+            }
+            redraw();
+        }
+
+        private static final int TotalXMargin = LeftMargin + RightMargin;
+        private static final int yMargin = 1; // blank space on top
+        // The minimum margin on each side of the zoom window, in pixels.
+        private static final int MinZoomPixelMargin = 10;
+        private GraphicsState mGraphicsState = GraphicsState.Normal;
+        private Point mMouse = new Point(LeftMargin, 0);
+        private int mMouseMarkStartX;
+        private int mMouseMarkEndX;
+        private boolean mDebug = false;
+        private ArrayList<Strip> mStripList = new ArrayList<Strip>();
+        private ArrayList<Range> mHighlightExclusive = new ArrayList<Range>();
+        private ArrayList<Range> mHighlightInclusive = new ArrayList<Range>();
+        private int mMinStripHeight = 2;
+        private double mCachedMinVal;
+        private double mCachedMaxVal;
+        private int mCachedStartRow;
+        private int mCachedEndRow;
+        private double mScalePixelsPerRange;
+        private double mScaleMinVal;
+        private double mScaleMaxVal;
+        private double mLimitMinVal;
+        private double mLimitMaxVal;
+        private double mMinDataVal;
+        private double mMaxDataVal;
+        private Cursor mNormalCursor;
+        private Cursor mIncreasingCursor;
+        private Cursor mDecreasingCursor;
+        private static final int ZOOM_TIMER_INTERVAL = 10;
+        private static final int HIGHLIGHT_TIMER_INTERVAL = 50;
+        private static final int ZOOM_STEPS = 8; // must be even
+        private int mHighlightHeight = 4;
+        private final int[] highlightHeights = { 0, 2, 4, 5, 6, 5, 4, 2, 4, 5,
+                6 };
+        private final int HIGHLIGHT_STEPS = highlightHeights.length;
+        private boolean mFadeColors;
+        private boolean mShowHighlightName;
+        private double[] mZoomFractions;
+        private int mZoomStep;
+        private int mZoomMouseStart;
+        private int mZoomMouseEnd;
+        private int mMouseStartDistance;
+        private int mMouseEndDistance;
+        private Point mMouseSelect = new Point(0, 0);
+        private double mZoomFixed;
+        private double mZoomFixedPixel;
+        private double mFixedPixelStartDistance;
+        private double mFixedPixelEndDistance;
+        private double mZoomMin2Fixed;
+        private double mMin2ZoomMin;
+        private double mFixed2ZoomMax;
+        private double mZoomMax2Max;
+        private double mZoomMin;
+        private double mZoomMax;
+        private Runnable mZoomAnimator;
+        private Runnable mHighlightAnimator;
+        private int mHighlightStep;
+    }
+
+    private int computeVisibleRows(int ydim) {
+        // If we resize, then move the bottom row down.  Don't allow the scroll
+        // to waste space at the bottom.
+        int offsetY = mScrollOffsetY;
+        int spaceNeeded = mNumRows * rowYSpace;
+        if (offsetY + ydim > spaceNeeded) {
+            offsetY = spaceNeeded - ydim;
+            if (offsetY < 0) {
+                offsetY = 0;
+            }
+        }
+        mStartRow = offsetY / rowYSpace;
+        mEndRow = (offsetY + ydim) / rowYSpace;
+        if (mEndRow >= mNumRows) {
+            mEndRow = mNumRows - 1;
+        }
+
+        return offsetY;
+    }
+
+    private void startHighlighting() {
+        // System.out.printf("startHighlighting()\n");
+        mSurface.mHighlightStep = 0;
+        mSurface.mFadeColors = true;
+        // Force a recomputation of the color strips
+        mSurface.mCachedEndRow = -1;
+        getDisplay().timerExec(0, mSurface.mHighlightAnimator);
+    }
+
+    private static class RowData {
+        RowData(Row row) {
+            mName = row.getName();
+            mStack = new ArrayList<Block>();
+        }
+
+        public void push(Block block) {
+            mStack.add(block);
+        }
+
+        public Block top() {
+            if (mStack.size() == 0)
+                return null;
+            return mStack.get(mStack.size() - 1);
+        }
+
+        public void pop() {
+            if (mStack.size() == 0)
+                return;
+            mStack.remove(mStack.size() - 1);
+        }
+
+        private String mName;
+        private int mRank;
+        private long mElapsed;
+        private long mEndTime;
+        private ArrayList<Block> mStack;
+    }
+
+    private static class Segment {
+        Segment(RowData rowData, Block block, long startTime, long endTime) {
+            mRowData = rowData;
+            if (block.isContextSwitch()) {
+                mBlock = block.getParentBlock();
+                mIsContextSwitch = true;
+            } else {
+                mBlock = block;
+            }
+            mStartTime = startTime;
+            mEndTime = endTime;
+        }
+
+        private RowData mRowData;
+        private Block mBlock;
+        private long mStartTime;
+        private long mEndTime;
+        private boolean mIsContextSwitch;
+    }
+
+    private static class Strip {
+        Strip(int x, int y, int width, int height, RowData rowData,
+                Segment segment, Color color) {
+            mX = x;
+            mY = y;
+            mWidth = width;
+            mHeight = height;
+            mRowData = rowData;
+            mSegment = segment;
+            mColor = color;
+        }
+
+        int mX;
+        int mY;
+        int mWidth;
+        int mHeight;
+        RowData mRowData;
+        Segment mSegment;
+        Color mColor;
+    }
+
+    private static class Pixel {
+        public void setFields(int start, double weight, Segment segment,
+                Color color, RowData rowData) {
+            mStart = start;
+            mMaxWeight = weight;
+            mSegment = segment;
+            mColor = color;
+            mRowData = rowData;
+        }
+
+        int mStart = -2; // some value that won't match another pixel
+        double mMaxWeight;
+        Segment mSegment;
+        Color mColor; // we need the color here because it may be faded
+        RowData mRowData;
+    }
+
+    private static class Range {
+        Range(int xStart, int width, int y, Color color) {
+            mXdim.x = xStart;
+            mXdim.y = width;
+            mY = y;
+            mColor = color;
+        }
+
+        Point mXdim = new Point(0, 0);
+        int mY;
+        Color mColor;
+    }
+}
diff --git a/traceview/src/main/java/com/android/traceview/TraceAction.java b/traceview/src/main/java/com/android/traceview/TraceAction.java
new file mode 100644
index 0000000..6717300
--- /dev/null
+++ b/traceview/src/main/java/com/android/traceview/TraceAction.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.traceview;
+
+final class TraceAction {
+    public static final int ACTION_ENTER = 0;
+    public static final int ACTION_EXIT = 1;
+    public static final int ACTION_INCOMPLETE = 2;
+
+    public final int mAction;
+    public final Call mCall;
+
+    public TraceAction(int action, Call call) {
+        mAction = action;
+        mCall = call;
+    }
+}
diff --git a/traceview/src/main/java/com/android/traceview/TraceReader.java b/traceview/src/main/java/com/android/traceview/TraceReader.java
new file mode 100644
index 0000000..fa76d27
--- /dev/null
+++ b/traceview/src/main/java/com/android/traceview/TraceReader.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2006 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.traceview;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public abstract class TraceReader {
+
+    private TraceUnits mTraceUnits;
+
+    public TraceUnits getTraceUnits() {
+        if (mTraceUnits == null)
+            mTraceUnits = new TraceUnits();
+        return mTraceUnits;
+    }
+
+    public ArrayList<TimeLineView.Record> getThreadTimeRecords() {
+        return null;
+    }
+
+    public HashMap<Integer, String> getThreadLabels() {
+        return null;
+    }
+
+    public MethodData[] getMethods() {
+        return null;
+    }
+
+    public ThreadData[] getThreads() {
+        return null;
+    }
+
+    public long getTotalCpuTime() {
+        return 0;
+    }
+
+    public long getTotalRealTime() {
+        return 0;
+    }
+
+    public boolean haveCpuTime() {
+        return false;
+    }
+
+    public boolean haveRealTime() {
+        return false;
+    }
+
+    public HashMap<String, String> getProperties() {
+        return null;
+    }
+
+    public ProfileProvider getProfileProvider() {
+        return null;
+    }
+
+    public TimeBase getPreferredTimeBase() {
+        return TimeBase.CPU_TIME;
+    }
+
+    public String getClockSource() {
+        return null;
+    }
+}
diff --git a/traceview/src/main/java/com/android/traceview/TraceUnits.java b/traceview/src/main/java/com/android/traceview/TraceUnits.java
new file mode 100644
index 0000000..20938f5
--- /dev/null
+++ b/traceview/src/main/java/com/android/traceview/TraceUnits.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2006 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.traceview;
+
+import java.text.DecimalFormat;
+
+// This should be a singleton.
+public class TraceUnits {
+
+    private TimeScale mTimeScale = TimeScale.MicroSeconds;
+    private double mScale = 1.0;
+    DecimalFormat mFormatter = new DecimalFormat();
+
+    public double getScaledValue(long value) {
+        return value * mScale;
+    }
+
+    public double getScaledValue(double value) {
+        return value * mScale;
+    }
+
+    public String valueOf(long value) {
+        return valueOf((double) value);
+    }
+
+    public String valueOf(double value) {
+        String pattern;
+        double scaled = value * mScale;
+        if ((int) scaled == scaled)
+            pattern = "###,###";
+        else
+            pattern = "###,###.###";
+        mFormatter.applyPattern(pattern);
+        return mFormatter.format(scaled);
+    }
+
+    public String labelledString(double value) {
+        String units = label();
+        String num = valueOf(value);
+        return String.format("%s: %s", units, num);
+    }
+
+    public String labelledString(long value) {
+        return labelledString((double) value);
+    }
+
+    public String label() {
+        if (mScale == 1.0)
+            return "usec";
+        if (mScale == 0.001)
+            return "msec";
+        if (mScale == 0.000001)
+            return "sec";
+        return null;
+    }
+
+    public void setTimeScale(TimeScale val) {
+        mTimeScale = val;
+        switch (val) {
+        case Seconds:
+            mScale = 0.000001;
+            break;
+        case MilliSeconds:
+            mScale = 0.001;
+            break;
+        case MicroSeconds:
+            mScale = 1.0;
+            break;
+        }
+    }
+
+    public TimeScale getTimeScale() {
+        return mTimeScale;
+    }
+
+    public enum TimeScale {
+        Seconds, MilliSeconds, MicroSeconds
+    };
+}
diff --git a/traceview/src/main/resources/icons/sort_down.png b/traceview/src/main/resources/icons/sort_down.png
new file mode 100644
index 0000000..2d4ccc1
Binary files /dev/null and b/traceview/src/main/resources/icons/sort_down.png differ
diff --git a/traceview/src/main/resources/icons/sort_up.png b/traceview/src/main/resources/icons/sort_up.png
new file mode 100644
index 0000000..3a0bc3c
Binary files /dev/null and b/traceview/src/main/resources/icons/sort_up.png differ
diff --git a/traceview/src/main/resources/icons/traceview-128.png b/traceview/src/main/resources/icons/traceview-128.png
new file mode 100644
index 0000000..5b4eff1
Binary files /dev/null and b/traceview/src/main/resources/icons/traceview-128.png differ
diff --git a/uiautomatorviewer/.classpath b/uiautomatorviewer/.classpath
new file mode 100644
index 0000000..1ce5b77
--- /dev/null
+++ b/uiautomatorviewer/.classpath
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry excluding="images" kind="src" path="src/main/java"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+	<classpathentry combineaccessrules="false" kind="src" path="/ddmlib"/>
+	<classpathentry kind="var" path="ANDROID_OUT_FRAMEWORK/swt.jar"/>
+	<classpathentry exported="true" kind="var" path="ANDROID_SRC/prebuilts/tools/common/m2/repository/com/android/external/eclipse/org-eclipse-core-commands/3.6.0/org-eclipse-core-commands-3.6.0.jar"/>
+	<classpathentry exported="true" kind="var" path="ANDROID_SRC/prebuilts/tools/common/m2/repository/com/android/external/eclipse/org-eclipse-equinox-common/3.6.0/org-eclipse-equinox-common-3.6.0.jar"/>
+	<classpathentry exported="true" kind="var" path="ANDROID_SRC/prebuilts/tools/common/m2/repository/com/android/external/eclipse/org-eclipse-jface/3.6.2/org-eclipse-jface-3.6.2.jar"/>
+	<classpathentry combineaccessrules="false" kind="src" path="/common"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/uiautomatorviewer/.project b/uiautomatorviewer/.project
new file mode 100644
index 0000000..d5a1115
--- /dev/null
+++ b/uiautomatorviewer/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>uiautomatorviewer</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>
diff --git a/uiautomatorviewer/.settings/org.eclipse.jdt.core.prefs b/uiautomatorviewer/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..9dbff07
--- /dev/null
+++ b/uiautomatorviewer/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,98 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
+org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault
+org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled
+org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
+org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
+org.eclipse.jdt.core.compiler.problem.deadCode=warning
+org.eclipse.jdt.core.compiler.problem.deprecation=warning
+org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
+org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
+org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
+org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning
+org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled
+org.eclipse.jdt.core.compiler.problem.fieldHiding=warning
+org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
+org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=error
+org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
+org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled
+org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
+org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning
+org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning
+org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
+org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
+org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
+org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
+org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
+org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
+org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
+org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
+org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
+org.eclipse.jdt.core.compiler.problem.nullReference=error
+org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning
+org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore
+org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
+org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning
+org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
+org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
+org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
+org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled
+org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
+org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
+org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
+org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled
+org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning
+org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error
+org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning
+org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.unusedImport=warning
+org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
+org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
+org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning
+org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled
+org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
+org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
+org.eclipse.jdt.core.compiler.source=1.6
diff --git a/uiautomatorviewer/MODULE_LICENSE_APACHE2 b/uiautomatorviewer/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
diff --git a/uiautomatorviewer/NOTICE b/uiautomatorviewer/NOTICE
new file mode 100644
index 0000000..c5b1efa
--- /dev/null
+++ b/uiautomatorviewer/NOTICE
@@ -0,0 +1,190 @@
+
+   Copyright (c) 2005-2008, 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.
+
+   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.
+
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
diff --git a/uiautomatorviewer/etc/uiautomatorviewer b/uiautomatorviewer/etc/uiautomatorviewer
new file mode 100755
index 0000000..79faf5a
--- /dev/null
+++ b/uiautomatorviewer/etc/uiautomatorviewer
@@ -0,0 +1,104 @@
+#!/bin/bash
+#
+# Copyright 2012, 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.
+
+# Set up prog to be the path of this script, including following symlinks,
+# and set up progdir to be the fully-qualified pathname of its directory.
+prog="$0"
+while [ -h "${prog}" ]; do
+    newProg=`/bin/ls -ld "${prog}"`
+    newProg=`expr "${newProg}" : ".* -> \(.*\)$"`
+    if expr "x${newProg}" : 'x/' >/dev/null; then
+        prog="${newProg}"
+    else
+        progdir=`dirname "${prog}"`
+        prog="${progdir}/${newProg}"
+    fi
+done
+oldwd=`pwd`
+progdir=`dirname "${prog}"`
+progname=`basename "${prog}"`
+cd "${progdir}"
+progdir=`pwd`
+prog="${progdir}"/"${progname}"
+cd "${oldwd}"
+
+jarfile=uiautomatorviewer.jar
+frameworkdir="$progdir"
+libdir="$progdir"
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+    frameworkdir=`dirname "$progdir"`/tools/lib
+    libdir=`dirname "$progdir"`/tools/lib
+fi
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+    frameworkdir=`dirname "$progdir"`/framework
+    libdir=`dirname "$progdir"`/lib
+fi
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+    echo "${progname}: can't find $jarfile"
+    exit 1
+fi
+
+javaCmd="java"
+
+os=`uname`
+if [ $os == 'Darwin' ]; then
+  javaOpts="-Xmx1600M -XstartOnFirstThread"
+else
+  javaOpts="-Xmx1600M"
+fi
+
+if [ `uname` = "Linux" ]; then
+    export GDK_NATIVE_WINDOWS=true
+fi
+
+while expr "x$1" : 'x-J' >/dev/null; do
+    opt=`expr "x$1" : 'x-J\(.*\)'`
+    javaOpts="${javaOpts} -${opt}"
+    shift
+done
+
+jarpath="$frameworkdir/$jarfile"
+
+# Figure out the path to the swt.jar for the current architecture.
+# if ANDROID_SWT is defined, then just use this.
+# else, if running in the Android source tree, then look for the correct swt folder in prebuilt
+# else, look for the correct swt folder in the SDK under tools/lib/
+swtpath=""
+if [ -n "$ANDROID_SWT" ]; then
+    swtpath="$ANDROID_SWT"
+else
+    vmarch=`${javaCmd} -jar "${frameworkdir}"/archquery.jar`
+    if [ -n "$ANDROID_BUILD_TOP" ]; then
+        osname=`uname -s | tr A-Z a-z`
+        swtpath="${ANDROID_BUILD_TOP}/prebuilts/tools/${osname}-${vmarch}/swt"
+    else
+        swtpath="${frameworkdir}/${vmarch}"
+    fi
+fi
+
+# Combine the swtpath and the framework dir path.
+if [ -d "$swtpath" ]; then
+    frameworkdir="${swtpath}:${frameworkdir}"
+else
+    echo "SWT folder '${swtpath}' does not exist."
+    echo "Please export ANDROID_SWT to point to the folder containing swt.jar for your platform."
+    exit 1
+fi
+
+exec "${javaCmd}" $javaOpts -Djava.ext.dirs="$frameworkdir" -Dcom.android.uiautomator.bindir="$progdir" -jar "$jarpath" "$@"
diff --git a/uiautomatorviewer/etc/uiautomatorviewer.bat b/uiautomatorviewer/etc/uiautomatorviewer.bat
new file mode 100755
index 0000000..f3f5d47
--- /dev/null
+++ b/uiautomatorviewer/etc/uiautomatorviewer.bat
@@ -0,0 +1,66 @@
+ at echo off
+rem Copyright (C) 2012 The Android Open Source Project
+rem
+rem Licensed under the Apache License, Version 2.0 (the "License");
+rem you may not use this file except in compliance with the License.
+rem You may obtain a copy of the License at
+rem
+rem      http://www.apache.org/licenses/LICENSE-2.0
+rem
+rem Unless required by applicable law or agreed to in writing, software
+rem distributed under the License is distributed on an "AS IS" BASIS,
+rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+rem See the License for the specific language governing permissions and
+rem limitations under the License.
+
+rem don't modify the caller's environment
+setlocal
+
+rem Set up prog to be the path of this script, including following symlinks,
+rem and set up progdir to be the fully-qualified pathname of its directory.
+set prog=%~f0
+
+rem Change current directory and drive to where the script is, to avoid
+rem issues with directories containing whitespaces.
+cd /d %~dp0
+
+rem Get the CWD as a full path with short names only (without spaces)
+for %%i in ("%cd%") do set prog_dir=%%~fsi
+
+rem Check we have a valid Java.exe in the path.
+set java_exe=
+call lib\find_java.bat
+if not defined java_exe goto :EOF
+
+set jarfile=uiautomatorviewer.jar
+set frameworkdir=
+
+if exist %frameworkdir%%jarfile% goto JarFileOk
+    set frameworkdir=lib\
+
+if exist %frameworkdir%%jarfile% goto JarFileOk
+    set frameworkdir=..\framework\
+
+:JarFileOk
+
+set jarpath=%frameworkdir%%jarfile%
+
+if not defined ANDROID_SWT goto QueryArch
+    set swt_path=%ANDROID_SWT%
+    goto SwtDone
+
+:QueryArch
+
+    for /f %%a in ('%java_exe% -jar %frameworkdir%archquery.jar') do set swt_path=%frameworkdir%%%a
+
+:SwtDone
+
+if exist %swt_path% goto SetPath
+    echo SWT folder '%swt_path%' does not exist.
+    echo Please set ANDROID_SWT to point to the folder containing swt.jar for your platform.
+    exit /B
+
+:SetPath
+set javaextdirs=%swt_path%;%frameworkdir%
+
+call %java_exe% -Djava.ext.dirs=%javaextdirs% -Dcom.android.uiautomator.bindir=%prog_dir% -jar %jarpath% %*
diff --git a/uiautomatorviewer/src/main/java/com/android/uiautomator/DebugBridge.java b/uiautomatorviewer/src/main/java/com/android/uiautomator/DebugBridge.java
new file mode 100644
index 0000000..bf435f6
--- /dev/null
+++ b/uiautomatorviewer/src/main/java/com/android/uiautomator/DebugBridge.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2012 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.uiautomator;
+
+import com.android.SdkConstants;
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.IDevice;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+
+public class DebugBridge {
+    private static AndroidDebugBridge sDebugBridge;
+
+    private static String getAdbLocation() {
+        String toolsDir = System.getProperty("com.android.uiautomator.bindir"); //$NON-NLS-1$
+        if (toolsDir == null) {
+            return null;
+        }
+
+        File sdk = new File(toolsDir).getParentFile();
+
+        // check if adb is present in platform-tools
+        File platformTools = new File(sdk, "platform-tools");
+        File adb = new File(platformTools, SdkConstants.FN_ADB);
+        if (adb.exists()) {
+            return adb.getAbsolutePath();
+        }
+
+        // check if adb is present in the tools directory
+        adb = new File(toolsDir, SdkConstants.FN_ADB);
+        if (adb.exists()) {
+            return adb.getAbsolutePath();
+        }
+
+        // check if we're in the Android source tree where adb is in $ANDROID_HOST_OUT/bin/adb
+        String androidOut = System.getenv("ANDROID_HOST_OUT");
+        if (androidOut != null) {
+            String adbLocation = androidOut + File.separator + "bin" + File.separator +
+                    SdkConstants.FN_ADB;
+            if (new File(adbLocation).exists()) {
+                return adbLocation;
+            }
+        }
+
+        return null;
+    }
+
+    public static void init() {
+        String adbLocation = getAdbLocation();
+        if (adbLocation != null) {
+            AndroidDebugBridge.init(false /* debugger support */);
+            sDebugBridge = AndroidDebugBridge.createBridge(adbLocation, false);
+        }
+    }
+
+    public static void terminate() {
+        if (sDebugBridge != null) {
+            sDebugBridge = null;
+            AndroidDebugBridge.terminate();
+        }
+    }
+
+    public static boolean isInitialized() {
+        return sDebugBridge != null;
+    }
+
+    public static List<IDevice> getDevices() {
+        return Arrays.asList(sDebugBridge.getDevices());
+    }
+}
diff --git a/uiautomatorviewer/src/main/java/com/android/uiautomator/OpenDialog.java b/uiautomatorviewer/src/main/java/com/android/uiautomator/OpenDialog.java
new file mode 100644
index 0000000..97a437b
--- /dev/null
+++ b/uiautomatorviewer/src/main/java/com/android/uiautomator/OpenDialog.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2012 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.uiautomator;
+
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+import java.io.File;
+
+/**
+ * Implements a file selection dialog for both screen shot and xml dump file
+ *
+ * "OK" button won't be enabled unless both files are selected
+ * It also has a convenience feature such that if one file has been picked, and the other
+ * file path is empty, then selection for the other file will start from the same base folder
+ *
+ */
+public class OpenDialog extends Dialog {
+    private static final int FIXED_TEXT_FIELD_WIDTH = 300;
+    private static final int DEFAULT_LAYOUT_SPACING = 10;
+    private Text mScreenshotText;
+    private Text mXmlText;
+    private boolean mFileChanged = false;
+    private Button mOkButton;
+
+    private static File sScreenshotFile;
+    private static File sXmlDumpFile;
+
+    /**
+     * Create the dialog.
+     * @param parentShell
+     */
+    public OpenDialog(Shell parentShell) {
+        super(parentShell);
+        setShellStyle(SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL);
+    }
+
+    /**
+     * Create contents of the dialog.
+     * @param parent
+     */
+    @Override
+    protected Control createDialogArea(Composite parent) {
+        Composite container = (Composite) super.createDialogArea(parent);
+        GridLayout gl_container = new GridLayout(1, false);
+        gl_container.verticalSpacing = DEFAULT_LAYOUT_SPACING;
+        gl_container.horizontalSpacing = DEFAULT_LAYOUT_SPACING;
+        gl_container.marginWidth = DEFAULT_LAYOUT_SPACING;
+        gl_container.marginHeight = DEFAULT_LAYOUT_SPACING;
+        container.setLayout(gl_container);
+
+        Group openScreenshotGroup = new Group(container, SWT.NONE);
+        openScreenshotGroup.setLayout(new GridLayout(2, false));
+        openScreenshotGroup.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+        openScreenshotGroup.setText("Screenshot");
+
+        mScreenshotText = new Text(openScreenshotGroup, SWT.BORDER | SWT.READ_ONLY);
+        if (sScreenshotFile != null) {
+            mScreenshotText.setText(sScreenshotFile.getAbsolutePath());
+        }
+        GridData gd_screenShotText = new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1);
+        gd_screenShotText.minimumWidth = FIXED_TEXT_FIELD_WIDTH;
+        gd_screenShotText.widthHint = FIXED_TEXT_FIELD_WIDTH;
+        mScreenshotText.setLayoutData(gd_screenShotText);
+
+        Button openScreenshotButton = new Button(openScreenshotGroup, SWT.NONE);
+        openScreenshotButton.setText("...");
+        openScreenshotButton.addListener(SWT.Selection, new Listener() {
+            @Override
+            public void handleEvent(Event event) {
+                handleOpenScreenshotFile();
+            }
+        });
+
+        Group openXmlGroup = new Group(container, SWT.NONE);
+        openXmlGroup.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+        openXmlGroup.setText("UI XML Dump");
+        openXmlGroup.setLayout(new GridLayout(2, false));
+
+        mXmlText = new Text(openXmlGroup, SWT.BORDER | SWT.READ_ONLY);
+        mXmlText.setEditable(false);
+        if (sXmlDumpFile != null) {
+            mXmlText.setText(sXmlDumpFile.getAbsolutePath());
+        }
+        GridData gd_xmlText = new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1);
+        gd_xmlText.minimumWidth = FIXED_TEXT_FIELD_WIDTH;
+        gd_xmlText.widthHint = FIXED_TEXT_FIELD_WIDTH;
+        mXmlText.setLayoutData(gd_xmlText);
+
+        Button openXmlButton = new Button(openXmlGroup, SWT.NONE);
+        openXmlButton.setText("...");
+        openXmlButton.addListener(SWT.Selection, new Listener() {
+            @Override
+            public void handleEvent(Event event) {
+                handleOpenXmlDumpFile();
+            }
+        });
+
+        return container;
+    }
+
+    /**
+     * Create contents of the button bar.
+     * @param parent
+     */
+    @Override
+    protected void createButtonsForButtonBar(Composite parent) {
+        mOkButton = createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true);
+        createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false);
+        updateButtonState();
+    }
+
+    /**
+     * Return the initial size of the dialog.
+     */
+    @Override
+    protected Point getInitialSize() {
+        return new Point(368, 233);
+    }
+
+    @Override
+    protected void configureShell(Shell newShell) {
+        super.configureShell(newShell);
+        newShell.setText("Open UI Dump Files");
+    }
+
+    private void handleOpenScreenshotFile() {
+        FileDialog fd = new FileDialog(getShell(), SWT.OPEN);
+        fd.setText("Open Screenshot File");
+        File initialFile = sScreenshotFile;
+        // if file has never been selected before, try to base initial path on the mXmlDumpFile
+        if (initialFile == null && sXmlDumpFile != null && sXmlDumpFile.isFile()) {
+            initialFile = sXmlDumpFile.getParentFile();
+        }
+        if (initialFile != null) {
+            if (initialFile.isFile()) {
+                fd.setFileName(initialFile.getAbsolutePath());
+            } else if (initialFile.isDirectory()) {
+                fd.setFilterPath(initialFile.getAbsolutePath());
+            }
+        }
+        String[] filter = {"*.png"};
+        fd.setFilterExtensions(filter);
+        String selected = fd.open();
+        if (selected != null) {
+            sScreenshotFile = new File(selected);
+            mScreenshotText.setText(selected);
+            mFileChanged = true;
+        }
+        updateButtonState();
+    }
+
+    private void handleOpenXmlDumpFile() {
+        FileDialog fd = new FileDialog(getShell(), SWT.OPEN);
+        fd.setText("Open UI Dump XML File");
+        File initialFile = sXmlDumpFile;
+        // if file has never been selected before, try to base initial path on the mScreenshotFile
+        if (initialFile == null && sScreenshotFile != null && sScreenshotFile.isFile()) {
+            initialFile = sScreenshotFile.getParentFile();
+        }
+        if (initialFile != null) {
+            if (initialFile.isFile()) {
+                fd.setFileName(initialFile.getAbsolutePath());
+            } else if (initialFile.isDirectory()) {
+                fd.setFilterPath(initialFile.getAbsolutePath());
+            }
+        }
+        String initialPath = mXmlText.getText();
+        if (initialPath.isEmpty() && sScreenshotFile != null && sScreenshotFile.isFile()) {
+            initialPath = sScreenshotFile.getParentFile().getAbsolutePath();
+        }
+        String[] filter = {"*.uix"};
+        fd.setFilterExtensions(filter);
+        String selected = fd.open();
+        if (selected != null) {
+            sXmlDumpFile = new File(selected);
+            mXmlText.setText(selected);
+            mFileChanged = true;
+        }
+        updateButtonState();
+    }
+
+    private void updateButtonState() {
+        mOkButton.setEnabled(sXmlDumpFile != null && sXmlDumpFile.isFile());
+    }
+
+    public boolean hasFileChanged() {
+        return mFileChanged;
+    }
+
+    public File getScreenshotFile() {
+        return sScreenshotFile;
+    }
+
+    public File getXmlDumpFile() {
+        return sXmlDumpFile;
+    }
+}
diff --git a/uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorHelper.java b/uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorHelper.java
new file mode 100644
index 0000000..8ead9de
--- /dev/null
+++ b/uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorHelper.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2012 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.uiautomator;
+
+import com.android.ddmlib.CollectingOutputReceiver;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.RawImage;
+import com.android.ddmlib.SyncService;
+import com.android.uiautomator.tree.BasicTreeNode;
+import com.android.uiautomator.tree.RootWindowNode;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.ImageLoader;
+import org.eclipse.swt.graphics.PaletteData;
+import org.eclipse.swt.widgets.Display;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class UiAutomatorHelper {
+    public static final int UIAUTOMATOR_MIN_API_LEVEL = 16;
+
+    private static final String UIAUTOMATOR = "/system/bin/uiautomator";    //$NON-NLS-1$
+    private static final String UIAUTOMATOR_DUMP_COMMAND = "dump";          //$NON-NLS-1$
+    private static final String UIDUMP_DEVICE_PATH = "/data/local/tmp/uidump.xml";  //$NON-NLS-1$
+    private static final int XML_CAPTURE_TIMEOUT_SEC = 40;
+
+    private static boolean supportsUiAutomator(IDevice device) {
+        String apiLevelString = device.getProperty(IDevice.PROP_BUILD_API_LEVEL);
+        int apiLevel;
+        try {
+            apiLevel = Integer.parseInt(apiLevelString);
+        } catch (NumberFormatException e) {
+            apiLevel = UIAUTOMATOR_MIN_API_LEVEL;
+        }
+
+        return apiLevel >= UIAUTOMATOR_MIN_API_LEVEL;
+    }
+
+    private static void getUiHierarchyFile(IDevice device, File dst, IProgressMonitor monitor) {
+        if (monitor == null) {
+            monitor = new NullProgressMonitor();
+        }
+
+        monitor.subTask("Deleting old UI XML snapshot ...");
+        String command = "rm " + UIDUMP_DEVICE_PATH;
+
+        try {
+            CountDownLatch commandCompleteLatch = new CountDownLatch(1);
+            device.executeShellCommand(command,
+                    new CollectingOutputReceiver(commandCompleteLatch));
+            commandCompleteLatch.await(5, TimeUnit.SECONDS);
+        } catch (Exception e1) {
+            // ignore exceptions while deleting stale files
+        }
+
+        monitor.subTask("Taking UI XML snapshot...");
+        command = String.format("%s %s %s", UIAUTOMATOR,
+                UIAUTOMATOR_DUMP_COMMAND,
+                UIDUMP_DEVICE_PATH);
+        CountDownLatch commandCompleteLatch = new CountDownLatch(1);
+
+        try {
+            device.executeShellCommand(
+                    command,
+                    new CollectingOutputReceiver(commandCompleteLatch),
+                    XML_CAPTURE_TIMEOUT_SEC * 1000);
+            commandCompleteLatch.await(XML_CAPTURE_TIMEOUT_SEC, TimeUnit.SECONDS);
+
+            monitor.subTask("Pull UI XML snapshot from device...");
+            device.getSyncService().pullFile(UIDUMP_DEVICE_PATH,
+                    dst.getAbsolutePath(), SyncService.getNullProgressMonitor());
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static UiAutomatorResult takeSnapshot(IDevice device, IProgressMonitor monitor)
+                                throws UiAutomatorException {
+        if (monitor == null) {
+            monitor = new NullProgressMonitor();
+        }
+
+        monitor.subTask("Checking if device support UI Automator");
+        if (!supportsUiAutomator(device)) {
+            String msg = "UI Automator requires a device with API Level "
+                                + UIAUTOMATOR_MIN_API_LEVEL;
+            throw new UiAutomatorException(msg, null);
+        }
+
+        monitor.subTask("Creating temporary files for uiautomator results.");
+        File tmpDir = null;
+        File xmlDumpFile = null;
+        File screenshotFile = null;
+        try {
+            tmpDir = File.createTempFile("uiautomatorviewer_", "");
+            tmpDir.delete();
+            if (!tmpDir.mkdirs())
+                throw new IOException("Failed to mkdir");
+            xmlDumpFile = File.createTempFile("dump_", ".uix", tmpDir);
+            screenshotFile = File.createTempFile("screenshot_", ".png", tmpDir);
+        } catch (Exception e) {
+            String msg = "Error while creating temporary file to save snapshot: "
+                    + e.getMessage();
+            throw new UiAutomatorException(msg, e);
+        }
+
+        tmpDir.deleteOnExit();
+        xmlDumpFile.deleteOnExit();
+        screenshotFile.deleteOnExit();
+
+        monitor.subTask("Obtaining UI hierarchy");
+        try {
+            UiAutomatorHelper.getUiHierarchyFile(device, xmlDumpFile, monitor);
+        } catch (Exception e) {
+            String msg = "Error while obtaining UI hierarchy XML file: " + e.getMessage();
+            throw new UiAutomatorException(msg, e);
+        }
+
+        UiAutomatorModel model;
+        try {
+            model = new UiAutomatorModel(xmlDumpFile);
+        } catch (Exception e) {
+            String msg = "Error while parsing UI hierarchy XML file: " + e.getMessage();
+            throw new UiAutomatorException(msg, e);
+        }
+
+        monitor.subTask("Obtaining device screenshot");
+        RawImage rawImage;
+        try {
+            rawImage = device.getScreenshot();
+        } catch (Exception e) {
+            String msg = "Error taking device screenshot: " + e.getMessage();
+            throw new UiAutomatorException(msg, e);
+        }
+
+        // rotate the screen shot per device rotation
+        BasicTreeNode root = model.getXmlRootNode();
+        if (root instanceof RootWindowNode) {
+            for (int i = 0; i < ((RootWindowNode)root).getRotation(); i++) {
+                rawImage = rawImage.getRotated();
+            }
+        }
+        PaletteData palette = new PaletteData(
+                rawImage.getRedMask(),
+                rawImage.getGreenMask(),
+                rawImage.getBlueMask());
+        ImageData imageData = new ImageData(rawImage.width, rawImage.height,
+                rawImage.bpp, palette, 1, rawImage.data);
+        ImageLoader loader = new ImageLoader();
+        loader.data = new ImageData[] { imageData };
+        loader.save(screenshotFile.getAbsolutePath(), SWT.IMAGE_PNG);
+        Image screenshot = new Image(Display.getDefault(), imageData);
+
+        return new UiAutomatorResult(xmlDumpFile, model, screenshot);
+    }
+
+    @SuppressWarnings("serial")
+    public static class UiAutomatorException extends Exception {
+        public UiAutomatorException(String msg, Throwable t) {
+            super(msg, t);
+        }
+    }
+
+    public static class UiAutomatorResult {
+        public final File uiHierarchy;
+        public final UiAutomatorModel model;
+        public final Image screenshot;
+
+        public UiAutomatorResult(File uiXml, UiAutomatorModel m, Image s) {
+            uiHierarchy = uiXml;
+            model = m;
+            screenshot = s;
+        }
+    }
+}
diff --git a/uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorModel.java b/uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorModel.java
new file mode 100644
index 0000000..c724f8b
--- /dev/null
+++ b/uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorModel.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2012 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.uiautomator;
+
+import com.android.uiautomator.tree.BasicTreeNode;
+import com.android.uiautomator.tree.BasicTreeNode.IFindNodeListener;
+import com.android.uiautomator.tree.UiHierarchyXmlLoader;
+import com.android.uiautomator.tree.UiNode;
+
+import org.eclipse.swt.graphics.Rectangle;
+
+import java.io.File;
+import java.util.List;
+
+public class UiAutomatorModel {
+    private BasicTreeNode mRootNode;
+    private BasicTreeNode mSelectedNode;
+    private Rectangle mCurrentDrawingRect;
+    private List<Rectangle> mNafNodes;
+
+    // determines whether we lookup the leaf UI node on mouse move of screenshot image
+    private boolean mExploreMode = true;
+
+    private boolean mShowNafNodes = false;
+
+    public UiAutomatorModel(File xmlDumpFile) {
+        UiHierarchyXmlLoader loader = new UiHierarchyXmlLoader();
+        BasicTreeNode rootNode = loader.parseXml(xmlDumpFile.getAbsolutePath());
+        if (rootNode == null) {
+            System.err.println("null rootnode after parsing.");
+            throw new IllegalArgumentException("Invalid ui automator hierarchy file.");
+        }
+
+        mNafNodes = loader.getNafNodes();
+        if (mRootNode != null) {
+            mRootNode.clearAllChildren();
+        }
+
+        mRootNode = rootNode;
+        mExploreMode = true;
+    }
+
+    public BasicTreeNode getXmlRootNode() {
+        return mRootNode;
+    }
+
+    public BasicTreeNode getSelectedNode() {
+        return mSelectedNode;
+    }
+
+    /**
+     * change node selection in the Model recalculate the rect to highlight,
+     * also notifies the View to refresh accordingly
+     *
+     * @param node
+     */
+    public void setSelectedNode(BasicTreeNode node) {
+        mSelectedNode = node;
+        if (mSelectedNode instanceof UiNode) {
+            UiNode uiNode = (UiNode) mSelectedNode;
+            mCurrentDrawingRect = new Rectangle(uiNode.x, uiNode.y, uiNode.width, uiNode.height);
+        } else {
+            mCurrentDrawingRect = null;
+        }
+    }
+
+    public Rectangle getCurrentDrawingRect() {
+        return mCurrentDrawingRect;
+    }
+
+    /**
+     * Do a search in tree to find a leaf node or deepest parent node containing the coordinate
+     *
+     * @param x
+     * @param y
+     * @return
+     */
+    public BasicTreeNode updateSelectionForCoordinates(int x, int y) {
+        BasicTreeNode node = null;
+
+        if (mRootNode != null) {
+            MinAreaFindNodeListener listener = new MinAreaFindNodeListener();
+            boolean found = mRootNode.findLeafMostNodesAtPoint(x, y, listener);
+            if (found && listener.mNode != null && !listener.mNode.equals(mSelectedNode)) {
+                node = listener.mNode;
+            }
+        }
+
+        return node;
+    }
+
+    public boolean isExploreMode() {
+        return mExploreMode;
+    }
+
+    public void toggleExploreMode() {
+        mExploreMode = !mExploreMode;
+    }
+
+    public void setExploreMode(boolean exploreMode) {
+        mExploreMode = exploreMode;
+    }
+
+    private static class MinAreaFindNodeListener implements IFindNodeListener {
+        BasicTreeNode mNode = null;
+        @Override
+        public void onFoundNode(BasicTreeNode node) {
+            if (mNode == null) {
+                mNode = node;
+            } else {
+                if ((node.height * node.width) < (mNode.height * mNode.width)) {
+                    mNode = node;
+                }
+            }
+        }
+    }
+
+    public List<Rectangle> getNafNodes() {
+        return mNafNodes;
+    }
+
+    public void toggleShowNaf() {
+        mShowNafNodes = !mShowNafNodes;
+    }
+
+    public boolean shouldShowNafNodes() {
+        return mShowNafNodes;
+    }
+}
diff --git a/uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorView.java b/uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorView.java
new file mode 100644
index 0000000..c5f3e59
--- /dev/null
+++ b/uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorView.java
@@ -0,0 +1,436 @@
+/*
+ * Copyright (C) 2012 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.uiautomator;
+
+import com.android.uiautomator.actions.ExpandAllAction;
+import com.android.uiautomator.actions.ToggleNafAction;
+import com.android.uiautomator.tree.AttributePair;
+import com.android.uiautomator.tree.BasicTreeNode;
+import com.android.uiautomator.tree.BasicTreeNodeContentProvider;
+
+import org.eclipse.jface.action.ToolBarManager;
+import org.eclipse.jface.layout.TableColumnLayout;
+import org.eclipse.jface.viewers.ArrayContentProvider;
+import org.eclipse.jface.viewers.CellEditor;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.ColumnWeightData;
+import org.eclipse.jface.viewers.EditingSupport;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.TextCellEditor;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.custom.StackLayout;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseMoveListener;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.ImageLoader;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.graphics.Transform;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.Tree;
+
+import java.io.File;
+
+public class UiAutomatorView extends Composite {
+    private static final int IMG_BORDER = 2;
+
+    // The screenshot area is made of a stack layout of two components: screenshot canvas and
+    // a "specify screenshot" button. If a screenshot is already available, then that is displayed
+    // on the canvas. If it is not availble, then the "specify screenshot" button is displayed.
+    private Composite mScreenshotComposite;
+    private StackLayout mStackLayout;
+    private Composite mSetScreenshotComposite;
+    private Canvas mScreenshotCanvas;
+
+    private TreeViewer mTreeViewer;
+    private TableViewer mTableViewer;
+
+    private float mScale = 1.0f;
+    private int mDx, mDy;
+
+    private UiAutomatorModel mModel;
+    private File mModelFile;
+    private Image mScreenshot;
+
+    public UiAutomatorView(Composite parent, int style) {
+        super(parent, SWT.NONE);
+        setLayout(new FillLayout());
+
+        SashForm baseSash = new SashForm(this, SWT.HORIZONTAL);
+
+        mScreenshotComposite = new Composite(baseSash, SWT.BORDER);
+        mStackLayout = new StackLayout();
+        mScreenshotComposite.setLayout(mStackLayout);
+
+        // draw the canvas with border, so the divider area for sash form can be highlighted
+        mScreenshotCanvas = new Canvas(mScreenshotComposite, SWT.BORDER);
+        mStackLayout.topControl = mScreenshotCanvas;
+        mScreenshotComposite.layout();
+
+        mScreenshotCanvas.addMouseListener(new MouseAdapter() {
+            @Override
+            public void mouseUp(MouseEvent e) {
+                if (mModel != null) {
+                    mModel.toggleExploreMode();
+                    redrawScreenshot();
+                }
+            }
+        });
+        mScreenshotCanvas.setBackground(
+                getShell().getDisplay().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND));
+        mScreenshotCanvas.addPaintListener(new PaintListener() {
+            @Override
+            public void paintControl(PaintEvent e) {
+                if (mScreenshot != null) {
+                    updateScreenshotTransformation();
+                    // shifting the image here, so that there's a border around screen shot
+                    // this makes highlighting red rectangles on the screen shot edges more visible
+                    Transform t = new Transform(e.gc.getDevice());
+                    t.translate(mDx, mDy);
+                    t.scale(mScale, mScale);
+                    e.gc.setTransform(t);
+                    e.gc.drawImage(mScreenshot, 0, 0);
+                    // this resets the transformation to identity transform, i.e. no change
+                    // we don't use transformation here because it will cause the line pattern
+                    // and line width of highlight rect to be scaled, causing to appear to be blurry
+                    e.gc.setTransform(null);
+                    if (mModel.shouldShowNafNodes()) {
+                        // highlight the "Not Accessibility Friendly" nodes
+                        e.gc.setForeground(e.gc.getDevice().getSystemColor(SWT.COLOR_YELLOW));
+                        e.gc.setBackground(e.gc.getDevice().getSystemColor(SWT.COLOR_YELLOW));
+                        for (Rectangle r : mModel.getNafNodes()) {
+                            e.gc.setAlpha(50);
+                            e.gc.fillRectangle(mDx + getScaledSize(r.x), mDy + getScaledSize(r.y),
+                                    getScaledSize(r.width), getScaledSize(r.height));
+                            e.gc.setAlpha(255);
+                            e.gc.setLineStyle(SWT.LINE_SOLID);
+                            e.gc.setLineWidth(2);
+                            e.gc.drawRectangle(mDx + getScaledSize(r.x), mDy + getScaledSize(r.y),
+                                    getScaledSize(r.width), getScaledSize(r.height));
+                        }
+                    }
+                    // draw the mouseover rects
+                    Rectangle rect = mModel.getCurrentDrawingRect();
+                    if (rect != null) {
+                        e.gc.setForeground(e.gc.getDevice().getSystemColor(SWT.COLOR_RED));
+                        if (mModel.isExploreMode()) {
+                            // when we highlight nodes dynamically on mouse move,
+                            // use dashed borders
+                            e.gc.setLineStyle(SWT.LINE_DASH);
+                            e.gc.setLineWidth(1);
+                        } else {
+                            // when highlighting nodes on tree node selection,
+                            // use solid borders
+                            e.gc.setLineStyle(SWT.LINE_SOLID);
+                            e.gc.setLineWidth(2);
+                        }
+                        e.gc.drawRectangle(mDx + getScaledSize(rect.x), mDy + getScaledSize(rect.y),
+                                getScaledSize(rect.width), getScaledSize(rect.height));
+                    }
+                }
+            }
+        });
+        mScreenshotCanvas.addMouseMoveListener(new MouseMoveListener() {
+            @Override
+            public void mouseMove(MouseEvent e) {
+                if (mModel != null && mModel.isExploreMode()) {
+                    BasicTreeNode node = mModel.updateSelectionForCoordinates(
+                            getInverseScaledSize(e.x - mDx),
+                            getInverseScaledSize(e.y - mDy));
+                    if (node != null) {
+                        updateTreeSelection(node);
+                    }
+                }
+            }
+        });
+
+        mSetScreenshotComposite = new Composite(mScreenshotComposite, SWT.NONE);
+        mSetScreenshotComposite.setLayout(new GridLayout());
+
+        final Button setScreenshotButton = new Button(mSetScreenshotComposite, SWT.PUSH);
+        setScreenshotButton.setText("Specify Screenshot...");
+        setScreenshotButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                FileDialog fd = new FileDialog(setScreenshotButton.getShell());
+                fd.setFilterExtensions(new String[] { "*.png" });
+                if (mModelFile != null) {
+                    fd.setFilterPath(mModelFile.getParent());
+                }
+                String screenshotPath = fd.open();
+                if (screenshotPath == null) {
+                    return;
+                }
+
+                ImageData[] data;
+                try {
+                    data = new ImageLoader().load(screenshotPath);
+                } catch (Exception e) {
+                    return;
+                }
+
+                // "data" is an array, probably used to handle images that has multiple frames
+                // i.e. gifs or icons, we just care if it has at least one here
+                if (data.length < 1) {
+                    return;
+                }
+
+                mScreenshot = new Image(Display.getDefault(), data[0]);
+                redrawScreenshot();
+            }
+        });
+
+
+        // right sash is split into 2 parts: upper-right and lower-right
+        // both are composites with borders, so that the horizontal divider can be highlighted by
+        // the borders
+        SashForm rightSash = new SashForm(baseSash, SWT.VERTICAL);
+
+        // upper-right base contains the toolbar and the tree
+        Composite upperRightBase = new Composite(rightSash, SWT.BORDER);
+        upperRightBase.setLayout(new GridLayout(1, false));
+
+        ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT);
+        toolBarManager.add(new ExpandAllAction(this));
+        toolBarManager.add(new ToggleNafAction(this));
+        toolBarManager.createControl(upperRightBase);
+
+        mTreeViewer = new TreeViewer(upperRightBase, SWT.NONE);
+        mTreeViewer.setContentProvider(new BasicTreeNodeContentProvider());
+        // default LabelProvider uses toString() to generate text to display
+        mTreeViewer.setLabelProvider(new LabelProvider());
+        mTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+            @Override
+            public void selectionChanged(SelectionChangedEvent event) {
+                BasicTreeNode selectedNode = null;
+                if (event.getSelection() instanceof IStructuredSelection) {
+                    IStructuredSelection selection = (IStructuredSelection) event.getSelection();
+                    Object o = selection.getFirstElement();
+                    if (o instanceof BasicTreeNode) {
+                        selectedNode = (BasicTreeNode) o;
+                    }
+                }
+
+                mModel.setSelectedNode(selectedNode);
+                redrawScreenshot();
+                if (selectedNode != null) {
+                    loadAttributeTable();
+                }
+            }
+        });
+        Tree tree = mTreeViewer.getTree();
+        tree.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
+        // move focus so that it's not on tool bar (looks weird)
+        tree.setFocus();
+
+        // lower-right base contains the detail group
+        Composite lowerRightBase = new Composite(rightSash, SWT.BORDER);
+        lowerRightBase.setLayout(new FillLayout());
+        Group grpNodeDetail = new Group(lowerRightBase, SWT.NONE);
+        grpNodeDetail.setLayout(new FillLayout(SWT.HORIZONTAL));
+        grpNodeDetail.setText("Node Detail");
+
+        Composite tableContainer = new Composite(grpNodeDetail, SWT.NONE);
+
+        TableColumnLayout columnLayout = new TableColumnLayout();
+        tableContainer.setLayout(columnLayout);
+
+        mTableViewer = new TableViewer(tableContainer, SWT.NONE | SWT.FULL_SELECTION);
+        Table table = mTableViewer.getTable();
+        table.setLinesVisible(true);
+        // use ArrayContentProvider here, it assumes the input to the TableViewer
+        // is an array, where each element represents a row in the table
+        mTableViewer.setContentProvider(new ArrayContentProvider());
+
+        TableViewerColumn tableViewerColumnKey = new TableViewerColumn(mTableViewer, SWT.NONE);
+        TableColumn tblclmnKey = tableViewerColumnKey.getColumn();
+        tableViewerColumnKey.setLabelProvider(new ColumnLabelProvider() {
+            @Override
+            public String getText(Object element) {
+                if (element instanceof AttributePair) {
+                    // first column, shows the attribute name
+                    return ((AttributePair)element).key;
+                }
+                return super.getText(element);
+            }
+        });
+        columnLayout.setColumnData(tblclmnKey,
+                new ColumnWeightData(1, ColumnWeightData.MINIMUM_WIDTH, true));
+
+        TableViewerColumn tableViewerColumnValue = new TableViewerColumn(mTableViewer, SWT.NONE);
+        tableViewerColumnValue.setEditingSupport(new AttributeTableEditingSupport(mTableViewer));
+        TableColumn tblclmnValue = tableViewerColumnValue.getColumn();
+        columnLayout.setColumnData(tblclmnValue,
+                new ColumnWeightData(2, ColumnWeightData.MINIMUM_WIDTH, true));
+        tableViewerColumnValue.setLabelProvider(new ColumnLabelProvider() {
+            @Override
+            public String getText(Object element) {
+                if (element instanceof AttributePair) {
+                    // second column, shows the attribute value
+                    return ((AttributePair)element).value;
+                }
+                return super.getText(element);
+            }
+        });
+        // sets the ratio of the vertical split: left 5 vs right 3
+        baseSash.setWeights(new int[]{5, 3});
+    }
+
+    private int getScaledSize(int size) {
+        if (mScale == 1.0f) {
+            return size;
+        } else {
+            return new Double(Math.floor((size * mScale))).intValue();
+        }
+    }
+
+    private int getInverseScaledSize(int size) {
+        if (mScale == 1.0f) {
+            return size;
+        } else {
+            return new Double(Math.floor((size / mScale))).intValue();
+        }
+    }
+
+    private void updateScreenshotTransformation() {
+        Rectangle canvas = mScreenshotCanvas.getBounds();
+        Rectangle image = mScreenshot.getBounds();
+        float scaleX = (canvas.width - 2 * IMG_BORDER - 1) / (float)image.width;
+        float scaleY = (canvas.height - 2 * IMG_BORDER - 1) / (float)image.height;
+        // use the smaller scale here so that we can fit the entire screenshot
+        mScale = Math.min(scaleX, scaleY);
+        // calculate translation values to center the image on the canvas
+        mDx = (canvas.width - getScaledSize(image.width) - IMG_BORDER * 2) / 2 + IMG_BORDER;
+        mDy = (canvas.height - getScaledSize(image.height) - IMG_BORDER * 2) / 2 + IMG_BORDER;
+    }
+
+    private class AttributeTableEditingSupport extends EditingSupport {
+
+        private TableViewer mViewer;
+
+        public AttributeTableEditingSupport(TableViewer viewer) {
+            super(viewer);
+            mViewer = viewer;
+        }
+
+        @Override
+        protected boolean canEdit(Object arg0) {
+            return true;
+        }
+
+        @Override
+        protected CellEditor getCellEditor(Object arg0) {
+            return new TextCellEditor(mViewer.getTable());
+        }
+
+        @Override
+        protected Object getValue(Object o) {
+            return ((AttributePair)o).value;
+        }
+
+        @Override
+        protected void setValue(Object arg0, Object arg1) {
+        }
+    }
+
+    /**
+     * Causes a redraw of the canvas.
+     *
+     * The drawing code of canvas will handle highlighted nodes and etc based on data
+     * retrieved from Model
+     */
+    public void redrawScreenshot() {
+        if (mScreenshot == null) {
+            mStackLayout.topControl = mSetScreenshotComposite;
+        } else {
+            mStackLayout.topControl = mScreenshotCanvas;
+        }
+        mScreenshotComposite.layout();
+
+        mScreenshotCanvas.redraw();
+    }
+
+    public void setInputHierarchy(Object input) {
+        mTreeViewer.setInput(input);
+    }
+
+    public void loadAttributeTable() {
+        // udpate the lower right corner table to show the attributes of the node
+        mTableViewer.setInput(mModel.getSelectedNode().getAttributesArray());
+    }
+
+    public void expandAll() {
+        mTreeViewer.expandAll();
+    }
+
+    public void updateTreeSelection(BasicTreeNode node) {
+        mTreeViewer.setSelection(new StructuredSelection(node), true);
+    }
+
+    public void setModel(UiAutomatorModel model, File modelBackingFile, Image screenshot) {
+        mModel = model;
+        mModelFile = modelBackingFile;
+
+        if (mScreenshot != null) {
+            mScreenshot.dispose();
+        }
+        mScreenshot = screenshot;
+
+        redrawScreenshot();
+        // load xml into tree
+        BasicTreeNode wrapper = new BasicTreeNode();
+        // putting another root node on top of existing root node
+        // because Tree seems to like to hide the root node
+        wrapper.addChild(mModel.getXmlRootNode());
+        setInputHierarchy(wrapper);
+        mTreeViewer.getTree().setFocus();
+
+    }
+
+    public boolean shouldShowNafNodes() {
+        return mModel != null ? mModel.shouldShowNafNodes() : false;
+    }
+
+    public void toggleShowNaf() {
+        if (mModel != null) {
+            mModel.toggleShowNaf();
+        }
+    }
+}
diff --git a/uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorViewer.java b/uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorViewer.java
new file mode 100644
index 0000000..37018b4
--- /dev/null
+++ b/uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorViewer.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2012 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.uiautomator;
+
+import com.android.uiautomator.actions.OpenFilesAction;
+import com.android.uiautomator.actions.ScreenshotAction;
+
+import org.eclipse.jface.action.ToolBarManager;
+import org.eclipse.jface.window.ApplicationWindow;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.ToolBar;
+
+import java.io.File;
+
+public class UiAutomatorViewer extends ApplicationWindow {
+    private UiAutomatorView mUiAutomatorView;
+
+    public UiAutomatorViewer() {
+        super(null);
+    }
+
+    @Override
+    protected Control createContents(Composite parent) {
+        Composite c = new Composite(parent, SWT.BORDER);
+
+        GridLayout gridLayout = new GridLayout(1, false);
+        gridLayout.marginWidth = 0;
+        gridLayout.marginHeight = 0;
+        gridLayout.horizontalSpacing = 0;
+        gridLayout.verticalSpacing = 0;
+        c.setLayout(gridLayout);
+
+        GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+        c.setLayoutData(gd);
+
+        ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT);
+        toolBarManager.add(new OpenFilesAction(this));
+        toolBarManager.add(new ScreenshotAction(this));
+        ToolBar tb = toolBarManager.createControl(c);
+        tb.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+        mUiAutomatorView = new UiAutomatorView(c, SWT.BORDER);
+        mUiAutomatorView.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+        return parent;
+    }
+
+    public static void main(String args[]) {
+        DebugBridge.init();
+
+        try {
+            UiAutomatorViewer window = new UiAutomatorViewer();
+            window.setBlockOnOpen(true);
+            window.open();
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            DebugBridge.terminate();
+        }
+    }
+
+    @Override
+    protected void configureShell(Shell newShell) {
+        super.configureShell(newShell);
+        newShell.setText("UI Automator Viewer");
+    }
+
+    @Override
+    protected Point getInitialSize() {
+        return new Point(800, 600);
+    }
+
+    public void setModel(final UiAutomatorModel model, final File modelFile,
+                                                                final Image screenshot) {
+        if (Display.getDefault().getThread() != Thread.currentThread()) {
+            Display.getDefault().syncExec(new Runnable() {
+                @Override
+                public void run() {
+                    mUiAutomatorView.setModel(model, modelFile, screenshot);
+                }
+            });
+        } else {
+            mUiAutomatorView.setModel(model, modelFile, screenshot);
+        }
+    }
+}
diff --git a/uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ExpandAllAction.java b/uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ExpandAllAction.java
new file mode 100644
index 0000000..a37539b
--- /dev/null
+++ b/uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ExpandAllAction.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2012 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.uiautomator.actions;
+
+import com.android.uiautomator.UiAutomatorView;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.resource.ImageDescriptor;
+
+public class ExpandAllAction extends Action {
+
+    UiAutomatorView mView;
+
+    public ExpandAllAction(UiAutomatorView view) {
+        super("&Expand All");
+        mView = view;;
+    }
+
+    @Override
+    public ImageDescriptor getImageDescriptor() {
+        return ImageHelper.loadImageDescriptorFromResource("images/expandall.png");
+    }
+
+    @Override
+    public void run() {
+        mView.expandAll();
+    }
+}
diff --git a/uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ImageHelper.java b/uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ImageHelper.java
new file mode 100644
index 0000000..603b226
--- /dev/null
+++ b/uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ImageHelper.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2012 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.uiautomator.actions;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.ImageLoader;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class ImageHelper {
+
+    public static ImageDescriptor loadImageDescriptorFromResource(String path) {
+        InputStream is = ImageHelper.class.getClassLoader().getResourceAsStream(path);
+        if (is != null) {
+            ImageData[] data = null;
+            try {
+                data = new ImageLoader().load(is);
+            } catch (SWTException e) {
+            } finally {
+                try {
+                    is.close();
+                } catch (IOException e) {
+                }
+            }
+            if (data != null && data.length > 0) {
+                return ImageDescriptor.createFromImageData(data[0]);
+            }
+        }
+        return null;
+    }
+}
diff --git a/uiautomatorviewer/src/main/java/com/android/uiautomator/actions/OpenFilesAction.java b/uiautomatorviewer/src/main/java/com/android/uiautomator/actions/OpenFilesAction.java
new file mode 100644
index 0000000..46ee9b6
--- /dev/null
+++ b/uiautomatorviewer/src/main/java/com/android/uiautomator/actions/OpenFilesAction.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2012 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.uiautomator.actions;
+
+import com.android.uiautomator.OpenDialog;
+import com.android.uiautomator.UiAutomatorModel;
+import com.android.uiautomator.UiAutomatorViewer;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.ImageLoader;
+import org.eclipse.swt.widgets.Display;
+
+import java.io.File;
+
+public class OpenFilesAction extends Action {
+    private UiAutomatorViewer mViewer;
+
+    public OpenFilesAction(UiAutomatorViewer viewer) {
+        super("&Open");
+
+        mViewer = viewer;
+    }
+
+    @Override
+    public ImageDescriptor getImageDescriptor() {
+        return ImageHelper.loadImageDescriptorFromResource("images/open-folder.png");
+    }
+
+    @Override
+    public void run() {
+        OpenDialog d = new OpenDialog(Display.getDefault().getActiveShell());
+        if (d.open() != OpenDialog.OK) {
+            return;
+        }
+
+        UiAutomatorModel model;
+        try {
+            model = new UiAutomatorModel(d.getXmlDumpFile());
+        } catch (Exception e) {
+            // FIXME: show error
+            return;
+        }
+
+        Image img = null;
+        File screenshot = d.getScreenshotFile();
+        if (screenshot != null) {
+            try {
+                ImageData[] data = new ImageLoader().load(screenshot.getAbsolutePath());
+
+                // "data" is an array, probably used to handle images that has multiple frames
+                // i.e. gifs or icons, we just care if it has at least one here
+                if (data.length < 1) {
+                    throw new RuntimeException("Unable to load image: "
+                            + screenshot.getAbsolutePath());
+                }
+
+                img = new Image(Display.getDefault(), data[0]);
+            } catch (Exception e) {
+                // FIXME: show error
+                return;
+            }
+        }
+
+        mViewer.setModel(model, d.getXmlDumpFile(), img);
+    }
+}
diff --git a/uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ScreenshotAction.java b/uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ScreenshotAction.java
new file mode 100644
index 0000000..700b041
--- /dev/null
+++ b/uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ScreenshotAction.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2012 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.uiautomator.actions;
+
+import com.android.ddmlib.IDevice;
+import com.android.uiautomator.DebugBridge;
+import com.android.uiautomator.UiAutomatorHelper;
+import com.android.uiautomator.UiAutomatorHelper.UiAutomatorException;
+import com.android.uiautomator.UiAutomatorHelper.UiAutomatorResult;
+import com.android.uiautomator.UiAutomatorViewer;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.dialogs.ErrorDialog;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.dialogs.ProgressMonitorDialog;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.List;
+
+public class ScreenshotAction extends Action {
+    UiAutomatorViewer mViewer;
+
+    public ScreenshotAction(UiAutomatorViewer viewer) {
+        super("&Device Screenshot");
+        mViewer = viewer;
+    }
+
+    @Override
+    public ImageDescriptor getImageDescriptor() {
+        return ImageHelper.loadImageDescriptorFromResource("images/screenshot.png");
+    }
+
+    @Override
+    public void run() {
+        if (!DebugBridge.isInitialized()) {
+            MessageDialog.openError(mViewer.getShell(),
+                    "Error obtaining Device Screenshot",
+                    "Unable to connect to adb. Check if adb is installed correctly.");
+            return;
+        }
+
+        final IDevice device = pickDevice();
+        if (device == null) {
+            return;
+        }
+
+        ProgressMonitorDialog dialog = new ProgressMonitorDialog(mViewer.getShell());
+        try {
+            dialog.run(true, false, new IRunnableWithProgress() {
+                @Override
+                public void run(IProgressMonitor monitor) throws InvocationTargetException,
+                                                                        InterruptedException {
+                    UiAutomatorResult result = null;
+                    try {
+                        result = UiAutomatorHelper.takeSnapshot(device, monitor);
+                    } catch (UiAutomatorException e) {
+                        monitor.done();
+                        showError(e.getMessage(), e);
+                        return;
+                    }
+
+                    mViewer.setModel(result.model, result.uiHierarchy, result.screenshot);
+                    monitor.done();
+                }
+            });
+        } catch (Exception e) {
+            showError("Unexpected error while obtaining UI hierarchy", e);
+        }
+    }
+
+    private void showError(final String msg, final Throwable t) {
+        mViewer.getShell().getDisplay().syncExec(new Runnable() {
+            @Override
+            public void run() {
+                Status s = new Status(IStatus.ERROR, "Screenshot", msg, t);
+                ErrorDialog.openError(
+                        mViewer.getShell(), "Error", "Error obtaining UI hierarchy", s);
+            }
+        });
+    }
+
+    private IDevice pickDevice() {
+        List<IDevice> devices = DebugBridge.getDevices();
+        if (devices.size() == 0) {
+            MessageDialog.openError(mViewer.getShell(),
+                    "Error obtaining Device Screenshot",
+                    "No Android devices were detected by adb.");
+            return null;
+        } else if (devices.size() == 1) {
+            return devices.get(0);
+        } else {
+            DevicePickerDialog dlg = new DevicePickerDialog(mViewer.getShell(), devices);
+            if (dlg.open() != Window.OK) {
+                return null;
+            }
+            return dlg.getSelectedDevice();
+        }
+    }
+
+    private static class DevicePickerDialog extends Dialog {
+        private final List<IDevice> mDevices;
+        private final String[] mDeviceNames;
+        private static int sSelectedDeviceIndex;
+
+        public DevicePickerDialog(Shell parentShell, List<IDevice> devices) {
+            super(parentShell);
+
+            mDevices = devices;
+            mDeviceNames = new String[mDevices.size()];
+            for (int i = 0; i < devices.size(); i++) {
+                mDeviceNames[i] = devices.get(i).getName();
+            }
+        }
+
+        @Override
+        protected Control createDialogArea(Composite parentShell) {
+            Composite parent = (Composite) super.createDialogArea(parentShell);
+            Composite c = new Composite(parent, SWT.NONE);
+
+            c.setLayout(new GridLayout(2, false));
+
+            Label l = new Label(c, SWT.NONE);
+            l.setText("Select device: ");
+
+            final Combo combo = new Combo(c, SWT.BORDER | SWT.READ_ONLY);
+            combo.setItems(mDeviceNames);
+            int defaultSelection =
+                    sSelectedDeviceIndex < mDevices.size() ? sSelectedDeviceIndex : 0;
+            combo.select(defaultSelection);
+            sSelectedDeviceIndex = defaultSelection;
+
+            combo.addSelectionListener(new SelectionAdapter() {
+                @Override
+                public void widgetSelected(SelectionEvent arg0) {
+                    sSelectedDeviceIndex = combo.getSelectionIndex();
+                }
+            });
+
+            return parent;
+        }
+
+        public IDevice getSelectedDevice() {
+            return mDevices.get(sSelectedDeviceIndex);
+        }
+    }
+}
diff --git a/uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ToggleNafAction.java b/uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ToggleNafAction.java
new file mode 100644
index 0000000..fe4cbfa
--- /dev/null
+++ b/uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ToggleNafAction.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2012 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.uiautomator.actions;
+
+import com.android.uiautomator.UiAutomatorView;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.resource.ImageDescriptor;
+
+public class ToggleNafAction extends Action {
+    private UiAutomatorView mView;
+
+    public ToggleNafAction(UiAutomatorView view) {
+        super("&Toggle NAF Nodes", IAction.AS_CHECK_BOX);
+        setChecked(view.shouldShowNafNodes());
+
+        mView = view;
+    }
+
+    @Override
+    public ImageDescriptor getImageDescriptor() {
+        return ImageHelper.loadImageDescriptorFromResource("images/warning.png");
+    }
+
+    @Override
+    public void run() {
+        mView.toggleShowNaf();
+        mView.redrawScreenshot();
+        setChecked(mView.shouldShowNafNodes());
+    }
+}
diff --git a/uiautomatorviewer/src/main/java/com/android/uiautomator/tree/AttributePair.java b/uiautomatorviewer/src/main/java/com/android/uiautomator/tree/AttributePair.java
new file mode 100644
index 0000000..ef59544
--- /dev/null
+++ b/uiautomatorviewer/src/main/java/com/android/uiautomator/tree/AttributePair.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2012 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.uiautomator.tree;
+
+public class AttributePair {
+    public String key, value;
+
+    public AttributePair(String key, String value) {
+        this.key = key;
+        this.value = value;
+    }
+}
diff --git a/uiautomatorviewer/src/main/java/com/android/uiautomator/tree/BasicTreeNode.java b/uiautomatorviewer/src/main/java/com/android/uiautomator/tree/BasicTreeNode.java
new file mode 100644
index 0000000..99434d1
--- /dev/null
+++ b/uiautomatorviewer/src/main/java/com/android/uiautomator/tree/BasicTreeNode.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2012 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.uiautomator.tree;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class BasicTreeNode {
+
+    private static final BasicTreeNode[] CHILDREN_TEMPLATE = new BasicTreeNode[] {};
+
+    protected BasicTreeNode mParent;
+
+    protected final List<BasicTreeNode> mChildren = new ArrayList<BasicTreeNode>();
+
+    public int x, y, width, height;
+
+    // whether the boundary fields are applicable for the node or not
+    // RootWindowNode has no bounds, but UiNodes should
+    protected boolean mHasBounds = false;
+
+    public void addChild(BasicTreeNode child) {
+        if (child == null) {
+            throw new NullPointerException("Cannot add null child");
+        }
+        if (mChildren.contains(child)) {
+            throw new IllegalArgumentException("node already a child");
+        }
+        mChildren.add(child);
+        child.mParent = this;
+    }
+
+    public List<BasicTreeNode> getChildrenList() {
+        return Collections.unmodifiableList(mChildren);
+    }
+
+    public BasicTreeNode[] getChildren() {
+        return mChildren.toArray(CHILDREN_TEMPLATE);
+    }
+
+    public BasicTreeNode getParent() {
+        return mParent;
+    }
+
+    public boolean hasChild() {
+        return mChildren.size() != 0;
+    }
+
+    public int getChildCount() {
+        return mChildren.size();
+    }
+
+    public void clearAllChildren() {
+        for (BasicTreeNode child : mChildren) {
+            child.clearAllChildren();
+        }
+        mChildren.clear();
+    }
+
+    /**
+     *
+     * Find nodes in the tree containing the coordinate
+     *
+     * The found node should have bounds covering the coordinate, and none of its children's
+     * bounds covers it. Depending on the layout, some app may have multiple nodes matching it,
+     * the caller must provide a {@link IFindNodeListener} to receive all found nodes
+     *
+     * @param px
+     * @param py
+     * @return
+     */
+    public boolean findLeafMostNodesAtPoint(int px, int py, IFindNodeListener listener) {
+        boolean foundInChild = false;
+        for (BasicTreeNode node : mChildren) {
+            foundInChild |= node.findLeafMostNodesAtPoint(px, py, listener);
+        }
+        // checked all children, if at least one child covers the point, return directly
+        if (foundInChild) return true;
+        // check self if the node has no children, or no child nodes covers the point
+        if (mHasBounds) {
+            if (x <= px && px <= x + width && y <= py && py <= y + height) {
+                listener.onFoundNode(this);
+                return true;
+            } else {
+                return false;
+            }
+        } else {
+            return false;
+        }
+    }
+
+    public Object[] getAttributesArray () {
+        return null;
+    };
+
+    public static interface IFindNodeListener {
+        void onFoundNode(BasicTreeNode node);
+    }
+}
diff --git a/uiautomatorviewer/src/main/java/com/android/uiautomator/tree/BasicTreeNodeContentProvider.java b/uiautomatorviewer/src/main/java/com/android/uiautomator/tree/BasicTreeNodeContentProvider.java
new file mode 100644
index 0000000..d78ceea
--- /dev/null
+++ b/uiautomatorviewer/src/main/java/com/android/uiautomator/tree/BasicTreeNodeContentProvider.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2012 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.uiautomator.tree;
+
+
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+public class BasicTreeNodeContentProvider implements ITreeContentProvider {
+
+    private static final Object[] EMPTY_ARRAY = {};
+
+    @Override
+    public void dispose() {
+    }
+
+    @Override
+    public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+    }
+
+    @Override
+    public Object[] getElements(Object inputElement) {
+        return getChildren(inputElement);
+    }
+
+    @Override
+    public Object[] getChildren(Object parentElement) {
+        if (parentElement instanceof BasicTreeNode) {
+            return ((BasicTreeNode)parentElement).getChildren();
+        }
+        return EMPTY_ARRAY;
+    }
+
+    @Override
+    public Object getParent(Object element) {
+        if (element instanceof BasicTreeNode) {
+            return ((BasicTreeNode)element).getParent();
+        }
+        return null;
+    }
+
+    @Override
+    public boolean hasChildren(Object element) {
+        if (element instanceof BasicTreeNode) {
+            return ((BasicTreeNode) element).hasChild();
+        }
+        return false;
+    }
+}
diff --git a/uiautomatorviewer/src/main/java/com/android/uiautomator/tree/RootWindowNode.java b/uiautomatorviewer/src/main/java/com/android/uiautomator/tree/RootWindowNode.java
new file mode 100644
index 0000000..d0e27c9
--- /dev/null
+++ b/uiautomatorviewer/src/main/java/com/android/uiautomator/tree/RootWindowNode.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2012 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.uiautomator.tree;
+
+
+
+public class RootWindowNode extends BasicTreeNode {
+
+    private final String mWindowName;
+    private Object[] mCachedAttributesArray;
+    private int mRotation;
+
+    public RootWindowNode(String windowName) {
+        this(windowName, 0);
+    }
+
+    public RootWindowNode(String windowName, int rotation) {
+        mWindowName = windowName;
+        mRotation = rotation;
+    }
+
+    @Override
+    public String toString() {
+        return mWindowName;
+    }
+
+    @Override
+    public Object[] getAttributesArray() {
+        if (mCachedAttributesArray == null) {
+            mCachedAttributesArray = new Object[]{new AttributePair("window-name", mWindowName)};
+        }
+        return mCachedAttributesArray;
+    }
+
+    public int getRotation() {
+        return mRotation;
+    }
+}
diff --git a/uiautomatorviewer/src/main/java/com/android/uiautomator/tree/UiHierarchyXmlLoader.java b/uiautomatorviewer/src/main/java/com/android/uiautomator/tree/UiHierarchyXmlLoader.java
new file mode 100644
index 0000000..2e897d9
--- /dev/null
+++ b/uiautomatorviewer/src/main/java/com/android/uiautomator/tree/UiHierarchyXmlLoader.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2012 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.uiautomator.tree;
+
+import org.eclipse.swt.graphics.Rectangle;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+public class UiHierarchyXmlLoader {
+
+    private BasicTreeNode mRootNode;
+    private List<Rectangle> mNafNodes;
+
+    public UiHierarchyXmlLoader() {
+    }
+
+    /**
+     * Uses a SAX parser to process XML dump
+     * @param xmlPath
+     * @return
+     */
+    public BasicTreeNode parseXml(String xmlPath) {
+        mRootNode = null;
+        mNafNodes = new ArrayList<Rectangle>();
+        // standard boilerplate to get a SAX parser
+        SAXParserFactory factory = SAXParserFactory.newInstance();
+        SAXParser parser = null;
+        try {
+            parser = factory.newSAXParser();
+        } catch (ParserConfigurationException e) {
+            e.printStackTrace();
+            return null;
+        } catch (SAXException e) {
+            e.printStackTrace();
+            return null;
+        }
+        // handler class for SAX parser to receiver standard parsing events:
+        // e.g. on reading "<foo>", startElement is called, on reading "</foo>",
+        // endElement is called
+        DefaultHandler handler = new DefaultHandler(){
+            BasicTreeNode mParentNode;
+            BasicTreeNode mWorkingNode;
+            @Override
+            public void startElement(String uri, String localName, String qName,
+                    Attributes attributes) throws SAXException {
+                boolean nodeCreated = false;
+                // starting an element implies that the element that has not yet been closed
+                // will be the parent of the element that is being started here
+                mParentNode = mWorkingNode;
+                if ("hierarchy".equals(qName)) {
+                    int rotation = 0;
+                    for (int i = 0; i < attributes.getLength(); i++) {
+                        if ("rotation".equals(attributes.getQName(i))) {
+                            try {
+                                rotation = Integer.parseInt(attributes.getValue(i));
+                            } catch (NumberFormatException nfe) {
+                                // do nothing
+                            }
+                        }
+                    }
+                    mWorkingNode = new RootWindowNode(attributes.getValue("windowName"), rotation);
+                    nodeCreated = true;
+                } else if ("node".equals(qName)) {
+                    UiNode tmpNode = new UiNode();
+                    for (int i = 0; i < attributes.getLength(); i++) {
+                        tmpNode.addAtrribute(attributes.getQName(i), attributes.getValue(i));
+                    }
+                    mWorkingNode = tmpNode;
+                    nodeCreated = true;
+                    // check if current node is NAF
+                    String naf = tmpNode.getAttribute("NAF");
+                    if ("true".equals(naf)) {
+                        mNafNodes.add(new Rectangle(tmpNode.x, tmpNode.y,
+                                tmpNode.width, tmpNode.height));
+                    }
+                }
+                // nodeCreated will be false if the element started is neither
+                // "hierarchy" nor "node"
+                if (nodeCreated) {
+                    if (mRootNode == null) {
+                        // this will only happen once
+                        mRootNode = mWorkingNode;
+                    }
+                    if (mParentNode != null) {
+                        mParentNode.addChild(mWorkingNode);
+                    }
+                }
+            }
+
+            @Override
+            public void endElement(String uri, String localName, String qName) throws SAXException {
+                //mParentNode should never be null here in a well formed XML
+                if (mParentNode != null) {
+                    // closing an element implies that we are back to working on
+                    // the parent node of the element just closed, i.e. continue to
+                    // parse more child nodes
+                    mWorkingNode = mParentNode;
+                    mParentNode = mParentNode.getParent();
+                }
+            }
+        };
+        try {
+            parser.parse(new File(xmlPath), handler);
+        } catch (SAXException e) {
+            e.printStackTrace();
+            return null;
+        } catch (IOException e) {
+            e.printStackTrace();
+            return null;
+        }
+        return mRootNode;
+    }
+
+    /**
+     * Returns the list of "Not Accessibility Friendly" nodes found during parsing.
+     *
+     * Call this function after parsing
+     *
+     * @return
+     */
+    public List<Rectangle> getNafNodes() {
+        return Collections.unmodifiableList(mNafNodes);
+    }
+}
diff --git a/uiautomatorviewer/src/main/java/com/android/uiautomator/tree/UiNode.java b/uiautomatorviewer/src/main/java/com/android/uiautomator/tree/UiNode.java
new file mode 100644
index 0000000..4adebf4
--- /dev/null
+++ b/uiautomatorviewer/src/main/java/com/android/uiautomator/tree/UiNode.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2012 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.uiautomator.tree;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class UiNode extends BasicTreeNode {
+    private static final Pattern BOUNDS_PATTERN = Pattern
+            .compile("\\[-?(\\d+),-?(\\d+)\\]\\[-?(\\d+),-?(\\d+)\\]");
+    // use LinkedHashMap to preserve the order of the attributes
+    private final Map<String, String> mAttributes = new LinkedHashMap<String, String>();
+    private String mDisplayName = "ShouldNotSeeMe";
+    private Object[] mCachedAttributesArray;
+
+    public void addAtrribute(String key, String value) {
+        mAttributes.put(key, value);
+        updateDisplayName();
+        if ("bounds".equals(key)) {
+            updateBounds(value);
+        }
+    }
+
+    public Map<String, String> getAttributes() {
+        return Collections.unmodifiableMap(mAttributes);
+    }
+
+    /**
+     * Builds the display name based on attributes of the node
+     */
+    private void updateDisplayName() {
+        String className = mAttributes.get("class");
+        if (className == null)
+            return;
+        String text = mAttributes.get("text");
+        if (text == null)
+            return;
+        String contentDescription = mAttributes.get("content-desc");
+        if (contentDescription == null)
+            return;
+        String index = mAttributes.get("index");
+        if (index == null)
+            return;
+        String bounds = mAttributes.get("bounds");
+        if (bounds == null) {
+            return;
+        }
+        // shorten the standard class names, otherwise it takes up too much space on UI
+        className = className.replace("android.widget.", "");
+        className = className.replace("android.view.", "");
+        StringBuilder builder = new StringBuilder();
+        builder.append('(');
+        builder.append(index);
+        builder.append(") ");
+        builder.append(className);
+        if (!text.isEmpty()) {
+            builder.append(':');
+            builder.append(text);
+        }
+        if (!contentDescription.isEmpty()) {
+            builder.append(" {");
+            builder.append(contentDescription);
+            builder.append('}');
+        }
+        builder.append(' ');
+        builder.append(bounds);
+        mDisplayName = builder.toString();
+    }
+
+    private void updateBounds(String bounds) {
+        Matcher m = BOUNDS_PATTERN.matcher(bounds);
+        if (m.matches()) {
+            x = Integer.parseInt(m.group(1));
+            y = Integer.parseInt(m.group(2));
+            width = Integer.parseInt(m.group(3)) - x;
+            height = Integer.parseInt(m.group(4)) - y;
+            mHasBounds = true;
+        } else {
+            throw new RuntimeException("Invalid bounds: " + bounds);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return mDisplayName;
+    }
+
+    public String getAttribute(String key) {
+        return mAttributes.get(key);
+    }
+
+    @Override
+    public Object[] getAttributesArray() {
+        // this approach means we do not handle the situation where an attribute is added
+        // after this function is first called. This is currently not a concern because the
+        // tree is supposed to be readonly
+        if (mCachedAttributesArray == null) {
+            mCachedAttributesArray = new Object[mAttributes.size()];
+            int i = 0;
+            for (String attr : mAttributes.keySet()) {
+                mCachedAttributesArray[i++] = new AttributePair(attr, mAttributes.get(attr));
+            }
+        }
+        return mCachedAttributesArray;
+    }
+}
diff --git a/uiautomatorviewer/src/main/java/images/expandall.png b/uiautomatorviewer/src/main/java/images/expandall.png
new file mode 100644
index 0000000..7bdf83d
Binary files /dev/null and b/uiautomatorviewer/src/main/java/images/expandall.png differ
diff --git a/uiautomatorviewer/src/main/java/images/open-folder.png b/uiautomatorviewer/src/main/java/images/open-folder.png
new file mode 100644
index 0000000..8c4a2e1
Binary files /dev/null and b/uiautomatorviewer/src/main/java/images/open-folder.png differ
diff --git a/uiautomatorviewer/src/main/java/images/screenshot.png b/uiautomatorviewer/src/main/java/images/screenshot.png
new file mode 100644
index 0000000..423f781
Binary files /dev/null and b/uiautomatorviewer/src/main/java/images/screenshot.png differ
diff --git a/uiautomatorviewer/src/main/java/images/warning.png b/uiautomatorviewer/src/main/java/images/warning.png
new file mode 100644
index 0000000..ca3b6ed
Binary files /dev/null and b/uiautomatorviewer/src/main/java/images/warning.png differ

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-java/androidsdk-tools.git



More information about the pkg-java-commits mailing list