[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